数据清洗与预处理:从原始数据到标准分子
数据清洗这步,说白了我把大部分时间都花在这里。你从公开数据库、实验室记录或者合作方拿来的原始数据,基本没有直接能用的。我见过太多次因为数据没洗干净,模型训完才发现全是噪声的情况——嗯,所以咱们把这章好好过一遍。
1. 去重:别让重复样本骗了你
同一个分子反复出现在训练集里,会导致模型对这条数据过拟合。你以为模型学得不错,其实它只是把那条重复样本背下来了。
我个人习惯用两个维度去重:
- SMILES 字符串去重:标准化的 SMILES 直接比较
- InChI Key 去重:同一分子的不同表示方式也能查出来
import pandas as pd
from rdkit import Chem
def deduplicate_molecules(df, smiles_col='SMILES'):
# 先把 SMILES 标准化
df['canonical_smiles'] = df[smiles_col].apply(
lambda x: Chem.MolToSmiles(Chem.MolFromSmiles(x)) if Chem.MolFromSmiles(x) else None
)
# 基于标准 SMILES 去重
df_clean = df.dropna(subset=['canonical_smiles'])
df_clean = df_clean.drop_duplicates(subset=['canonical_smiles'])
return df_clean
我曾经碰到过一个项目,数据显示有12000个分子,去重后发现实际只有7000个不同的结构。剩下那5000条全是不同批次、不同浓度、不同测量方式录入的同一个分子——你想想看,要是直接用,模型一半的精力都白费了。
2. 标准化:把数据拉到同一把尺子上
ADMET 数据里边有溶解度(logS)、渗透性(logP)、清除率(CL)等等,量级差异巨大。不标准化的话,数值大的特征直接主导模型,小的特征就废了。
我建议分两步走:
- 范围标准化(MinMaxScaler):适合已知上下界的指标
- Z-score 标准化:适合服从正态分布的指标
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# Z-score 标准化
scaler_standard = StandardScaler()
df['logD_standard'] = scaler_standard.fit_transform(df[['logD']])
# MinMax 归一化
scaler_minmax = MinMaxScaler(feature_range=(0, 1))
df['solubility_norm'] = scaler_minmax.fit_transform(df[['Solubility']])
3. 缺失值处理:别往模型里灌空值
现实中很少有数据是完整的。出现缺失值的原因很多:测试没跑完、记录遗漏、样品不够等等。怎么处理?嗯,这取决于缺失率。
我个人的处理规则大致如下:
- 缺失率 > 50%:直接删除该特征,信息太少,填了也是噪声
- 缺失率 10%-50%:尝试用中位数或 KNN 填充
- 缺失率 < 10%:用中位数或平均值填充,简单高效
为什么会这样?因为 ADMET 数据的分布往往有偏,方差大。用中位数比均值更稳健。
from sklearn.impute import SimpleImputer
# 中位数填充
imputer = SimpleImputer(strategy='median')
df_filled = pd.DataFrame(
imputer.fit_transform(df.select_dtypes(include=[np.number])),
columns=df.select_dtypes(include=[np.number]).columns
)
4. 异常值检测:揪出那些离谱的值
有些测量值明显违背物理规律。比如 logP 跑到 20 多、溶解度写成负数十几倍——这些基本可以断定是记录错误。我把异常值检测分成两个层次:
- 统计方法:IQR(四分位距)和 Z-score
- 领域知识方法:结合化学常识做规则过滤
我这里用 IQR 举个例子:
def detect_outliers_iqr(df, column, multiplier=1.5):
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - multiplier * IQR
upper_bound = Q3 + multiplier * IQR
# 返回异常值的索引
outliers = df[(df[column] < lower_bound) | (df[column] > upper_bound)].index
return outliers
不过要注意,对于 ADMET 数据,有些“极端值”其实是真实的活性化合物。比如某些 P-gp 底物的 logP 特别高——你不能一刀切全删了。我建议先用统计方法标记出来,然后人工复查一遍。
| 异常类型 | 统计方法 | 领域方法 |
|---|---|---|
| 记录错误 | Z-score > 3 | 检查原始记录 |
| 测量异常 | IQR 法 | 检查实验条件 |
| 真实极端值 | 标记不删除 | 保留并验证 |
5. 分子表示标准化:RDKit 归一化
这是最重要的步骤。同样的分子,不同人写出的 SMILES 可以千差万别。RDKit 就是帮我们把所有写法统一成一种规范形式。
我常用的标准化流程包括:
- 芳香性感知:把凯库勒式转为芳香式表示
- 电荷中和:处理带电荷的官能团
- 同位素移除:对于常规 ADMET 建模,去掉同位素标签
- 立体化学处理:保留或移除手性信息(根据需求)
from rdkit import Chem
from rdkit.Chem import MolStandardize
def normalize_smiles(smiles):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
# 移除立体化学(如果不需要)
Chem.RemoveStereochemistry(mol)
# 芳香性归一化
Chem.Kekulize(mol, clearAromaticFlags=True)
# 生成标准 SMILES
canonical_smiles = Chem.MolToSmiles(mol, canonical=True)
return canonical_smiles
再加上一步更严格的标准化:
from rdkit.Chem import MolStandardize
# 使用 RDKit 内置标准化器
normalizer = MolStandardize.Standardizer()
def standardize_mol(mol):
# 标准化共价键、电荷、质子化状态等
mol = normalizer.normalize(mol)
return mol
6. 完整的清洗流水线
上面这几步不是分开做的。我通常会把它整合成一个流水线,一条命令跑完所有预处理:
class ADMETPreprocessor:
def __init__(self, data_path):
self.data = pd.read_csv(data_path)
def run_pipeline(self):
# 1. 分子标准化
self.data['canonical_smiles'] = self.data['SMILES'].apply(normalize_smiles)
self.data = self.data.dropna(subset=['canonical_smiles'])
# 2. 去重
self.data = self.data.drop_duplicates(subset=['canonical_smiles'])
# 3. 异常值检测
for col in self.data.select_dtypes(include=[np.number]).columns:
outliers = detect_outliers_iqr(self.data, col)
# 这里我选择标记不删除,留到人工审核
self.data.loc[outliers, col] = np.nan
# 4. 缺失值填充
imputer = SimpleImputer(strategy='median')
numeric_cols = self.data.select_dtypes(include=[np.number]).columns
self.data[numeric_cols] = imputer.fit_transform(self.data[numeric_cols])
# 5. 标准化
scaler = StandardScaler()
self.data[numeric_cols] = scaler.fit_transform(self.data[numeric_cols])
return self.data
df.shape 看一眼数据量。我习惯在流水线里加几个检查点——数据量突然减少,说明某些步骤有问题,可以及时回头查。
嗯,数据清洗这部分就这些。下一节咱们开始做特征工程,把分子结构转化成模型能吃的特征向量。