基于复杂网络的药物-疾病关系预测:数据清洗、特征归一化与交叉验证
2026/9/10 23:57:25 网站建设 项目流程

简介:基于复杂网络的药物疾病关系预测方法实现源码,面向生物信息、计算机、人工智能等专业学生,可支撑课程大作业、毕业设计、期末项目或科研早期探索。压缩包共9个文件,包含7个Python脚本、1份Markdown项目说明和1份CSV数据文件,整体仅69KB,轻量便携;脚本内容覆盖数据读取与预处理、Z-score标准化、副作用统计、复杂网络指标计算以及ROC/AUC评估等环节,同时提供Markdown文档说明环境配置与使用方式,结构清晰,方便按图索骥。该资源已有176人学习下载,且代码经过实际运行验证,稳定可靠,适合直接运行并复现结果。透过源码可系统理解药物-疾病关联预测的网络建模思路,掌握节点特征提取、链路预测和评估指标的可视化流程;在此基础上还可二次开发,拓展至其他生物医学关系挖掘场景,是一个兼具学习与扩展价值的高性价比项目。

1. 药物-疾病二分网络与复杂网络预测:毕设题目背后在做什么

很多人拿到“基于复杂网络的药物疾病关系预测方法”这个题目时,第一反应是要不要训练一个深度模型。但解压源码包,看到的是 main1.py、du.py、AUCPaint.py 这一组脚本,项目并没有想象中那么重。它的核心思路是把药物和疾病放进同一个网络,已知的药物-疾病关联作为边,然后用网络中的邻居信息、路径或相似度去推断未知关系。相比传统机器学习,复杂网络方法的优势在于中间结果可解释:为什么推荐某个药物,可以用共同邻居或网络扩散路径来说明。这套源码非常适合作为计算机、数据科学相关专业的课程大作业,也能作为毕业设计第一版基线。工程上的难点不在于算法本身,而在于 du.csv 的格式、ID 映射、交叉验证信息泄露,以及最终阈值怎么选。

2. du.csv 数据清洗与邻接矩阵构建:从边表到可算的网络

2.1 先确认 du.csv 的格式

拿到源码后,不要急着运行 main1.py,大概率会卡在数据读取上。项目里的 du.csv 是药物-疾病关系边表,我见过两种常见写法:一种只有 drug、disease 两列,每一行代表一条已确认关联;另一种带 label 列,用 0/1 表示是否存在关联。两种格式后续处理方式差别很大,需要先确认。

import pandas as pd df = pd.read_csv("du.csv", header=None) print(df.head(10)) print(df.shape)

逻辑说明:header=None 是为了防止文件没有表头时,第一行数据被当作列名。df.shape 的第二个维度很关键,如果输出是 (3000, 2),说明是两列边表;如果是 (3000, 3),说明包含 label 或其他权重列。参数说明:当文件确实有表头时要去掉 header=None,否则药物名称那一行会被误当成数据。

如果遇到两列边表,需要自己构造 label 列,后面评估阶段会用到:

df = pd.read_csv("du.csv", names=["drug", "disease"]) df["label"] = 1

这里 names 参数直接指定列名,省去表头处理。注意,两列边表隐含的正样本只有 1,负样本要从未知关系里采样。采样比例会影响最终 AUC,一般按 1:1 到 1:10 之间调节。如果项目里已经提供负样本,则不需要这一步。

下面把源码包的文件职责先列出来,方便后续对号入座:

文件在流水线中的角色
du.csv药物-疾病关系边表,构造网络的边来源
du.py加载边表、生成邻接矩阵、提供预测接口
side.py加载副作用或相似性 side 信息
zscoreSides.py对 side 特征矩阵做 z-score 标准化
main1.py主控脚本,串联整个预测和评估流程
ROC.py / AUCPaint.py计算 ROC 数据并绘制曲线

从表里能看到整个源码包的模块划分。实际运行顺序是 du.csv → du.py → side.py/zscoreSides.py → main1.py → ROC.py/AUCPaint.py。如果中途某个模块报错,优先检查上一个模块的输出形状,尤其注意 side 矩阵的行数是否和药物节点数一致。

2.2 构建药物-疾病二分网络

数据格式确认后,要把字符串 ID 转成数字索引,再构造邻接矩阵。这一步是所有复杂网络模型共享的底层操作,后面计算 z-score、扩散打分、ROC 评估都会围绕这个矩阵展开。

from scipy.sparse import lil_matrix import numpy as np drugs = sorted(df["drug"].unique()) diseases = sorted(df["disease"].unique()) drug2idx = {d: i for i, d in enumerate(drugs)} disease2idx = {d: i for i, d in enumerate(diseases)} n_drug, n_dis = len(drugs), len(diseases) adj = lil_matrix((n_drug, n_dis), dtype=np.float32) for _, row in df[df["label"] == 1].iterrows(): adj[drug2idx[row["drug"]], disease2idx[row["disease"]]] = 1.0

逻辑说明:先对药物和疾病分别排序并建立映射字典,再用 label==1 的行填充邻接矩阵。矩阵形状为 n_drug 行、n_disease 列,元素 1 表示已知关联。参数说明:dtype 用 float32 是为了减少后续相似度矩阵计算时的内存占用;如果你打算用 NetworkX 做可视化,可以把 lil_matrix 转为 coo_matrix,再用 nx.from_scipy_sparse_array 转换成图对象。

这一步最常见的坑是药物名称前后有空格,导致同一个药物被当成两个节点。稳妥的做法是在读取后对所有字符串列做 strip:

for col in ["drug", "disease"]: df[col] = df[col].astype(str).str.strip()

另外,如果 du.csv 里疾病 ID 使用疾病本体(DOID)或 MeSH 代码,和 side.py 里另一套 ID 混合时,必须提前统一 ID 体系,否则网络会出现大量本应重叠的节点被拆成两个独立节点,网络密度会被严重稀释。构建完的邻接矩阵,建议顺便统计一下每个药物的度分布。如果平均度小于 2,后面所有基于邻居传播的预测都会变得不稳定。

3. z-score 归一化与相似度打分:side.py 与 zscoreSides.py 的核心设计

3.1 side 信息在药物-疾病预测里代表什么

只靠药物和疾病的二分网络,预测覆盖度很有限。比如一个药物只出现在一条已知关联里,它在网络里能借用的邻居太少。所以源码中 side.py 和 zscoreSides.py 承担的是引入侧面信息的任务。从文件名看,side 很可能指药物副作用或疾病语义特征。实际项目中,side 矩阵的行对应网络节点,列是某种生物学特征。

常见做法是把 side 矩阵作为辅助特征矩阵,在预测打分前先做 z-score 标准化。如果不做标准化,副作用列里频次差异大的特征会主导打分结果。比如某一列几乎所有药物都是 1,另一列只有极少数药物为 1,后者对区分的贡献会被前者淹没。z-score 能把所有列统一为均值 0、标准差 1,让每个特征只贡献方向和强度,不贡献绝对大小。

3.2 z-score 标准化实现与空值处理

zscoreSides.py 的核心逻辑,用 scipy.stats.zscore 可以还原。原始数据中经常有空值,比如某个药物没有测到某个副作用,这时不能直接丢弃整列,否则样本量会大幅下降。

from scipy import stats import numpy as np def zscore_sides(side_matrix): z = stats.zscore(side_matrix, axis=0, nan_policy="omit") z = np.nan_to_num(z, nan=0.0, posinf=3.0, neginf=-3.0) return z

逻辑说明:stats.zscore 在 axis=0 时按列计算每个特征的均值和标准差,然后做 (x - mean) / std 转换。nan_policy="omit" 会跳过 NaN,但输出对应位置仍然是 NaN,所以需要 nan_to_num 处理。参数说明:posinf 和 neginf 限制在 ±3,是为了防止极少数异常值在后续矩阵乘法中把分数拉到极限。如果业务场景里希望保留异常信息,可以把这两个参数改成 np.inf。

空值填充为 0 的含义要理解:z-score 中 0 代表该特征等于平均值。填充 0 等效于用整个数据集的平均水平去猜测缺失值,这是一种保守处理。副作用矩阵里的缺失通常不是随机缺失,但课程大作业层面这个做法足够。

还需要注意一种边界情况:某列标准差为 0,zscore 的结果是 NaN 或 inf。nan_to_num 可以把 nan 替换成 0,但如果你希望该列不参与后续计算,更合理的做法是先检查方差,把方差为 0 的列直接删掉。用一行代码就能完成:

side_matrix = side_matrix.loc[:, side_matrix.std(axis=0) > 0]

3.3 基于网络相似度的打分函数

有了邻接矩阵和特征,预测关系的方法可以有很多种。du.py 中封装的算法,我理解可以近似看作资源扩散或网络传播。这里用最基础的药物相似性打分做演示:先通过疾病关联计算药物-药物相似度,再对每个疾病候选药物累加相似度。

def resource_allocation_score(adj, gamma=0.5): normalized = adj.tocsr() row_sum = np.asarray(normalized.sum(axis=1)).ravel() row_sum[row_sum == 0] = 1.0 normalized = normalized.multiply(1.0 / row_sum[:, None]).tocsr() drug_sim = normalized @ normalized.T return drug_sim, normalized

这是计算药物相似度的部分。逻辑说明:先对行做归一化,让每个药物连接到邻居的资源均匀分配。drug_sim 矩阵中的元素 (i, j) 表示药物 i 和药物 j 通过共同疾病建立的相似度。参数 gamma=0.5 在部分资源扩散算法里用来调节直接邻居和两步邻居的权重,如果源码中是一个 sparse_alpha 参数,实际含义是保留多少自身信息,gamma = 1 - alpha。

接下来对未知关系打分:

def predict_by_similarity(adj, drug_sim): n_drugs, n_diseases = adj.shape scores = np.dot(drug_sim, adj.toarray()) mask = adj.toarray().astype(bool) scores[mask] = -1 return scores

逻辑说明:scores 矩阵里每个位置 (i, d) 表示药物 i 对疾病 d 的预测分。为了避免已知关联也出现在结果里,把原始关系存在的位置置为 -1,评估时只关心未知部分。参数说明:如果数据集负样本占比很高,建议把 scores 转换成 rank 再计算 ROC,因为排序对 AUC 有意义,原始分数绝对值并不重要。

到这一步,课程大作业的基本功能已经能跑通。再往下优化,就是把 zscoreSides 得到的特征矩阵和这里的拓扑相似度做加权拼接,然后替换 drug_sim 参与打分。权重可以用网格搜索,常见范围在 0.3 到 0.7 之间。需要注意的是,side 特征与网络拓扑高度相关时,加权后 AUC 提升可能不明显,这时不要强行加特征。

4. main1.py 交叉验证与 AUCPaint 曲线评估

4.1 从入口函数理解整体流程

main1.py 是整个项目的启动文件,它把前面模块串起来。拿到源码后,建议先读 main 部分而不是逐行看代码。典型的 main1.py 调用顺序是:读取 du.csv,调用 du.py 生成邻接矩阵,调用 side.py 和 zscoreSides.py 构造特征,得到预测分数,最后用 ROC.py 和 AUCPaint.py 输出评估结果。下面用模块化示例还原 main1.py 的核心骨架:

from du import load_data, build_adjacency, predict_by_similarity from side import load_side_features from zscoreSides import zscore_sides from ROC import compute_auc from AUCPaint import draw_roc def run(): df = load_data("du.csv") adj, drug2idx, disease2idx = build_adjacency(df) sides = load_side_features("side.csv") if sides is not None: sides = zscore_sides(sides) scores = predict_by_similarity(adj) auc, fpr, tpr = compute_auc(df["label"], scores[df.index]) draw_roc(fpr, tpr, save_path="roc_curve.png") print("AUC = {:.4f}".format(auc))

逻辑说明:这个骨架最大的价值是让数据流清晰。predict_by_similarity 接收邻接矩阵,返回与 adj 形状相同的分数矩阵。compute_auc 接收真实标签和预测分数,返回 AUC 和 ROC 坐标序列。参数说明:如果你的 du.csv 还配套有 side 信息 CSV,load_side_features 必须返回与药物顺序一致的特征矩阵,顺序对不上会导致结果完全错乱。

4.2 交叉验证:先用药物分组再用随机划分

药物疾病预测里最常被评审老师追问的问题是“交叉验证时有没有信息泄露”。如果直接用 train_test_split 随机切分,同一药物在训练集和测试集同时出现,模型会记住药物的整体特征,导致 AUC 虚高。正确做法是用 GroupKFold,把药物 ID 作为分组变量。

交叉验证方式划分子集依据得分可信度适合场景
随机样本切分样本行偏高,有泄露演示用
药物分组切分drug 列的 ID接近真实泛化毕业设计
疾病分组切分disease 列的 ID接近真实泛化新疾病预测任务

代码实现:

from sklearn.model_selection import GroupKFold from sklearn.metrics import roc_auc_score gkf = GroupKFold(n_splits=5) for train_idx, test_idx in gkf.split(df, df["label"], groups=df["drug"]): train_df = df.iloc[train_idx] test_df = df.iloc[test_idx] # 在 train_df 上构建邻接矩阵和 side 特征 # 对 test_df 中未知关系打分 auc = roc_auc_score(test_label, test_score) print("fold AUC:", auc)

逻辑说明:GroupKFold 根据 groups 参数把同一药物 ID 的所有样本放入同一折,测试集药物在训练网络里完全不出现,这样评估的是新药物上的泛化能力。参数说明:n_splits 不一定要大,药物总量少于 50 个时建议用 3 折;如果还要保持正负样本比例,可以用 sklearn 的 StratifiedGroupKFold。

需要特别留意,在药物分组交叉验证中,训练集构建的网络会比全量网络稀疏很多。因为测试集药物被移除,它们关联的边也消失,这会降低打分绝对值,但不会破坏排序。如果 AUC 和全量训练相比下降超过 0.1,说明算法过度依赖全局拓扑,而不是真正的泛化信号。

4.3 ROC 曲线怎么看

AUCPaint.py 的核心工作是把 ROC 数据画出来。ROC.py 根据真实标签和预测分数,从高到低遍历阈值,计算每个阈值处的 TPR 和 FPR,然后绘制曲线。AUC 是曲线下面积。在药物疾病预测里,AUC 达到 0.85 以上通常说明网络结构包含有效信号。

如果 ROC 曲线在中线上方且起始段很陡,说明排序靠前的预测中真阳性密度很高,候选列表前几名值得重点关注。如果曲线出现明显平台,说明正负样本分布不均衡,建议换成 PR 曲线观察,因为 PR 曲线对样本不平衡更敏感。AUCPaint 如果没有 PR 选项,用 matplotlib 自己实现二十行就能完成,这里不展开。

补充一点:ROC.py 计算的 AUC 是整个测试集所有样本的。如果论文需要按疾病类型分别报告,可以把测试集按疾病分组,分别计算 AUC 再取宏平均。这种细节在答辩时很加分。

5. 把源码迁移到新数据集的三个检查点:疾病本体、孤立节点和阈值

5.1 统一疾病 ID 体系

换一套数据集时,最容易翻车的是疾病 ID 对不上。du.csv 里用 DOID,side.py 里用 MeSH,构建出的矩阵会出现大量孤立列。在 build_adjacency 后面加一个断言:

assert len(set(df["disease"]) & set(disease2idx.keys())) == df["disease"].nunique()

这个断言确保所有疾病字符串都能映射到索引。失败时优先检查疾病名称标准化,尤其是大小写、空格和 ICD 版本差异。多来源数据合并前,先用别名表做一次映射,不要直接用原始字符串拼接。

5.2 孤立节点让 AUC 失真

如果数据集中药物的平均关联数不足 2,大部分节点没有有效邻居,预测分数会集中到同一数值。此时 AUC 可能虚高,但推荐列表没有实际意义。建议统计每个药物的度,观察直方图。对低度节点可以单独分组评估,或者给邻接矩阵加一个小的自环值,让每个节点至少保留部分自身信号。

5.3 从预测分数到推荐列表的阈值选择

预测分数矩阵生成后,最终要输出一个推荐药物列表。通常用 PR 曲线选择阈值,因为药物-疾病关系中的正样本往往很少,而 ROC 曲线会掩盖这种不平衡。下面是常见实现:

from sklearn.metrics import precision_recall_curve import numpy as np prec, rec, thresholds = precision_recall_curve(y_true, y_score) f1 = 2 * prec * rec / (prec + rec + 1e-9) best_idx = np.argmax(f1[:-1]) best_threshold = thresholds[best_idx] print("best threshold:", best_threshold)

逻辑说明:precision_recall_curve 返回每个阈值下的精确率和召回率,f1 数组最后一位对应无阈值情况,所以索引切片到 f1[:-1]。np.argmax 返回最大 F1 的位置,对应阈值就是推荐列表的切分点。参数说明:如果数据极度不平衡,可以把 F1 换成 F2,给召回率更高权重。把筛选出的药物-疾病对保存成 CSV,配合 AUCPaint 画出的 ROC 曲线,就是毕设里“预测结果可视化”最完整的一版素材。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询