1. 背景
在 DTI(Drug-Target Interaction)预测任务中,不同数据集之间的蛋白空间是否相似,会影响模型的泛化能力。例如,在 cold protein split 或跨数据集测试中,如果测试集蛋白虽然 ID 没有出现在训练集中,但和训练集蛋白序列高度相似,那么这个任务实际上并不算特别"冷启动"。
因此,我们可以使用 MMseqs2 计算蛋白序列之间的相似度。这里主要关注两类指标:
-
数据集内部蛋白最近邻相似度
- 每个蛋白在同一个数据集内部,找到除自身外最相似的蛋白。
- 用于分析数据集内部蛋白冗余程度。
-
数据集之间蛋白最大相似度
- 每个蛋白在另一个数据集中找到最相似的蛋白。
- 用于分析不同数据集之间 target space 的覆盖程度。
本文以四个 DTI 数据集为例:
SNAP
DRH
TTD
DrugBank
数据目录如下:
/home/qfchen/DTIPred_Plus/database
结果保存目录如下:
/home/qfchen/DTIPred_Plus/analysis/results
2. 安装 MMseqs2
如果服务器上还没有安装 MMseqs2,可以使用 conda 安装:
soedinglab/MMseqs2: MMseqs2: ultra fast and sensitive search and clustering suite

conda install -c conda-forge -c bioconda mmseqs2
检查是否安装成功:
mmseqs version
3. 从 ProtInfo.xlsx 提取 FASTA 文件
每个数据集的蛋白信息保存在:
SNAP/final_data/ProtInfo.xlsx
DRH/final_data/ProtInfo.xlsx
TTD/final_data/ProtInfo.xlsx
DrugBank/final_data/ProtInfo.xlsx
每个 ProtInfo.xlsx 中至少包含两列:
UNIPROTID
SEQUENCE
下面的脚本会读取四个数据集的蛋白序列,并按 UNIPROTID 去重,导出成 FASTA 文件。
python
#!/usr/bin/env python
# File: export_protein_fasta.py
import os
import pandas as pd
BASE_DIR = "/home/qfchen/DTIPred_Plus/database"
SAVE_DIR = "/home/qfchen/DTIPred_Plus/analysis/results/protein_fasta"
DATASETS = {
"SNAP": "SNAP/final_data/ProtInfo.xlsx",
"DRH": "DRH/final_data/ProtInfo.xlsx",
"TTD": "TTD/final_data/ProtInfo.xlsx",
"DrugBank": "DrugBank/final_data/ProtInfo.xlsx",
}
def wrap_sequence(sequence, width=80):
sequence = str(sequence).strip()
return "\n".join(sequence[i:i + width] for i in range(0, len(sequence), width))
def export_one_dataset(dataset_name, rel_path):
data_path = os.path.join(BASE_DIR, rel_path)
df = pd.read_excel(data_path)[["UNIPROTID", "SEQUENCE"]].drop_duplicates("UNIPROTID")
fasta_path = os.path.join(SAVE_DIR, f"{dataset_name}.fasta")
with open(fasta_path, "w", encoding="utf-8") as f:
for _, row in df.iterrows():
uniprot_id = str(row["UNIPROTID"]).strip()
sequence = str(row["SEQUENCE"]).strip()
f.write(f">{dataset_name}|{uniprot_id}\n")
f.write(wrap_sequence(sequence) + "\n")
return {
"Dataset": dataset_name,
"Protein_N": len(df),
"FASTA": fasta_path,
}
def main():
os.makedirs(SAVE_DIR, exist_ok=True)
summary = []
for dataset_name, rel_path in DATASETS.items():
summary.append(export_one_dataset(dataset_name, rel_path))
summary_df = pd.DataFrame(summary)
summary_df.to_csv(os.path.join(SAVE_DIR, "protein_fasta_summary.csv"), index=False)
print(summary_df.to_string(index=False))
print(f"\nSaved protein FASTA files to: {SAVE_DIR}")
if __name__ == "__main__":
main()
运行:
python export_protein_fasta.py
运行后会得到:
/home/qfchen/DTIPred_Plus/analysis/results/protein_fasta/SNAP.fasta
/home/qfchen/DTIPred_Plus/analysis/results/protein_fasta/DRH.fasta
/home/qfchen/DTIPred_Plus/analysis/results/protein_fasta/TTD.fasta
/home/qfchen/DTIPred_Plus/analysis/results/protein_fasta/DrugBank.fasta
FASTA header 形式如下:
>SNAP|P05108
MLAKGLPPRSVLVKGCQTFLSAPREGLGRLRVPTG...
4. 使用 MMseqs2 计算蛋白相似度
MMseqs2 的基本命令格式如下:
mmseqs easy-search query.fasta target.fasta output.tsv tmp_dir \
--format-output query,target,pident,alnlen,evalue,bits \
--threads 16
其中:
query 查询蛋白
target 目标蛋白
pident sequence identity,百分比形式,范围 0 到 100
alnlen alignment length
evalue E-value
bits bit score
例如,计算 SNAP 到 DRH 的蛋白相似度:
mmseqs easy-search \
/home/qfchen/DTIPred_Plus/analysis/results/protein_fasta/SNAP.fasta \
/home/qfchen/DTIPred_Plus/analysis/results/protein_fasta/DRH.fasta \
/home/qfchen/DTIPred_Plus/analysis/results/protein_similarity/mmseqs_tsv/SNAP_vs_DRH.tsv \
/home/qfchen/DTIPred_Plus/analysis/results/protein_similarity/tmp/SNAP_vs_DRH \
--format-output query,target,pident,alnlen,evalue,bits \
--threads 16
5. 批量运行四个数据集两两比较
四个数据集需要计算:
bash
SNAP vs SNAP
SNAP vs DRH
SNAP vs TTD
SNAP vs DrugBank
DRH vs SNAP
DRH vs DRH
...
DrugBank vs DrugBank
一共是 4 × 4 = 16 组比较。
可以写一个 bash 脚本批量运行。
bash
#!/bin/bash
# File: run_mmseqs_protein_similarity.sh
FASTA_DIR="/home/qfchen/DTIPred_Plus/analysis/results/protein_fasta"
OUT_DIR="/home/qfchen/DTIPred_Plus/analysis/results/protein_similarity/mmseqs_tsv"
TMP_DIR="/home/qfchen/DTIPred_Plus/analysis/results/protein_similarity/tmp"
mkdir -p "$OUT_DIR"
mkdir -p "$TMP_DIR"
DATASETS=("SNAP" "DRH" "TTD" "DrugBank")
for QUERY in "${DATASETS[@]}"; do
for TARGET in "${DATASETS[@]}"; do
echo "Running ${QUERY} vs ${TARGET}"
mmseqs easy-search \
"${FASTA_DIR}/${QUERY}.fasta" \
"${FASTA_DIR}/${TARGET}.fasta" \
"${OUT_DIR}/${QUERY}_vs_${TARGET}.tsv" \
"${TMP_DIR}/${QUERY}_vs_${TARGET}" \
--format-output query,target,pident,alnlen,evalue,bits \
--threads 16
done
done
保存为:
run_mmseqs_protein_similarity.sh
运行:
bash run_mmseqs_protein_similarity.sh
6. 后台静默运行
如果不想占用终端,也不想保存日志,可以使用:
nohup bash run_mmseqs_protein_similarity.sh > /dev/null 2>&1 &
命令含义:
nohup 断开终端后继续运行
> /dev/null 标准输出不保存
2>&1 错误输出也重定向到标准输出
& 后台运行
运行后终端会显示类似:
[1] 123456
其中 123456 是后台任务的进程号。
7. 查看运行状态
查看 bash 脚本是否还在运行:
ps -ef | grep run_mmseqs_protein_similarity.sh | grep -v grep
查看 MMseqs2 进程:
ps -ef | grep mmseqs | grep -v grep
查看后台任务:
jobs -l
查看已经生成了多少结果文件:
ls /home/qfchen/DTIPred_Plus/analysis/results/protein_similarity/mmseqs_tsv
统计 .tsv 文件数量:
ls /home/qfchen/DTIPred_Plus/analysis/results/protein_similarity/mmseqs_tsv/*.tsv | wc -l
查看某个结果文件前几行:
head /home/qfchen/DTIPred_Plus/analysis/results/protein_similarity/mmseqs_tsv/SNAP_vs_DRH.tsv
8. MMseqs2 结果格式
每个 .tsv 文件包含六列:
query target pident alnlen evalue bits
例如:
bash
SNAP|P05108 DRH|P05108 100.0 521 0.0 1000
SNAP|P05108 DRH|Q9XXX1 45.2 300 1e-50 200
其中:
bash
pident = sequence identity percentage
如果 pident = 85.6,表示两个蛋白 alignment 区域的序列一致性为 85.6%。
9. 后处理:提取每个蛋白的最大相似度
我们关心的是:
-
数据集内部
- 例如 SNAP vs SNAP。
- 需要去掉
query == target的自身匹配。 - 对每个 query 取最大
pident,得到同数据集内部最近邻相似度。
-
数据集之间
- 例如 SNAP vs DRH。
- 对每个 query 取最大
pident,得到该蛋白到另一个数据集的最大相似度。
下面是后处理脚本:
python
#!/usr/bin/env python
# File: process_mmseqs_protein_similarity.py
import os
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
MMSEQS_DIR = "/home/qfchen/DTIPred_Plus/analysis/results/protein_similarity/mmseqs_tsv"
SAVE_DIR = "/home/qfchen/DTIPred_Plus/analysis/results/protein_similarity"
DATASETS = ["SNAP", "DRH", "TTD", "DrugBank"]
def read_mmseqs_result(path):
cols = ["query", "target", "pident", "alnlen", "evalue", "bits"]
return pd.read_csv(path, sep="\t", names=cols)
def summarize(values):
s = pd.Series(values, dtype="float64")
return {
"N": int(s.shape[0]),
"Mean": float(s.mean()),
"Median": float(s.median()),
"Std": float(s.std()),
"Min": float(s.min()),
"Q25": float(s.quantile(0.25)),
"Q75": float(s.quantile(0.75)),
"Q90": float(s.quantile(0.90)),
"Q95": float(s.quantile(0.95)),
"Max": float(s.max()),
"Ratio_GE_30": float((s >= 30).mean()),
"Ratio_GE_50": float((s >= 50).mean()),
"Ratio_GE_70": float((s >= 70).mean()),
"Ratio_GE_90": float((s >= 90).mean()),
}
def process_one(query_dataset, target_dataset):
path = os.path.join(MMSEQS_DIR, f"{query_dataset}_vs_{target_dataset}.tsv")
df = read_mmseqs_result(path)
if query_dataset == target_dataset:
df = df[df["query"] != df["target"]].copy()
comparison = "IntraNearestNeighbor"
else:
comparison = "InterMaxToReference"
best_df = (
df.sort_values(["query", "pident", "bits"], ascending=[True, False, False])
.drop_duplicates("query")
.copy()
)
best_df["QueryDataset"] = query_dataset
best_df["ReferenceDataset"] = target_dataset
best_df["Comparison"] = comparison
summary = {
"Comparison": comparison,
"QueryDataset": query_dataset,
"ReferenceDataset": target_dataset,
**summarize(best_df["pident"]),
}
return best_df, summary
def plot_results(intra_df, inter_df):
sns.set_theme(style="whitegrid", context="paper", font_scale=1.25)
plt.figure(figsize=(8, 5.2), dpi=300)
for dataset in DATASETS:
values = intra_df.loc[intra_df["QueryDataset"] == dataset, "pident"]
sns.kdeplot(x=values, label=dataset, linewidth=2.2, fill=False, common_norm=False)
plt.xlabel("Nearest-neighbor sequence identity (%)")
plt.ylabel("Density")
plt.title("Intra-dataset Protein Nearest-neighbor Similarity")
plt.xlim(0, 100)
plt.legend(frameon=False)
plt.tight_layout()
plt.savefig(os.path.join(SAVE_DIR, "intra_dataset_protein_similarity_density.png"), bbox_inches="tight")
plt.close()
inter_df = inter_df.copy()
inter_df["Pair"] = inter_df["QueryDataset"] + " -> " + inter_df["ReferenceDataset"]
order = [
f"{query} -> {target}"
for query in DATASETS
for target in DATASETS
if query != target
]
plt.figure(figsize=(11, 5.6), dpi=300)
sns.boxplot(data=inter_df, x="Pair", y="pident", order=order, showfliers=False)
plt.xlabel("")
plt.ylabel("Maximum sequence identity to reference dataset (%)")
plt.title("Inter-dataset Protein Maximum Similarity")
plt.ylim(0, 100)
plt.xticks(rotation=45, ha="right")
plt.tight_layout()
plt.savefig(os.path.join(SAVE_DIR, "inter_dataset_protein_similarity_boxplot.png"), bbox_inches="tight")
plt.close()
mean_matrix = inter_df.pivot_table(
index="QueryDataset",
columns="ReferenceDataset",
values="pident",
aggfunc="mean",
).reindex(index=DATASETS, columns=DATASETS)
plt.figure(figsize=(6.5, 5.6), dpi=300)
sns.heatmap(
mean_matrix,
annot=True,
fmt=".1f",
cmap="YlGnBu",
vmin=0,
vmax=100,
linewidths=0.5,
linecolor="white",
cbar_kws={"label": "Mean maximum sequence identity (%)"},
)
plt.xlabel("Reference dataset")
plt.ylabel("Query dataset")
plt.title("Inter-dataset Mean Maximum Protein Similarity")
plt.tight_layout()
plt.savefig(os.path.join(SAVE_DIR, "inter_dataset_protein_similarity_heatmap.png"), bbox_inches="tight")
plt.close()
def main():
os.makedirs(SAVE_DIR, exist_ok=True)
detail_rows = []
summary_rows = []
for query_dataset in DATASETS:
for target_dataset in DATASETS:
best_df, summary = process_one(query_dataset, target_dataset)
detail_rows.append(best_df)
summary_rows.append(summary)
detail_df = pd.concat(detail_rows, ignore_index=True)
summary_df = pd.DataFrame(summary_rows)
intra_df = detail_df[detail_df["Comparison"] == "IntraNearestNeighbor"].copy()
inter_df = detail_df[detail_df["Comparison"] == "InterMaxToReference"].copy()
intra_df.to_csv(os.path.join(SAVE_DIR, "intra_dataset_protein_similarity.csv"), index=False)
inter_df.to_csv(os.path.join(SAVE_DIR, "inter_dataset_protein_similarity.csv"), index=False)
summary_df.to_csv(os.path.join(SAVE_DIR, "protein_similarity_summary.csv"), index=False)
plot_results(intra_df, inter_df)
print(summary_df.to_string(index=False))
print(f"\nSaved protein similarity results to: {SAVE_DIR}")
if __name__ == "__main__":
main()
运行:
bash
python process_mmseqs_protein_similarity.py
10. 输出结果说明
后处理后会得到:
bash
intra_dataset_protein_similarity.csv
inter_dataset_protein_similarity.csv
protein_similarity_summary.csv
同时生成图:
bash
intra_dataset_protein_similarity_density.png
inter_dataset_protein_similarity_boxplot.png
inter_dataset_protein_similarity_heatmap.png
其中:
bash
intra_dataset_protein_similarity.csv
表示每个蛋白在同一数据集内部的最高相似蛋白。
bash
inter_dataset_protein_similarity.csv
表示每个蛋白到另一个数据集中的最高相似蛋白。
bash
protein_similarity_summary.csv
是统计汇总表,包括均值、中位数、分位数,以及大于 30%、50%、70%、90% identity 的比例。
11. 小结
整个流程如下:
bash
ProtInfo.xlsx
↓
导出 FASTA
↓
MMseqs2 easy-search 批量比较
↓
每个 query 蛋白取最大 pident
↓
得到内部最近邻相似度和跨数据集最大相似度
↓
绘制密度图、箱型图和热图
这个分析可以帮助解释:
- 哪些数据集内部蛋白冗余程度高;
- 哪些数据集之间蛋白空间更接近;
- cold protein 或跨数据集实验是否真的具有较强泛化挑战。