第4章 数据集收集与整理——别让数据坑了你的模型
做QSAR模型这么多年,我最大的感受是:模型好不好,七分靠数据,三分靠调参。你想想看,给我一堆脏数据、偏数据、重复数据,再牛的算法也白搭。这一章,我们就聊聊怎么把数据集这个“地基”打牢固。
4.1 数据从哪来?三个主渠道
我个人习惯先把数据来源分三类:
- 公共数据库:ChEMBL、PubChem、BindingDB 这些是首选。结构多、活性标幺清楚,缺点是部分数据标注质量参差不齐。
- 文献挖掘:从论文表格里手动摘或OCR。我踩过最大的坑——有一次从PDF里复制IC50,结果把单位“μM”错变成“nM”,整整把模型带偏了一周。
- 自家实验数据:最准,但量少。建议把它作为验证集的“锚点”。
举个筛选例子,我从ChEMBL下载的时候,通常会加这几个过滤条件:
# 示例:用Python筛选ChEMBL数据
import pandas as pd
raw = pd.read_csv("chembl_activity.csv")
# 只保留IC50,排除其他活性类型
clean = raw[raw['standard_type'] == 'IC50']
# 去掉pChEMBL为空的
clean = clean.dropna(subset=['pchembl_value'])
# 保留assay_type为纯结合实验的
clean = clean[clean['assay_type'] == 'B']
print(f"原始{len(raw)}条,过滤后{len(clean)}条")
4.2 数据清洗——最枯燥但最重要的一步
说实话,我见过很多新人拿到数据就直接跑模型。结果呢?过拟合、泛化差,还找不到原因。你想想看,数据里要是混了同一个化合物的不同对映体,模型怎么可能学对?
缺失值处理
- 活性值缺失:直接删除。别用均值填补,那是骗自己。
- 描述符计算失败:先检查分子结构是否完整,结构问题就删,计算问题重跑。
异常值检测
我习惯用Z-score或IQR方法。天然产物里偶尔会出现活性特别反常的化合物,别急着删,先查文献确认。
重复结构与冲突活性
同一个SMILES,两个不同的IC50(比如差3个log单位)怎么办?我曾经遇到过,后来原则是:保留置信度更高的来源,或者取几何平均。
# 重复处理示例
# 对相同SMILES按pIC50取中位数
dedup = clean.groupby('canonical_smiles').agg({
'pchembl_value': 'median',
'molecule_id': 'first'
}).reset_index()
4.3 分子表示与特征——把化学翻译成数学
模型看不懂分子结构,它只认得数字。特征工程就是做翻译。我常用的三种方式:
| 方法 | 优点 | 缺点 | 我推荐场景 |
|---|---|---|---|
| RDKit 2D描述符 | 计算快,物理意义明确 | 209个固定特征,可能遗漏3D信息 | 小数据集、基线模型 |
| ECFP4/MACCS指纹 | 捕获局部结构,适合分类 | 高维稀疏,需要特征选择 | 分类任务、虚拟筛选 |
| 图神经网络节点特征 | 保留拓扑与原子属性 | 需要GPU、炼丹成本高 | 大规模数据集、前沿研究 |
举个例子,计算摩根指纹的代码很简单:
from rdkit import Chem
from rdkit.Chem import AllChem
mol = Chem.MolFromSmiles("CC(=O)OC1=CC=CC=C1C(=O)O")
fp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=2, nBits=2048)
print(list(fp.GetOnBits())[:10]) # 前10个开启位
4.4 数据划分——别让模型“作弊”
大多数初学者直接random split,这在QSAR里往往是灾难。因为同系物的结构太像,随机切分会泄露信息,导致模型在测试集上虚高,实际应用翻车。
我建议用这三种策略:
- Scaffold(骨架)划分:按Bemis–Murcko骨架分组,同一骨架的化合物放在同一个集里。这最贴近真实筛选场景。
- 时间划分:如果数据有时间戳,按先发表/年份切分,模拟预测新分子。
- 聚类划分:用Butina聚类或t-SNE+KMeans,保证训练集和测试集在化学空间上分布不一致。
我曾经遇到一个项目,用随机划分R²=0.85,换成骨架划分直接掉到0.52。你猜哪个更真实?——后来外部测试,骨架划分的结果几乎就是实际泛化性能。
# 骨架划分示例(使用RDKit)
from rdkit.Chem.Scaffolds import MurckoScaffold
from rdkit import Chem
def get_scaffold(mol):
return MurckoScaffold.MurckoScaffoldSmiles(mol, includeChirality=False)
smiles_list = ["CC(=O)OC1=CC=CC=C1C(=O)O", ...]
scaffolds = [get_scaffold(Chem.MolFromSmiles(smi)) for smi in smiles_list]
# 将所有唯一骨架随机分配到train/val/test
关键指标提醒:数据划分后一定要检查两个分布——训练集和测试集的描述符均值/方差是否接近,活性值分布是否一致。差别大了说明划分有问题,模型根本学不到有效的结构-活性关系。
4.5 整章知识体系:数据流水线
下面这张图概括了我自己项目里常用的数据整理流程。你会看到,每一环都可能让模型“带病上路”,所以宁可前期多花时间,也别等模型训完再找原因。
我的一个小习惯:每完成一步数据清洗,就用pandas_profiling或ydata-profiling生成一次数据报告。几分钟的事,能帮你发现太多肉眼看不到的问题——比如某列所有值都一样,或者SMILES解析失败比例突然增高。
避坑指南:我曾经在合并多个来源的数据时,忘记标准化活性单位(有的用pIC50,有的用log(1/IC50)),结果模型学出来的系数完全不可解释。所以,每一次合并数据,务必检查单位、变量名、属性类型,并写进项目的README里。