在这个项目中,我们拥有一个含有4000多分子量的文件,我们需要通过这个文件,来预测分子的xlogp值。以下是项目的需求:
- 先对数据进行初步的处理,加载我们需要的数据:smiles列与xlogp列,并处理异常值。
- 对于缺失的异常值,利用rdkit计算xlogp的数值进行填充。
- 特征化处理smiles,利用分子指纹将smiles表示成字符串的形式,利于机器理解。
- 构建随机森林计算模型,并将数据集分为训练/测试两个集合,进行训练。
- 查看模型的结果
第一步先导入需要的包:
python
# 数据分析所需要的包
import pandas as pd
import numpy as np
#化学分析需要的包
import rdkit as rd
from rdkit.Chem import Descriptors
from rdkit import Chem
from rdkit.Chem import Crippen
from rdkit import rdBase #处理rdkit刷屏警告的方法
rdBase.DisableLog('rdApp.warning') #处理rdkit刷屏警告的方法
#机器学习所需要的包
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import r2_score, mean_squared_error
数据的处理
用到的是pandas这个包,这个包经常用于对数据的初步处理:
- 加载数据集
- 对异常数据进行修改/删除
- 对多个不同数据集的数据进行合并
- ...
加载数据集
ini
# 以pubchem中下载的药物分子数据集为例
# 读取数据集
df = pd.read_csv("data.csv")
可以使用以下代码来了解数据集的结构。
shell
# df.columns # 查看数据集的所有列名
# df.head() # 查看数据集的前几行
# df.info() # 查看数据集的基本信息,包括数据类型和非空值数量
df.shape
选取特定的行,此处将数据集中的列smiles和列xlogp选取了出来。
ini
xlog = df['XLogP']
smiles = df['SMILES']
data_1 = pd.DataFrame({'SMILES': smiles, 'XLogP': xlog})
data_1.head()
数据集的清洗
我们现在已经初步加载了数据集,下一步是对数据的异常有个初步的了解,针对这个数据集,我们通常会遇到以下问题(经验所得):
- 存在NA值。也就是数据缺失,我们需要保证输入到模型中的数据都是数字形式,因此我们需要对NA值进行处理,在处理之前,我们可以先检查一下数据集有多少NA值。
- 存在无效值。针对这个数据集来说,我们后续需要对smiles值进行特征化处理,如果smiles值异常,就会导致后期报错,因此我们也需要检查一下数据集中存在多少无效值
接下来我们就对症下药:先查看一下目标列中是否存在异常值
python
xlogp_na = df['XLogP'].isna().sum()
smiles_invalid = df['SMILES'].apply(lambda x: Chem.MolFromSmiles(x) is None).sum()
print(f"XLogP 中 NaN 的数量:{xlogp_na}")
print(f"无效的 SMILES 数量:{smiles_invalid}")
再处理掉异常值:
- xlogp中的异常值,用rdkit计算的logp值替代。
- smiles的异常值直接删去。
less
# 替换无效的 XLogP 值
df['XLogP'] = df.apply(
lambda x: Crippen.MolLogP(Chem.MolFromSmiles(x['SMILES']))
if pd.isna(x['XLogP']) else x['XLogP'], axis=1
)
# 删除无效的 SMILES
df = df[df['SMILES'].apply(lambda x: Chem.MolFromSmiles(x) is not None)]
做完这一步,我们可以再次检查一下数据结构
python
xlogp_na = df['XLogP'].isna().sum()
smiles_invalid = df['SMILES'].apply(lambda x: Chem.MolFromSmiles(x) is None).sum()
df.shape
print(f"XLogP 中 NaN 的数量:{xlogp_na}")
print(f"无效的 SMILES 数量:{smiles_invalid}")
比较前后数据结构的变化。 到这一步,xlogp中的na值与smiles中的异常值都被我们去掉了,数据的清洗完美结束了!下一步就是对数据进行特征化处理了。
特征化数据
由于smiles所含的信息太少,并且机器只能读懂数字,我们需要用到rdkit中提供的分子指纹办法,将smiles进行特征化处理。
scss
# 把 SMILES 转成分子对象
molecules = df["SMILES"].apply(Chem.MolFromSmiles)
# 分别计算 14 个描述符,并添加到新的列中
df["MolLogP"] = molecules.apply(Descriptors.MolLogP)
df["HeavyAtomCount"] = molecules.apply(Descriptors.HeavyAtomCount)
df["NumHAcceptors"] = molecules.apply(Descriptors.NumHAcceptors)
df["NumHeteroatoms"] = molecules.apply(Descriptors.NumHeteroatoms)
df["NumHDonors"] = molecules.apply(Descriptors.NumHDonors)
df["MolWt"] = molecules.apply(Descriptors.MolWt)
df["NumRotatableBonds"] = molecules.apply(Descriptors.NumRotatableBonds)
df["RingCount"] = molecules.apply(Descriptors.RingCount)
df["Ipc"] = molecules.apply(Descriptors.Ipc)
df["HallKierAlpha"] = molecules.apply(Descriptors.HallKierAlpha)
df["NumValenceElectrons"] = molecules.apply(Descriptors.NumValenceElectrons)
df["NumSaturatedRings"] = molecules.apply(Descriptors.NumSaturatedRings)
df["NumAliphaticRings"] = molecules.apply(Descriptors.NumAliphaticRings)
df["NumAromaticRings"] = molecules.apply(Descriptors.NumAromaticRings)
print(df.head())
做完这一步,可以查看一下现在表格的结构,是否维度发生了变化,
简单的特征化处理就结束了。
模型的训练与评估
我们现在计算完分子描述符,可以尝试使用这些分子描述符进行训练了。
- 数据集的切割
- 使用随机森林模型
- 评估模型
数据的切割
我们需要将描述符作为输入,并将xlogp值作为输出,因此我们需要定义一个包含14个描述符的X(输入),和一个包含xlogp的y(输出)。
ini
# X:描述符
X = df[['MolLogP', 'HeavyAtomCount', 'NumHAcceptors', 'NumHeteroatoms',
'NumHDonors', 'MolWt', 'NumRotatableBonds', 'RingCount', 'Ipc',
'HallKierAlpha', 'NumValenceElectrons', 'NumSaturatedRings',
'NumAliphaticRings', 'NumAromaticRings']]
# y:目标性质
y = df['XLogP']
rdkit计算出来的某些描述符可能会很大,也有可能产生新的na值。我们可以检查一下X中数据是否能够训练
scss
# 1. 检查 NaN 个数
nan_counts = X.isna().sum()
# 2. 检查 +inf 和 -inf 个数
posinf_counts = np.isposinf(X).sum()
neginf_counts = np.isneginf(X).sum()
# 3. 检查超大值(>1e5,可调整阈值)
threshold = 1e5
large_counts = (X > threshold).sum()
print("=== NaN 个数 ===")
print(nan_counts[nan_counts > 0])
print("\n=== 正无穷个数 ===")
print(posinf_counts[posinf_counts > 0])
print("\n=== 负无穷个数 ===")
print(neginf_counts[neginf_counts > 0])
print("\n=== 大于阈值的个数 ===")
print(large_counts[large_counts > 0])
以[Ipc]超大值为例,如果出现超大值,使用以下方式去掉:
ini
# 看到Ipc有超大值,进行处理
# 将 Ipc 列中的超大值替换为阈值
X["Ipc"] = X["Ipc"].clip(upper=1e5)
接下来就可以对数据进行分割了:
ini
# 分割数据集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
使用随机森林模型
分割完数据集,我们就可以开始训练模型了,我们直接选用简单的随机森林模型。
scss
model = RandomForestRegressor(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
运行完后,进行模型的评估
scss
y_pred = model.predict(X_test)
print("R²:", r2_score(y_test, y_pred))
print("MSE:", mean_squared_error(y_test, y_pred))
一个简单的预测xlogp的训练模型就完成了。