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