前回の続きです。ChEBLから抽出したLogDのデータを予測モデル構築用にクレンジングしていきます。
必要な処理に関して
今回は意味のある予測モデルを構築するために以下の処理を行います。
- pH7.4の実験条件で取得されたデータの抽出
- 塩の測定データの除去
- 構造情報の標準化
- 重複データの処理
- 外れ値の除去
pH7.4の実験条件で取得されたデータの抽出
予測対象のLogDはpHに依存したパラメータであり、ChEMBLから取得したデータも異なるpH条件下で取得されたデータが共存しています。
LogDはpHによって意味合いが変わるパラメータであるため、ChEMBLの「standard_type=”LogD”」のデータをそのままを使ってしまうと予測値が何を意味するのかわからなくなってしまいます。
そこで今回は「pH7.4条件下のLogD」に絞って予測モデルを構築したいと思います。
構造情報の標準化(塩の測定データの除去、イオンの中和)
ChEMBLには塩の状態で測定されたLogDのデータも含まれています。
塩のデータは後の特徴量を算出する段階で非塩状態との差別化が難しい場面(フィンガープリントなど)があります。そのため、今回のモデル構築では塩のデータを使わないこととします。
また、化合物の中にはイオン化状態の化合物情報を持つものもあります。
イオン化・非イオン化状態の化合物が混在していると後々厄介なのでどちらかに統一するのが望ましいです。この操作はなかなかに骨の折れる作業なので今回は厳密には行いません。関数を通してイオン化状態の表現ルールを統一するくらいの処理にとどめておきます。
データの中には構造情報が紐付いていないデータがありますが、そのような化合物を予測モデルに利用するのは難しいためこのタイミングで削除します。
重複データの処理
ChEMBLから取得したデータは1つの化合物が複数の値を持っている場合があります。
1つのデータが複数の値を持っているとモデル構築のときに不便なことが多いので、1化合物につき1つの値が紐づくように処理するのが無難です。
処理の方法は色々考えられますが、今回は相加平均でまとめようと思います。
外れ値の除去
予測モデルを構築する際に用意したデータセットの分布からあまりにもかけ離れた値を持つデータ(外れ値)がある場合、その外れ値に引っ張られてモデリングが困難になることがあります。
上記のような事態を避けるために、今回は四分位範囲を基準として外れ値を除去したいと思います。
データクレンジング
実際のデータクレンジングに移ります。Jupyter Notebookでの実行を想定しているので、各自の.ipyファイルのセルに貼り付けて使用してください。
pH7.4の実験条件で取得されたデータの抽出
以下のコードを実行します。ChEMBLから抽出したデータはstandard_valueに欠損値があるものがあったので、ついでに削除したいと思います。
利用するライブラリはpandasです。小さなテーブルデータの処理はだいたいpandasで片付きます。
import pandas as pd
# データの読み込み
# 前回取得したChEMBLのデータを使用する
df = pd.read_csv('~/shaeo-blog/pj-logd/dataset/chembl_logd.csv')
# standard_valueが欠損しているデータを削除
df = df[~df['standard_value'].isna()] # standard_value列の欠損値以外のデータを抽出
# pH7.4条件の抽出
filtered_df = df[df['assay_description'].str.contains('7.4', na=False)] # assay_description列に'7.4'が含まれる行を抽出
print('pH7.4条件抽出前のデータ数: ', len(df))
print('pH7.4条件抽出後のデータ数: ', len(filtered_df))
構造情報の標準化(塩の測定データの除去、イオンの中和)
以下のコードを実行します。
利用するライブラリはRDkitがメインです。化合物データの前処理をする場合はほぼ必須となります。
その他のライブラリに関しては、numpyとMolVSというライブラリを明示的にimportしています。numpyは配列計算をサポートしておりPythonの計算系ライブラリの裏に基本導入されています。
MolVSはRDkitのラッパーです。イオンの中和をサポートする関数を持っているので使用しましたが、今回のデータセットではあまり意味がありませんでした。
import numpy as np
from rdkit import Chem, RDLogger
from rdkit.Chem.MolStandardize import rdMolStandardize
from molvs.charge import Uncharger
# 塩が含まれるかどうかを判定する関数
def has_salt(smi):
# もとのSMILESをRDkitのcanonical表現に変換
try:
mol = Chem.MolFromSmiles(smi)
original_smiles = Chem.MolToSmiles(mol, isomericSmiles=True, canonical=True)
except:
return np.nan
# 分子内の最大フラグメントを抽出する
parent_mol = rdMolStandardize.FragmentParent(mol)
# 最大フラグメント抽出後のSMILES
parent_smiles = Chem.MolToSmiles(parent_mol)
return original_smiles != parent_smiles # 変化したかどうかを判定
# 塩を判定するカラムを追加
filtered_df['has_salt'] = filtered_df['canonical_smiles'].map(has_salt)
print('塩の化合物数: ', (filtered_df['has_salt']==True).sum())
print('RDkitでの読み込みに失敗した化合物数: ', filtered_df['has_salt'].isna().sum())
# 塩以外のデータを抽出
filtered_df2 = filtered_df[filtered_df['has_salt']==False]
# 中和で構造情報が変わるかどうかを判定する関数
def check_neutlized(smi):
# もとのSMILESをRDkitのcanonical表現に変換
mol = Chem.MolFromSmiles(smi)
original_smiles = Chem.MolToSmiles(mol, isomericSmiles=True, canonical=True)
# 構造を中和する
neutlized_mol = uncharger(mol)
neutlized_smiles = Chem.MolToSmiles(neutlized_mol) # 中和後のSMILES
return original_smiles != neutlized_smiles # 変化したかどうかを判定
# 中和を判定するカラムを追加
filtered_df2['change_neutlized_flag'] = filtered_df2['canonical_smiles'].map(check_neutlized)
print('中和された化合物数: ', (filtered_df2['change_neutlized_flag']==True).sum())
# 構造情報を中和する
filtered_df2['canonical_smiles'] = filtered_df2['canonical_smiles'].map(lambda x: Chem.MolToSmiles(uncharger(Chem.MolFromSmiles(x)), isomericSmiles=True, canonical=True))
print('標準化前のデータ数: ', len(filtered_df))
print('標準化後のデータ数: ', len(filtered_df2))
重複データの処理
以下のコードを実行します。
pandasのgroupby関数でcanonical_smiles列のユニークな値でグルーピングし、mean関数を適用することで重複データの相加平均値を取得しています。
また、このタイミングで以降必要となるデータ以外は除去しています。
# canonical_smiles列でグルーピングして、各グループのstandard_valueを平均する
filtered_df3 = filtered_df2.groupby('canonical_smiles', as_index=False)['standard_value'].mean()
filtered_df3
外れ値の除去
以下のコードを実行します。
今回はIQR(四分位範囲)を基準に外れ値を定めました。具体的には、「第一四分位数 – IQR × 1.5」〜「第三四分位数 + IQR × 1.5」の範囲に収まるデータを正常として、範囲外のデータを外れ値とみなし削除しました。
ボックスプロットやバイオリンプロットでデータを確認しながら処理方法を決めたほうが良いですが、今回は割愛します。
# IQR(四分位範囲)と最大・最小値の計算
q1 = filtered_df3['standard_value'].quantile(0.25) #Q1(第一四分位数)
q3 = filtered_df3['standard_value'].quantile(0.75) #Q3(第三四分位数)
_min = filtered_df3['standard_value'].min()
_max = filtered_df3['standard_value'].max()
iqr = q3 -q1
print('*'*100)
print('外れ値除去前の最大値: ', _min)
print('外れ値除去前の最大値: ', _max)
print('外れ値除去前の第一四分位数: ', q1)
print('外れ値除去前の第三四分位数: ', q3)
print('*'*100)
# 外れ値の除去: (Q1 - 1.5*IQR) ~ (Q3 + 1.5*IQR) の範囲から外れるものを外れ値として扱う
filtered_df4 = filtered_df3.query('(@q1 - 1.5 * @iqr <= standard_value <= @q3 + 1.5 * @iqr)')
filterd_q1 = filtered_df4['standard_value'].quantile(0.25) #Q1(第一四分位数)
filterd_q3 = filtered_df4['standard_value'].quantile(0.75) #Q3(第三四分位数)
filterd_min = filtered_df4['standard_value'].min()
filterd_max = filtered_df4['standard_value'].max()
print('外れ値除去後の最大値: ', filterd_min)
print('外れ値除去後の最大値: ', filterd_max)
print('外れ値除去後の第一四分位数: ', filterd_q1)
print('外れ値除去後の第三四分位数: ', filterd_q3)
print('*'*100)
print('外れ値除去前のデータ数: ', len(filtered_df3))
print('外れ値除去後のデータ数: ', len(filtered_df4))
以上でクレンジング終了とします。27,300程度あったデータがクレンジングを経て15,700程度まで減ったのではないでしょうか?以降はこちらのデータをきれいなデータとして扱っていきたいと思います。
一点、今回のクレンジングは厳密さよりも初学者の理解しやすさを優先しています。ベンチマークレベルにきれいなデータではないのでその点はご注意ください。
最後は以下のコードでデータを保存します。
# データの保存
filtered_df4.to_csv('~/shaeo-blog/pj-logd/dataset/chembl_logd_crensing.csv', index=False)
データの分割
次回に向けてクレンジングしたデータをモデルの訓練用とテスト用に分割したいと思います。
以下のコードでクレンジグしたデータを1対1にランダム分割し、それぞれ訓練データとテストデータとして扱いたいと思います。
# データの読み込み
df = pd.read_csv('~/shaeo-blog/pj-logd/dataset/chembl_logd_crensing.csv')
# データの分割
df_train = df.sample(frac=0.5, random_state=42)
df_test = df.drop(index=df_train.index)
# データの保存
df_train.to_csv('~/shaeo-blog/pj-logd/dataset/train.csv', index=False)
df_test.to_csv('~/shaeo-blog/pj-logd/dataset/test.csv', index=False)
最後に
ChEMBLから取得したデータを予測モデル構築用にクレンジングしました。第1回と第2回の内容はipynbファイルにまとめてGithubにアップロードしています。こちらもご参考いただけると幸いです。
次回以降は用意したデータを使って予測モデルを構築していきたいと思います。従来型の機械学習からグラフ深層学習系まで幅広く取り扱っていくつもりです。興味を持っていただけましたら引き続きご覧ください。
コメント