magattacaのブログ

日付以外誤報

SSSRを使って環状化合物と鎖状化合物を区別しようとする話

以前の記事で、創薬レイドバトルの共闘用シェアデータを2つにクラスタリングした後、環状ペプチドとそれ以外の分子の集まりとに手作業で分類しました。これでは数増えた時に大変ですし、見落としもありそうです。そこで今回はRDKit オンラインドキュメンテーションに記載のあったSSSRという考え方を頼りに同様の分類できるかどうか試してみたいと思います。

RDKitオンラインドキュメンテーションの該当箇所は下記です。
1. Ring Information
2. Ring Finding and SSSR

SSSR(smallest set of smallest rings; SSSR)については @mojaie さんのこちらの記事 化学構造情報とグラフ理論の説明が非常にわかりやいのでご参照ください。

とにかく、分子に含まれる環構造のうち「環の個数と環の大きさが最小になるような組み合わせ」だそうです。(ふわっとした理解)

環の情報がわかるなら、大員環とそれ以外の分子と区別することもできるのでは??? というのが今回の記事の目的です。

まずはデータの準備

from rdkit import rdBase, Chem
from rdkit.Chem import AllChem, Draw, Descriptors, PandasTools
import pandas as pd
 

df = pd.read_csv('../pd1_inhibitor_dataset-master/PD1_inhibitor_dataset.csv')
PandasTools.AddMoleculeColumnToFrame(df, 'smiles')

GetSSSR関数

RDKitのドキュメンテーションによると、SSSRに関してはGetSSSRとGetSymmSSSRの2つの関数があるそうです。

まずはGetSSSR関数を用いて見ます。こちらはSSSRの数を取得することができる関数です。

データセットの最初の分子をテスト化合物としてどのような結果が出るのか眺めて見ます。

最初の分子の構造はこのようなものです。

test_mol = df.loc[0, 'ROMol']
Draw.MolToImage(test_mol)

f:id:magattaca:20190127102613p:plain

この分子のSSSRの数は???

test_SSSR = Chem.GetSSSR(test_mol)
print(test_SSSR)
# 8

8個だそうです!

・・・わからん。

SSSRの数だけではよくわかりません。

GetSymmSSSR関数

もう一方の関数、GetSymmSSSRを使うと、認識された各環構造の情報を取得することもできるそうです。

test_SymmSSSR = Chem.GetSymmSSSR(test_mol)
print(type(test_SymmSSSR))
# <class 'rdkit.rdBase._vectNSt3__16vectorIiNS_9allocatorIiEEEE'>

print(type(test_SSSR))
# <class 'int'>

GetSSSRで得た結果は整数値(int)ですが、GetSymmSSSRで得た結果はrdkitのオブジェクトでSSSRについてより詳細な情報をもっていそうです。

GetSymmSSSRの結果から、SSSRの数を取得するには長さを求めれば良いそうです。

print(len(test_SymmSSSR))
#8

先ほどの結果と一致しました。

最大の環構造を探す

ドキュメンテーションの説明をみる限り、GetSymmSSSRで得られるものは、認識された環構造のリストとなっており、リストの要素は各環構造に含まれる原子(atom index)のリストとなっているようなオブジェクトのようです(リストのリスト)。

8個の各環構造のサイズを求めてみます。

for i in range(len(test_SymmSSSR)):
    print('{}番目の環のサイズ:{}'.format(i, len(test_SymmSSSR[i])))

"""
出力結果
0番目の環のサイズ:45
1番目の環のサイズ:5
2番目の環のサイズ:5
3番目の環のサイズ:6
4番目の環のサイズ:5
5番目の環のサイズ:6
6番目の環のサイズ:5
7番目の環のサイズ:6
"""

0番目が環員数 45となっています。ぼんやりと眺めていましたが環状ペプチドの長さはこんなにも長いものなんですね。具体的な数値を見るとあらためてびっくりしました。

この最大の環構造を構成する原子の番号(atom index)を取得して見ます。

largest_ring_member = list(test_SymmSSSR[0])
print(largest_ring_member)

# [4, 5, 7, 8, 9, 11, 12, 13, 14, 15, 16, 18, 19, 20, 22, 23, 24, 26, 27, 28, 30, 34, 35, 37, 38, 39, 41, 42, 43, 45, 46, 47, 49, 50, 51, 53, 54, 55, 57, 58, 59, 61, 62, 63, 65]

構成要素の原子がわかったので、この情報を用いて分子のどの原子が該当の環構造に含まれるのか、ハイライトして描画して見ました。

f:id:magattaca:20190127103859p:plain

期待通り、環状ペプチドの骨格となる構造を認識できていそうです。

環構造を順番に眺める

ipywidgetsを利用して各環構造を順番に眺めて見ます。

# 描画用
from rdkit.Chem.Draw import rdMolDraw2D
from IPython.display import SVG
from matplotlib.colors import ColorConverter

# インタラクティブなビューワー
from ipywidgets import interact,fixed,IntSlider
import ipywidgets

# ビューワーに与える関数を作成
def SSSR_viwer(idx):
    ring_member_atoms = list(test_SymmSSSR[idx])
    prepro_test_mol = rdMolDraw2D.PrepareMolForDrawing(test_mol)
    target_ring_atoms = list(test_SymmSSSR[idx])
    
    #デフォルトの設定は見づらいので色を変える
    color_dict = {}
    radius_dict = {}
    for i in target_ring_atoms:
        color_dict[i] = ColorConverter().to_rgb('khaki')
        radius_dict[i] = 0.7
    
    #コンテナとなるオブジェクトの作成
    view = rdMolDraw2D.MolDraw2DSVG(600,600)
    
    #コンテナの描画設定
    option = view.drawOptions()
    option.circleAtoms=False
    option.continuousHighlights=False
    
    #最大の環構造に含まれる原子をハイライトに設定
    view.DrawMolecule(prepro_test_mol, 
                      highlightAtoms=target_ring_atoms,
                      highlightAtomColors= color_dict,
                      highlightAtomRadii= radius_dict,
                      highlightBonds=[])

    #コンテナをファイナライズ
    view.FinishDrawing()

    #コンテナに書き込んだデータを取り出す
    svg = view.GetDrawingText()

    #データを描画
    return(SVG(svg.replace('svg:,','')))

#ビューワーを実行
interact(SSSR_viwer, idx=ipywidgets.IntSlider(min=0,max=len(test_SymmSSSR)-1, step=1));

f:id:magattaca:20190127104409g:plain

順番に表示させることでSSSRがどのような環構造を認識するのか、非常にわかりやすくなりました。

SSSRを利用して大員環を区別

大体SSSRでどのような情報が得られるのかわかったので、これを用いてマクロサイクル型の化合物とそれ以外を区別できるか検証して見たいと思います。

Wikipediaによると大員環化合物(macrocyclic compound)は「概ね10個以上の原子からなる環状構造を持つ有機化合物」ということなので、これを基準として用います。SSSRで得た環構造のうち、最大の大きさの環構造を構成する原子数が10以上の場合、大員環化合物とすることとしたいと思います。

まずは、各化合物のSSSRをもとめ、最大の環構造の情報を取得します。

関数を作成し、Molオブジェクトを含むカラムに適用し、得た値を新しいカラムへと格納します。

def largest_ring_member_count(mol):
    SymmSSSR = Chem.GetSymmSSSR(mol)

    size_list = []
    
    # 各環の大きさを順番にリストに追加
    for i in range(len(SymmSSSR)):
        size_list.append(len(SymmSSSR[i]))
    
    # リストの最大値をとりだす
    max_size = max(size_list)
                   
    return(max_size)

# 最大の環の情報をもつ新しいカラムを作成
df['largest_ring_size'] = df['ROMol'].map(largest_ring_member_count)

環のサイズが10以上の時はマクロサイクル型(macro)、それより小さい場合は鎖型(chain)として分類してみます。

#classifierとか一回言ってみたかった
def macro_classifier(size):
    if size >= 10:
        return('macro')
    elif size < 10:
        return('chain')

df['macro_chain'] = df['largest_ring_size'].map(macro_classifier)

どのような分類になったか数を確認してみます。

df['macro_chain'].value_counts()

"""
chain    39
macro    14
Name: macro_chain, dtype: int64
"""

クロサイクル型 14個、鎖状型39個、となりました。

前回の分類では、外れ値としたマクロサイクル型化合物1つの除いた、合計52個を、マクロサイクル型13個と鎖状型39個とに分けました。数を見る限り同様の結果が得られていそうです。

それぞれ別のDataFrameに分けたあと、構造を確認します。

クロサイクル型・・・

df_macro = df[df['macro_chain']== 'macro']
macro_list = list(df_macro['ROMol'])

def macro_viewer(idx):
    mol = macro_list[idx]
    return(display(Draw.MolToImage(mol)))
    
interact(class_1_viewer, idx=ipywidgets.IntSlider(min=0,max=len(macro_list)-1, step=1));

f:id:magattaca:20190127105231g:plain

鎖状型・・・

df_chain = df[df['macro_chain']== 'chain']
chain_list = list(df_chain['ROMol'])

def chain_viewer(idx):
    mol = chain_list[idx]
    return(display(Draw.MolToImage(mol)))
    
interact(chain_viewer, idx=ipywidgets.IntSlider(min=0,max=len(chain_list)-1, step=1));

f:id:magattaca:20190127105411g:plain

しっかりと望み通りの分類ができていそうです。

まとめ

今回はSSSRを使って、大員環化合物とそれ以外の化合物を分類できるか?ということを行ってみました。
RDKitのドキュメンテーションを読んでいる時は、どのような目的で使えば良いものかさっぱりわからず、どうして The SSSR Problem という節を設けるほど解説してあるのか疑問に思っていたのですが、実際に使って見るととても使い勝手の良い重要な考え方ということがわかりました。

かなり適当に使ってしまったので、概念の理解の誤り等ございましたらご指摘いただければ幸いです。

ライブラリの指標計算結果まとめ

前回の記事で、活性化合物群の情報をもとにライブラリから候補化合物を絞り込むため、指標の閾値(フィルター)を決めようと試みました。とりあえずの結果として、以下の値を求めました。

指標 small_molecules
min
small_molecules
max
フィルタリング目安
分子量 MW 215 835 > 300
cLogP MolLogP -2.5 9.5 -3 ~ 10
水素結合供与体数 NumHDonoes 1 7 < 10
水素結合受容体数 NumHAcceptors 3 9 < 10
回転可能結合数 NumRotatableBonds 2 15 < 15
極性表面積 TPSA 48 250 < 250

こちらのフィルタリング目安を、前処理済みのライブラリEnamine_Premium_collection(分子量300より小さいものは除去済み)に適用すべく、各化合物の指標を計算、その統計値を求めました。

結果は以下の通り・・・

f:id:magattaca:20190126163131p:plain

なんと、欲張って幅を広くとりすぎたためか、分子量以外の指標では化合物の数をうまく減らせそうにありません。

絞り込み対象のライブラリにおける指標の値の分布を検証せずに、手抜きして活性化合物群のみから閾値を決めようとしたことに原因があるかもしれません。

まずは、相手を知る必要がありそうです。

そこで各指標値の値を残りのライブラリ3つ(Enamine_Advanced_collection、Enamine_HTS_collection、UOS_HTS)に対しても求め分布を把握してみることにしました。 これらのライブラリに対しては分子量 300以下を除く処理はしていません。

まずは結果から・・・

① Enamine_Advanced_collection

f:id:magattaca:20190126163312p:plain

② Enamine_HTS_collection

f:id:magattaca:20190126163415p:plain

③ UOS_HTS

f:id:magattaca:20190126163429p:plain

分子量300以下を除く処理をしていないためか、分子量の最小値の小ささにすこしびっくりしました。

このままではよくわからないので、値のばらつきを見るため箱ひげ図を用いてばらつきを可視化しました。 以下は、Enamine_Advanced_collectionの場合です。

f:id:magattaca:20190126163614p:plain

こちらを見ると先日設定したフィルタリング目安は、最大値よりも大きく、外れ値とも言えそうな化合物も全て残してしまう、ということがわかります。

先にこっちを見ておくべきだった!!

以下に、上記を求めた際につまづいた点などを記載しておきます。

MolLogPの計算

Enamine_Premium_collectionを計算する際には問題とならなかったのですが、Enamine_Advanced_collectionを処理する際に、MolLogPの計算でエラーが生じました。指標の要約統計量を確認するとEnamine_Advanced_collectionのMolLogPのみ、count(値の総数:486321)が分子の総数(486322)よりも1小さくなっています。

エラーの内容は以下の通り。

---> 42   MolLogP_value = Chem.Descriptors.MolLogP(mol_neu)
     43   mol_neu.SetDoubleProp('MolLogP', MolLogP_value)
     44   MolLogP_list.append(MolLogP_value)

/root/miniconda/lib/python3.6/site-packages/rdkit/Chem/Crippen.py in <lambda>(*x, **y)
    168 _pyMolMR.version = "1.1.0"
    169 
--> 170 MolLogP = lambda *x, **y: rdMolDescriptors.CalcCrippenDescriptors(*x, **y)[0]
    171 MolLogP.version = rdMolDescriptors._CalcCrippenDescriptors_version
    172 MolLogP.__doc__ = """ Wildman-Crippen LogP value

ValueError: Sanitization error: Explicit valence for atom # 6 B, 5, is greater than permitted

Value Error と書かれたエラー内容から推測すると、この分子はホウ素(B : 原子番号 6)を含み、その原子に対する結合の数が、通常よりも多いため分子としてうまく認識できなくなっているようです。構造は以下の通り。

f:id:magattaca:20190126163852p:plain

上記のようにホウ素に3つのフッ素原子とピロールが結合した分子のカリウム塩となっています。4本の結合+負電荷で 「valence # 6 B, 5」となりエラーとなったようです。どうやらこのライブラリにはクロスカップリング反応の試薬(有機トリフルオロボレート塩 (Chem-Station さんより))となるような化合物も含まれているようです。

前処理のコード全体

上記エラーのためforループが途中でとまってしまい、前処理ができなくなってしまいました。そこで、MolLogPの計算については例外処理(try…except…)を追加しました。前処理のコード全体としては下記となります。

from rdkit import Chem
import gzip
from rdkit.Chem import Descriptors, MolStandardize

EAc_gz = gzip.open('Enamine_Advanced_collection.sdf.gz')
EAc_suppl = Chem.ForwardSDMolSupplier(EAc_gz) 

#Molオブジェクト、および各指標を計算し格納するためのからのリストを作成
EAc_mols = []
MW_list = []
MolLogP_list = []
NumHDonors_list = []
NumHAcceptors_list = []
NumRotatableBonds_list = []
TPSA_list = []

for x in EAc_suppl:
  if x is not None:
    mol = x
    
  #処理の前にidnumberを取り出しておく
  ID = mol.GetProp('idnumber') 

    
  #構造の標準化
  normalizer =MolStandardize.normalize.Normalizer()
  mol_norm = normalizer.normalize(mol)
  
  #一番大きいサイズのフラグメントのみ残す
  lfc = MolStandardize.fragment.LargestFragmentChooser()
  mol_desalt = lfc.choose(mol_norm)
  
  #電荷の中和
  uc = MolStandardize.charge.Uncharger()
  mol_neu = uc.uncharge(mol_desalt)
  
  #処理後のMolオブジェクトに、取り出しておいた元々のidnumberをoriginal_idとして付与
  mol_neu.SetProp('original_id', ID)
  
  #分子量を計算
  MW_value = Chem.Descriptors.MolWt(mol_neu)

  #分子量をプロパティとして持たせる。(小数点を含むfloat型)
  mol_neu.SetDoubleProp('MW', MW_value)
  
  #値のみのリストにも追加する
  MW_list.append(MW_value)
  
  #MolLogPを計算(MolLogPは対応しない元素を含むとエラーを返すので例外処理として記入)
  try: 
    MolLogP_value = Chem.Descriptors.MolLogP(mol_neu)
    mol_neu.SetDoubleProp('MolLogP', MolLogP_value)
    MolLogP_list.append(MolLogP_value)
  except:
    MolLogP_list.append(None)
   
  #NumHDonorsでも同じことをする。
  NumHDonors_value = Chem.Descriptors.NumHDonors(mol_neu)

  #非負の整数値(int型)のプロパティなので SetUnsignedIntProp でも良いが面倒なのでSetDoublePropにする。
  #たぶんメモリの無駄遣い。
  mol_neu.SetDoubleProp('NumHDonors', NumHDonors_value)
  NumHDonors_list.append(NumHDonors_value)  

  #NumHAcceptors
  NumHAcceptors_value = Chem.Descriptors.NumHAcceptors(mol_neu)
  mol_neu.SetDoubleProp('NumHAcceptors', NumHAcceptors_value)
  NumHAcceptors_list.append(NumHAcceptors_value)  

  #NumRotatableBonds
  NumRotatableBonds_value = Chem.Descriptors.NumRotatableBonds(mol_neu)
  mol_neu.SetDoubleProp('NumRotatableBonds', NumRotatableBonds_value)
  NumRotatableBonds_list.append(NumRotatableBonds_value)    

  #TPSA
  TPSA_value = Chem.Descriptors.TPSA(mol_neu)
  mol_neu.SetDoubleProp('TPSA', TPSA_value)
  TPSA_list.append(TPSA_value)   
 
  #Molオブジェクトのリストも忘れずに
  EAc_mols.append(mol_neu)

以後の処理をできるだけ簡単にするため、指標の数値のみを含むcsvファイルを作成しました。

import pandas as pd
property_names = ['MW', 'MolLogP', 'NumHDonors', 'NumHAcceptors', 'NumRotatableBonds', 'TPSA'] 
property_values = [MW_list, MolLogP_list, NumHDonors_list, NumHAcceptors_list, NumRotatableBonds_list, TPSA_list]

df = pd.DataFrame(index=[], columns=property_names)

for i, j in zip(property_names, property_values):
  df[i] = j
  
df.to_csv('Enamine_Advanced_collection_desc.csv', sep=',')

また別途、構造情報と各指標の値を紐づけた上でsdfを作成しました。

#構造と各種プロパティを残したsdfファイルを作成
writer = Chem.SDWriter('Enamine_Advanced_collection_desc.sdf')

writer.SetProps(property_names)   #01/29追記 このままだとidが抜けるので適当に追加してください。またやってしまった・・・
for mol in EAc_mols:
  writer.write(mol)
writer.close()

以上が、各ライブラリファイルに対して実行した前処理の全体の流れとなります。

処理に失敗した分子の確認

先の処理でMolLogPの計算ができなかったものについてはNoneを格納するように例外処理を指定しました。このリストをpandasで読み込むとNoneは自動的にNaNとしてDataFrameに取り込まれるそうです。

どのような化合物が処理に失敗したかを確認するため、

  • 上記で出力したcsvファイルをPandasのDataFrameとして読み込む
  • MolLogPの列で値がNaNとなっている行のindexを取得する
  • 取得したindexに相当する分子の構造を確認する という手順を実施しました。

(上述のValue Errorとなった構造の描画のために行った処理です)

# csvをDataFrameとする
df_EAc = pd.read_csv('./Enamine_Advanced_collection_desc.csv')

# MolLogPの列がNaNとなっている(query)行番号(index)を取り出し、リスト化する(list)
df_EAc_MolLogP_None = list(df_EAc.query('MolLogP == "NaN"').index)

print(df_EAc_MolLogP_None)
# [8135] と出力された

index 8135の分子一つのみが、処理に失敗したもののようです。コードは省略しますが、この分子をsdfから取り出し描画することで、先の構造(ピロールのトリフルオロボレート塩)を確認しました。

SDFの持つ情報をとりあえず確認する方法

上記、前処理ですが Enamine~~ と名前のついた3つのsdfではうまくいったものの、UOS_HTSではうまくいきませんでした。原因はidnumber というプロパティが存在しなかったことです。どうやらこちらのライブラリのみ、化合物のIDが別の名称の属性として格納されているようです。

TextエディタやMarvinViewなどで開いてSDFの持つ構造情報以外の属性を確認する、という手段もありますが、なにぶんファイルサイズが大きく開くだけでも大変です。ここは RDKitをうまく使いたい・・・ということで、とりあえずsdfから最初の1分子のみを読み込んでみることとしました。

以前こちらの記事で検証したように、ForwardSDMolSupplierで読み込んだSDFからはnext()を使うことで順番に一つずつ取り出すことができます。

UH_suppl = Chem.ForwardSDMolSupplier('./UOS_HTS.sdf') 

#組み込み関数next()を使って最初の一つの分子を取り出す
test_mol = next(UH_suppl)

#どんなPropertyが含まれているか取り出し、リスト化、出力
prop_names = list(test_mol.GetPropNames())
print(prop_names)
#['ID']と出力された

UOS_HTSについては、化合物のIDはIDという属性に格納されているようです。 これでidnumberの代わりにIDとして、他のライブラリに行ったのと同じ処理を行えば良いことがわかりました。

なんどもクドイようですが、上記処理に続けて前処理のコードを実行すると失敗します。

理由は属性の確認のため、ForwardSDMolSupplierのオブジェクトから最初の一つをとりだしてしまったため、再度sdfを読み込むところから始めないと、最初の一つを捨ててしまったことになるからです。(私は以前記事を書いておきながらもこの失敗をまた繰り返しました・・・学習能力低すぎ・・・)

統計量の計算

pandasのdescribeを用いると項目数が増えるので、総数(count)、最小値(min)、平均値(mean)、最大値(max)を別個に計算しました。

df_count = df_EAc[descriptors].count() 
df_min = df_EAc[descriptors].min()
df_mean = df_EAc[descriptors].mean()
df_max = df_EAc[descriptors].max()

df_desc_view_EAc =  pd.DataFrame([df_count, df_min, df_mean, df_max], index=['count', 'min', 'mean', 'max'])

df_desc_view_EAc.round(2)

これで冒頭の統計量を出しました。

箱ひげ図を用いたばらつきの可視化

要約統計量のみでは値の広がりがわからないので箱ひげ図を用いてばらつきを可視化しました。

import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline

一つのプロットに全ての指標を乗せてしまうと、値の範囲の違い(分子量と水素結合受容体の数では値の幅が全然違う)のため、潰れてしまったのでsubplotを指定しました。

descriptors = ['MW', 'MolLogP', 'NumHDonors', 'NumHAcceptors', 'NumRotatableBonds', 'TPSA']

df_EAc[descriptors].plot(kind = 'box', subplots=True, figsize=(20, 6))

これで冒頭の図を得ました。

以上です。

活性化合物群を分析して指標の閾値を決めようとする話

前回、約13万個の化合物を含むEnamine_Premium_collectionから、分子量300を基準とした分子の絞り込みにより約11万個まで化合物数を減らすことができました。

まだまだ多い・・・と思っていたら@iwatobipen先生に分子を絞り込む指標を考える上でとても参考になる記事 SAR News Np.19 をご紹介いただきました。

SAR newsは日本薬学会 構造活性相関部会のHPの発行しているニュースレターで、だいたい年に2本のペースで発行されており、部会に所属していなくても過去のニュースレターを含めて自由に閲覧可能となっています(バックナンバーはこちらから閲覧可能です)。

SAR Newsで用いられている指標

さて、ご紹介いただいたニュースレターの 株式会社理論創薬研究所 吉森篤史 氏による記事では「標的タンパク質指向型フラグメントライブラリの構築」を適用例として、RDKitを用いたフィルタリングの方法が実際のコード含めて解説されています。

用いられているフィルタリング基準は、フラグメント指標「Rule of Three」で、具体的には以下となっています。

指標 基準 記事中のコード 現在のコード?
分子量 ≦300 AvailDescriptors.descDict['MolWt'](mol) Descriptors.MolWt(mol)
cLogP ≦3 AvailDescriptors.descDict['MolLogP'](mol) Descriptors.MolLogP(mol)
水素結合供与体数 ≦3 AvailDescriptors.descDict['NumHDonors'](mol) Descriptors.NumHDonors(mol)
水素結合受容体数 ≦3 AvailDescriptors.descDict['NumHAcceptors'](mol) Descriptors.NumHAcceptors(mol)
回転可能結合数 ≦3 AvailDescriptors.descDict['NumRotatableBonds'](mol) Descriptors.NumRotatableBonds(mol)
極性表面積 ≦60 AvailDescriptors.descDict['TPSA'](mol) Descriptors.TPSA(mol)

記事が2010年のもののためか、現在のRDKitのモジュールを眺めてもAvailDescriptorsというものが見つけられませんでした。「化学の新しいカタチ」さんの記事「 RDKitにおける記述子の扱い方をリピンスキーの法則を通して学ぶ」 に同じ目的と思われるモジュール(Descriptors)が紹介されていましたので、こちらも上記の表に追加しました。

上記、指標の基準はフラグメント("活性が弱いながらも適切な結合を形成できる小分子")を対象としているためか、かなり小さな値となっているように見えます。今回の創薬レイドバトルの目標は、ある程度活性が強いものを取得することなので、この基準をそのまま適用するのは良くなさそうです。

また、他の指標としてリピンスキーの法則を用いる、というのも良さそうですが、前回活性化合物群の分子量のデータを眺めた印象では、分子量 500以上の大きなものも多く、すでにRule of Five を外れてしまっています。

一般的な指標をそのまま適用するだけでは、既知活性化合物群ですら外れてしまいそうなので、まずは活性化合物群の値がどのようになっているかを検証したいと思います。

PD/PD-L1阻害活性 化合物群の指標の分析

指標の計算

まずは必要なものをimportします。

from rdkit import rdBase, Chem
from rdkit.Chem import AllChem, Draw, Descriptors, PandasTools
import pandas as pd
 
print('rdkit version: ',rdBase.rdkitVersion)
print('pandas version: ', pd.__version__)
#rdkit version:  2018.09.1
#pandas version:  0.23.4

前回同様、創薬レイドバトルの共闘用シェアデータを使用します。

#csv形式のデータをPandasのDataFrameで読み込む
df = pd.read_csv('../pd1_inhibitor_dataset-master/PD1_inhibitor_dataset.csv')

#smilesカラムのSMILES情報を使ってをMolオブジェクトを追加する
PandasTools.AddMoleculeColumnToFrame(df, 'smiles')

#ROMolカラムに追加されているMolオブジェクトを使って記述子を計算し、値を追加する。
#①分子量
df['MW'] = df.ROMol.map(Descriptors.MolWt)
#②LogP
df['MolLogP'] = df.ROMol.map(Descriptors.MolLogP)
#③水素結合供与体数
df['NumHDonors'] = df.ROMol.map(Descriptors.NumHDonors)
#④水素結合受容体数
df['NumHAcceptors'] = df.ROMol.map(Descriptors.NumHAcceptors)
#⑤回転可能結合数
df['NumRotatableBonds'] = df.ROMol.map(Descriptors.NumRotatableBonds)
#⑥極性表面積
df['TPSA'] = df.ROMol.map(Descriptors.TPSA)

計算した指標の要約統計量を求めてみます。

#計算した指標のリスト
Descriptors_list = ['MW', 'MolLogP', 'NumHDonors', 'NumHAcceptors', 'NumRotatableBonds', 'TPSA']

#要約統計量を計算。小数点以下の桁数が多いので丸める
df[Descriptors_list].describe().round(2)

f:id:magattaca:20190120180844p:plain

相関係数は以下のようになっています。

#相関係数の計算
df[Descriptors_list].corr().round(2)

f:id:magattaca:20190120180932p:plain

ざっと見る限りMolLogPのみが他の指標との相関係数かつ絶対値が小さくなっています。

指標をプロット

数字を見てもイマイチ感触がつかめません。前回分子量を眺めた際に描画すると値の分布がわかりやすかったので今回も描画を行います。一つ一つみるのは面倒なのでSeabornを使ってペアプロットを作成します。

import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns

%matplotlib inline

#Seabornを用いたペアプロットの作成
sns.pairplot(df[Descriptors_list])

f:id:magattaca:20190120181042p:plain

確かに、MolLogPだけが他の指標とのプロットした際に相関が悪そうです。

見やすさのためMolLogPを外して再度プロットして見ます。

#計算した指標のリストから MolLogPを除いたもの
Descriptors_list_2 = ['MW', 'NumHDonors', 'NumHAcceptors', 'NumRotatableBonds', 'TPSA']
sns.pairplot(df[Descriptors_list_2])

f:id:magattaca:20190120181154p:plain

残りの指標間は直線上に乗っているようにみえわかりやすくなりました。

外れ値を取り除く

各プロットを見ると、最大値となっている分子が1つだけ他の分子から遠く、外れ値のようになっています。特にNumRotatableBondsで顕著にみえるので、こちらの最大値がどのような分子なのか眺めて見たいと思います。

#NumRotatableBondsが最大値となる化合物を眺める。
#まずは最大値となる化合物の行番号を取得する
print(df['NumRotatableBonds'].idxmax())
#5と出力

#Molオブジェクトの格納されているcolumnの番号を確認
print(df.columns.get_loc('ROMol'))
#6と出力

DatafFrameの(5, 6) がみたい構造のMOlオブジェクトがある場所のようです。これを描画します。描画の手順は省きます。

f:id:magattaca:20190120181634p:plain

環状ペプチドにさらに長い鎖状構造がつながっています。他の構造と比べてかなり特異な構造なので、こちらは外れ値として省いてしまっても良さそうです。

新しいDataFrameを作成し、再度ペアプロットを作成します。

#行番号5を除去する
df_new = df.drop(5, axis=0)
sns.pairplot(df_new[Descriptors_list_2])

f:id:magattaca:20190120181824p:plain

かなり分布がわかりやすくなってきました。いずれの指標においても大きく2つのグループに分子を分けることができそうです。

これは・・・クラスタリングというやつが使えるのではないか???

クラスタリングによる分類

Morganフィンガープリントとタニモト係数による類似性評価

「化学の新しいカタチ」さんのこちらの記事(RDKitとk平均法による化合物の非階層型クラスタリング )を参考に化合物を2つのクラスターに分けたいと思います。以下の操作は上記記事の処理手順をほぼそのまま使用させていただいています。

各分子のMorganフィンガープリントを計算し、分子間の類似性をタニモト係数によって比較、クラスタリングに用いるための距離行列を作成します。

from rdkit import DataStructs
from rdkit.Chem import AllChem
import numpy as np

#計算に使用するためMolオブジェクトのリストを作成
mol_list = list(df_new['ROMol'])
print(len(mol_list))
#52個の分子を含む

#Morganフィンガープリントの計算
morgan_fp = [AllChem.GetMorganFingerprintAsBitVect(x,2,2048) for x in mol_list]

#総数52の分子間で距離行列の計算
dis_matrix = [DataStructs.BulkTanimotoSimilarity(morgan_fp[i], morgan_fp[:52],returnDistance=True) for i in range(52)]

### numpy.ndarrayへの変換
dis_array = np.array(dis_matrix)
dis_array.shape

52 x 52 の距離行列が得られました。

k平均法によるクラスタリング

k平均法で2つのクラスターに分類します。

from sklearn.cluster import KMeans
import pandas as pd
kmeans = KMeans(n_clusters=2)
kmeans.fit(dis_array)
pd.value_counts(kmeans.labels_)

f:id:magattaca:20190120182815p:plain

10個の分子を含むクラス0と、42個の分子を含むクラス1に分類できました。 このクラス分類もDataFrameに追加しておきます。

class_list = []
for i in range(len(kmeans.labels_)):
    if kmeans.labels_[i] == 0:
        class_list.append('class_0')
    elif kmeans.labels_[i] == 1:
        class_list.append('class_1')
        
df_new['kmeans_class'] = class_list

ペアプロットへのクラス分類の反映

先に作成したpairplotをkmeans法で分類したクラスに応じて色分けするとどうなるか、見て見たいと思います。

もともと他の指標との相関が悪そうだったMolLogPについても、色分けしたらどうなるか気になるので、今回はpairplotに加えたいと思います。

#計算した指標のリスト(MolLogPを含む)にkmeansで形成したクラスも加える
Descriptors_class = ['MW', 'MolLogP', 'NumHDonors', 'NumHAcceptors', 'NumRotatableBonds', 'TPSA', 'kmeans_class']
sns.pairplot(df_new[Descriptors_class], hue='kmeans_class')

f:id:magattaca:20190120183026p:plain

Morganフィンガープリントをつかって2つのクラスターに分けた結果で色分けすると、指標の分布でも比較的良好に2つのクラスターに分かれているように見えます。

とくに興味深いのが、MolLogPで、ヒストグラムでは2つのクラスターが重なっていますが、ペアプロット上では割と良好に分布が分かれているように見えます。

Morganフィンガープリントがどのように計算しているのか、まだ理解していないのですが指標の分布をうまく説明するようなクラスタリングができているのは面白い結果です。

クラスターに含まれる構造のインタラクティブな描画

プロット上での描画

各点の構造がどのようなものか、分布の広がりがありつつ2つのクラスターが綺麗に分かれているMolLogPTPSAの2軸によるプロットを用いて可視化して見たいと思います。

@iwatobipen先生の記事 Plot Chemical space with d3js based libraryを参考にさせていただきました。

import mpld3  #別途インストールしておかないとimportできない
from mpld3 import plugins
mpld3.enable_notebook()

#描画を作成するためのモジュール
from rdkit.Chem.Draw import rdMolDraw2D
from IPython.display import SVG

#svgを格納するための空のリスト
svgs_list = []


for i in range(len(df_new)):
    view = rdMolDraw2D.MolDraw2DSVG(300,300)
    
    #Molオブジェクトを含むカラムの番号
    ROMol_col = df_new.columns.get_loc('ROMol')
    
    #i行目のMolオブジェクトを取得
    mol = df_new.iloc[i, ROMol_col]
    
    #描画する分子の前処理
    prepro_mol = rdMolDraw2D.PrepareMolForDrawing(mol)
    
    #分子をコンテナに追加
    view.DrawMolecule(prepro_mol)
    
    #コンテナをファイナライズ
    view.FinishDrawing()
    
    #コンテナに書き込んだデータを取り出す
    svg = view.GetDrawingText()
    
    #データをリストに追加
    svgs_list.append(svg.replace('svg:,',''))

以上で、見たい化合物の構造情報(svg形式)を含むリストが作成できました。あとは、プロットと組み合わせて、プロット上の各点がどのような構造に相当するのかを眺めます。

fig, ax = plt.subplots()
ax.set_xlabel('MolLogP')
ax.set_ylabel('TPSA')
points = ax.scatter(df_new['MolLogP'], df_new['TPSA'])

tooltip = plugins.PointHTMLTooltip(points, svgs_list)
plugins.connect(fig, tooltip)

f:id:magattaca:20190120183609g:plain

y軸(TPSA)値が 500よりも上に位置するのがclass_1、500よりも下に位置するのがclass_0となっています。

カーソルをうまくGIFに写すことができずわかりづらくなってしまいましたが、プロット上を右上から反時計回りに順にカーソルを移動させ、構造を表示させています。 class_1は全て環状ペプチド、class_0はほとんどが環状ペプチドではない低分子ですが、左下に位置する3つは環状ペプチド構造となっています。

スライドバー形式での描画

少しわかりづらかったので、もうひとつのインタラクティブな描画方法を試します。こちらはRDKitのBlogで紹介されていたものです。

まずは class_0 のみを要素とするDataFrameを作成し、さらにMolオブジェクトのリストとします。

df_class_0 = df_new[df_new['kmeans_class']== 'class_0']
class_0_list = list(df_class_0['ROMol'])

以下の方法ではipywidgetsを使って可視化します。

from ipywidgets import interact,fixed,IntSlider
import ipywidgets

#class_0 に分類された構造を順番に眺めていく
def class_0_viewer(idx):
    mol = class_0_list[idx]
    return(display(Draw.MolToImage(mol)))
    
interact(class_0_viewer, idx=ipywidgets.IntSlider(min=0,max=len(class_0_list)-1, step=1));

f:id:magattaca:20190120184006g:plain

class_0 のうち idx = [29, 30, 32] は環状ペプチド構造となっており、その他の構造とは少し違った形となっています。

ついでにclass_1もやってみます。同様の操作なのでコードは省略します。

f:id:magattaca:20190120184131g:plain

順番に構造をおくるとかなりわかりやすくなりました。

環状ペプチドと低分子とで2つに再度クラス分け

環状ペプチドと、それ以外で分けた方がより良いクラス分けとなりそうです。 そこでclass_0 に含まれる環状構造 をclass_1に移動し、新しいデータフレームを2つ作成します。

df_peptides = pd.concat([df_class_1, df_class_0.iloc[[29,30,32]]])
df_small_molecules = df_class_0.drop([40,41,43])  # idx=[29, 30, 32]番目の行のindexは[40,41,43]

print(len(df_peptides))  # 13と出力
print(len(df_small_molecules)) # 39と出力

合計52(13 + 39)個の2つのデータフレームに分けることができました。 処理の最初の段階で、外れ値とも思える大きな分子を一つ取り除いているので、もともとのデータセット(53個)よりも一つ少なくなっています。

活性化合物群に基づき指標の閾値を考察

さて、いろいろと操作してきたため、本来の目的を見失ってきました。

もともとの目的は、化合物ライブラリから化合物の数を絞り込むための指標の閾値を、活性があると報告されている化合物群のデータをもとに決めよう、というものでした。

活性化合物群を可視化した結果、2つのクラスターに分類することができ、これをもとに環状ペプチドと、それ以外の低分子との2つに分けることができました。このうち、「PD/PD-L1の相互作用を阻害する低分子を見つける」という目標として参照するのに適しているのは、後者となりそうです。

以下のテーブルでは、低分子化合物クラスター(small_molecules)と環状ペプチドクラスター(peptides)における各指標の最小値と最大値、およびLipinski の Rule of Five の値を並べています。

化合物を絞り込む際に用いる指標の閾値としては、値の範囲よりも少し広めにとって最右列のフィルタリング目安とするのが手かもしれません。

指標 small_molecules
min
small_molecules
max
peptides
min
peptides
max
Lipinski
Rule of 5
フィルタリング目安
分子量 MW 215 835 491 2096 <= 500 > 300
(実施済)
cLogP MolLogP -2.5 9.5 -6.35 3.20 <= 5 -3 ~ 10
水素結合供与体数 NumHDonoes 1 7 9 22 <= 5 < 10
水素結合受容体数 NumHAcceptors 3 9 10 26 <= 10 < 10
回転可能結合数 NumRotatableBonds 2 15 3 39 < 15
極性表面積 TPSA 48 250 237 713 < 250

念のため今回クラス分類した2つのデータフレームを出力しておきます。

properties = df_small_molecules.columns.values
Chem.PandasTools.WriteSDF(df_small_molecules, 'small_molecules.sdf', molColName='ROMol', idName='compound_id', properties=properties)
Chem.PandasTools.WriteSDF(df_peptides, 'peptides.sdf', molColName='ROMol', idName='compound_id', properties=properties)

まとめ

以上、本記事では分子量以外の化合物絞り込みの基準を探すべく、活性化合物群の情報を分析して見ました。

Morganフィンガープリントとk平均法を組み合わせた分類では、環状ペプチドのような構造もいくつかは鎖状低分子と同一のクラスにふくまれるなど、想定外のクラス分けがされて興味深い結果になりました。 また、ある程度構造が限られた化合物セットであれば順番に表示させるような機能を利用することで、化合物セットの雰囲気をつかむことができることがわかりました。

本記事を作成するにあたって、さまざまなページを参照させていただきました。また、多くのコードを拝借させていただいております。誤り等ございましたらご指摘いただければ幸いです。

追記

前回の記事で分子量で絞り込んだあとの化合物群(約11万個)の指標を計算してみたら、上記のテーブルのフィルタリング目安ではほとんど絞り込みにならなさそうです。 もっとちゃんと考えないとダメそうです・・・私の休日・・・

f:id:magattaca:20190121000021p:plain

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

参考記事

① SAR News No.19
Chemoinformatics Toolkits を用いた創薬システム開発におけるラピッドラタイピング 吉森 篤史
http://bukai.pharm.or.jp/bukai_kozo/SARNews/SARNews_19.pdf

② 化学の新しいカタチ
RDKitにおける記述子の扱い方をリピンスキーの法則を通して学ぶ

③ 化学の新しいカタチ
RDKitとk平均法による化合物の非階層型クラスタリング

④ IS LIFE WORTH LIVING? Plot Chemical space with d3js based library #RDKit #Chemoinformatics | Is life worth living?

⑤ The RDKit Blog
RDKit: Using the new fingerprint bit rendering code

⑥ ipywidgetsのインストール方法
ipywidgetsの使い方

⑦ ipywidgets の User Guide
ipywidgets — Jupyter Widgets 7.4.2 documentation

分子量で絞り込もうとした話

なんとか化合物を読み込むということはできましたが、総数は200万以上・・・

このままでは全くどうして良いかわかりません。

どうしたものかと思っていたらTwitterで処理の方法を紹介してくださっている方がいらっしゃいました。

@mayudqxさんのされていた処理の順序は、

処理 意図
分子量の範囲を設定 粗い絞り込み
部分構造で絞り込み 活性化合物を参照したLBVS(?)
粗いドッキング タンパク質との相互作用を予測するSBVS(?)
ファーマコフォアを指定したより詳細なドッキング SBVS 2回目(?)

という感じです。

このうち①と②なら、RDKitを使って行うことができそうです。とりあえずプロの真似をしよう!

今回は、まずは①分子量での絞り込みに挑戦したいと思います。

絞りこみの対象となる化合物群の構造式をいくつか眺めた感想として、確かにとても小さな分子(試薬??)もライブラリに含まれていることが意外でした。今回の目標は、生理活性のある分子を予測しよう、というものなので、@mayudqxさんがされているように、あまりにも小さく活性の期待できなさそうなものを省くというのは、最初に行う絞り込みとして良さそうです。

ところで、絞り込みの閾値分子量300とされていましたが、こういう値をどうやって設定して良いものかわかりません。

何か参考になるデータはないか・・・と思っていたら創薬レイドバトルのgithubページ に「共闘用シェアデータ」という、活性化合物群のデータを掲載してくださっていました!

様々な特許から「低分子PD-1阻害剤と思われる 低分子化合物の構造を抽出し」たもので、「各特許の記述に基づいて最も高活性とされる化合物を1例ずつ抽出し」たものだそうです。

こんな大変そうなデータセットを公開してくださるとは・・・。ありがとうございます!

活性のある化合物は閾値を決める根拠としてとても良さそうです。これを使わない手はない!ということでこちらを参照して見たいと思います。

前回に引き続き、RDKitのPandasToolsを使って解析を行って見たいと思います。「化学の新しいカタチ」さんのこちらの記事 を参考にさせていただきました。

まずは色々必要なものをimportします。

from rdkit import rdBase, Chem
from rdkit.Chem import AllChem, Draw, Descriptors, PandasTools
import pandas as pd
 
print('rdkit version: ',rdBase.rdkitVersion)
print('pandas version: ', pd.__version__)

#rdkit version:  2018.09.1
#pandas version:  0.23.4

データはcsv形式で提供されているので、まずはpandasで読み込みます。

githubからダウンロードしてくるとpd1_inhibitor_dataset-masterというフォルダの中にPD1_inhibitor_dataset.csvという名前でデータが保存されていました。

df = pd.read_csv('../pd1_inhibitor_dataset-master/PD1_inhibitor_dataset.csv')
print(df.shape)
#(53, 6)

print(df.columns)
#Index(['compound_id', 'patent_no', 'example_no', 'schembl_id', 'applicant', 'smiles'], dtype='object')

f:id:magattaca:20190119223825p:plain

全部で53個の化合物が含まれており、構造情報はSMILESとして格納されています。

SMILESのままではよくわからないので、構造式を眺めるためにMolオブジェクトに変換します。

PandasTools.AddMoleculeColumnToFrame(df, 'smiles')

f:id:magattaca:20190119223956p:plain

ROMolという列にMolオブジェクトが保存されており、小さくて見づらいですが構造式として表示されます。 最初の8個を構造式だけを取り出して並べて見ます。

PandasTools.FrameToGridImage(df[:8], column='ROMol', legendsCol='compound_id', molsPerRow=4, subImgSize=(300,300))

f:id:magattaca:20190119224146p:plain

BMS-##というcompoun_idのついた化合物はマクロサイクル(環状ペプチド)のような分子も含まれているようです。このあたりは分子量が大きそうです。

今回知りたいのは、これらの活性化合物の分子量がどのような値となっているか?、なので分子量を計算しDataframeに加えます。

df['MW'] = df.ROMol.map(Descriptors.MolWt)

f:id:magattaca:20190119224433p:plain

分子量の分布がどのようになっているか知りたいので、まずはpandasのdescribe()で要約統計量を取得します。

df['MW'].describe()

'''
count      53.000000
mean      819.726075
std       626.128326
min       215.213000
25%       419.525000
50%       574.899000
75%       753.340000
max      2699.165000
Name: MW, dtype: float64
'''

平均値 820 で最大値 2699 ! 予想以上に分子量が大きいです。分子量の大きそうな巨大なマクロサイクルで、分子量の統計量が大きな値へと引っ張られていそうです。

要約統計量だけから判断すると、大きな値に引きづられてしまいそうなので、念のためヒストグラムを作成し、分子量の値の広がりを確認したいと思います。

import matplotlib.pyplot as plt

#notebookにグラフを描くためのマジックコマンド
%matplotlib inline

#MWのMaxが約2700なので 0 から 2800 までの幅50のヒストグラムを作成
edges = range(0,2800,50)
n, bins, patches = plt.hist(df['MW'], bins=edges)

f:id:magattaca:20190119225412p:plain

分子量500前後と分子量2000を超える分子の大きく2つに山が分かれています。 今回の創薬レイドバトルの目標は低分子のPD/PD-L1の阻害剤を見つけるということですから、分子量500前後の山に相当するような分子を見つけ出すことが目的になりそうです。(そもそも分子量2000を超える分子がライブラリに含まれているか疑問・・・)

分子量が小さい山から、分子量による絞り込みの閾値を決めたいと思います。より具体的な値をみるため各ビンに含まれる数を取り出して見ます。

hist_df= pd.DataFrame(n, bins[1:]).T

f:id:magattaca:20190119230452p:plain

分子量300以下は一つしかないため、300を足切りにして絞り込むというのは非常に妥当な値のように見えます。閾値を一瞬で見定めるとは・・・プロすごい・・・

それでは、スクリーニング対象の化合物を検証したいと思います。

データサイズが大きいのでまたしてもGoogle colaboratoryを利用したいと思います。

ちょうど以前の記事で前処理の際に分子量を計算し、圧縮ファイル(Enamine_Premium_collection_pro_id_MW_sdf.gz)としてGoogleドライブに保存していたので、こちらを利用します。

もろもろの設定は省略し、SDFを読み込むところから始めます。

from rdkit import Chem
import gzip
from rdkit.Chem import Descriptors

EPc_gz = gzip.open('Enamine_Premium_collection_pro_id_MW_sdf.gz')

#ForwardSDMolSupplierで読み込む。
EPc_suppl = Chem.ForwardSDMolSupplier(EPc_gz)  

#molオブジェクトのリストを作る段階で、分子量を取得して分子量のみのリストを作成する。
#空のリストを作成
EPc_MW_list = []
EPc_mols = []

#ループを回せ!!!
for x in EPc_suppl:
  if x is not None:
    mol = x
  
  #MolオブジェクトにMWという名前で格納している分子量の値(プロパティ)を取得し、リストに追加する。
  #小数点を含むfloat型のプロパティなので GetDoubleProp を使う
  mw = mol.GetDoubleProp('MW')
  
  EPc_MW_list.append(mw)
 
  #Molオブジェクトのリストも作成
  EPc_mols.append(mol)
  
#総数を確認
len(EPc_MW_list)

# 128816と出力された

分子量のリスト(EPc_MW_list)が作成できたので、pandasで処理します。

import pandas as pd
df_MW = pd.DataFrame(EPc_MW_list, columns=['MW'])
df_MW['MW'].describe()

"""
count    128816.000000
mean        332.064740
std          32.450643
min         204.277000
25%         312.377000
50%         332.426000
75%         352.398000
max         527.150000
Name: MW, dtype: float64
"""

平均 332 で 204 から 527 の範囲の分子量となっているようです。

ヒストグラムを描いてみます。

import matplotlib.pyplot as plt
%matplotlib inline

#分子量 100 から 600 までの幅50のヒストグラムを作成
edges = range(100,600,50)
n, bins, patches = plt.hist(df_MW['MW'], bins=edges)

f:id:magattaca:20190119230828p:plain

分子量が300を超えるものについてのみのMolオブジェクトのリストを作成します。

EPc_mols_300 = []

for mol in EPc_mols:
    MW = mol.GetDoubleProp('MW')
    
    if MW > 300:
        EPc_mols_300.append(mol) 

len(EPc_mols_300)

#109602

残った分子の総数は約11万個。元々のEnamine_Premium_collectionには約13万の分子が含まれていたので、とりあえず2万個程度の分子を減らすことができたようです!

粗い絞り込みでまだまだ残った分子の数は多いですが、最初のフィルタリングとしてはそれなりにうまくいったのではないでしょうか。

sdfとして出力したいと思いますが、前回気づかぬ間にMolオブジェクトのプロパティが失われているというミスを犯したので、まずはプロパティを確認します。

#今あるプロパティのリスト
prop_names = [p for p in EPc_mols_300[0].GetPropNames()]
print(prop_names)
# ['idnumber', 'original_id', 'MW']

きちんとプロパティが保持されていそうです。安心してSDFを作成できます。

#構造と各種プロパティを残したsdfファイルを作成
writer = Chem.SDWriter('Enamine_Premium_collection_MW_filter.sdf')

writer.SetProps(prop_names)
for mol in EPc_mols_300:
  writer.write(mol)
writer.close()

#大きいので圧縮
!gzip -c Enamine_Premium_collection_MW_filter.sdf > Enamine_Premium_collection_MW_filter.sdf.gz

#Googleドライブへ出力
upload_file = drive.CreateFile()
upload_file.SetContentFile('Enamine_Premium_collection_MW_filter.sdf.gz')
upload_file.Upload()

以上、最初のステップとして分子量による絞り込みでした。

まだまだ分子数多い・・・しかもこれより多いデータセットがまだ3つある・・・

締め切りに間に合うのでしょうか???っていうか締め切りいつ???

SDFを読み込んでPythonのジェネレータを理解しようとした話

RDKitにはSDFを読み込む方法としてSDMolSupplierFowardSDMolSupplierの大きく二つの方法があるそうです。

後者を使えば圧縮されたSDFを直接読み込めるということで、大きなデータを読み込むため使用してきましたが、今一両者の違いについて理解できない点がありました。問題の箇所はこの部分です。

毎度引用させていただいて恐縮ですがいますが、「化学の新しいカタチ」さんの参考記事1 から

SDMolSupplierとForwardSDMolSupplierのもう1つの違いは,前者はMolオブジェクトのリストを作成するのに対し,後者は異なるという点が挙げられます.そのため上の例ではsuppl[0]とすると1つめのMolオブジェクトにアクセス可能ですが,fsuppl[0]ではエラーを返します.

ForwardSDMolSupplierはリストではない・・・では一体なんなのか?

回答

「ジェネレータ 」

twitterで@iwatobipen先生に教えていただきました。「メモリの消費が少ないのがメリット」だそうです。 ありがとうございました!

・・・・・・格好つけてジェネレータとか呟いてしまいましたが、私、オブジェクトよく分からない。

ということでもうちょっと調べてみました。*1

入門 Python3の記述

まずは手元にあった書籍「入門 Python3 」から関係のありそうな記述を引用します。

ジェネレータは一度しか実行できない。リスト、集合、文字列、辞書はメモリ内にあるが、ジェネレータは一度に一つずつその場で値を作り、イテレータに渡して行ってしまうので、作った値を覚えていない。そのため、ジェネレータをもう一度使ったり、バックアップしたりすることはできない。入門 Python 3 オライリージャパン Bill Lubanovic 著 斎藤 康毅 監訳 長尾 高弘 訳(p109)

ジェネレーターは、Pythonのシーケンスを作成するオブジェクトである。ジェネレータがあれば、シーケンス全体を作ってメモリに格納しなくても、(巨大になることがある)シーケンスを反復処理できる。・・・(省略)・・・ジェネレータは、反復処理のたびに、最後に呼び出されたときにどこにいたかを管理し、次の値を返す。これは、以前の呼び出しについて何も覚えておらずいつも同じ状態で1行目を実行する通常の関数とは異なる 同上 (p125)

上記と@momijiameさんのこちらの記事(参考記事2) Python: ジェネレータをイテレータから理解する を合わせると以下のようになりそうです。

コンテナオブジェクト ジェネレータオブジェクト
リスト、集合、辞書
ランダムアクセスできる できない
値を取り出した後も値を覚えている 取り出すと値を忘れる
何度でも反復して使用できる 一回きりしか使えない
処理後、最初の場所に戻る 処理ごとに順番に一つずつ進んでいく
メモリ内に全ての要素を格納したまま 忘れるのでメモリを節約できる

以上の性質をふまえると、SDMolSupplierで読み込むとコンテナ、ForwardSDMolSupplierで読み込むとジェネレータ、となっていそうです。一つずつ違いを確認したいと思いますが、その前に両者の違いをまとめます。

どちらも分子のリストを作ることができますが、それぞれ

SDMolSupplier FowardSDMolSupplier
圧縮ファイルからは読み込めない 読み込める(file-like オブジェクトも使える)
ランダムアクセスできる できない
分子を取り出しても分子を覚えている 取り出すと忘れる
くり返し同じ分子のリストを作ることができる 一回きりしか使えない
処理後、最初の場所に戻る 処理ごとに順番に一つずつ進んでいく(状態を覚えている)

実際のデータで検証してみる

検証用のデータとして73個の構造を含むSDF('test.sdf')を使用しました。 上記「参考記事1」 で例として用いられている、東京化成から購入可能なサリチル酸メチル誘導体のリストです。

from rdkit import rdBase, Chem
print(rdBase.rdkitVersion) 
#2018.09.1

①圧縮ファイルの読み込み

まずは圧縮されていないSDFで確認
#SDMolSupplierの場合
SDMSup = Chem.SDMolSupplier('./test.sdf')
SDMols = [x for x in SDMSup if x is not None]
len(SDMols)

「73」と出力されました。無事読み込めたようです。

#ForwardSDMolSupplierの場合
FSDMSup = Chem.ForwardSDMolSupplier('./test.sdf')
FSDMols = [x for x in FSDMSup if x is not None]
len(FSDMols)

こちらも「73」と出力されました。

圧縮されたSDFの場合
import gzip
test_gz = gzip.open('./test.sdf.gz')
#SDMolSupplierの場合
SDMSup = Chem.SDMolSupplier(test_gz)

#エラーが返ってきた
---------------------------------------------------------------------------
ArgumentError                             Traceback (most recent call last)
<ipython-input-30-eb17d9fedebe> in <module>()
      1 #SDMolSupplierの場合
----> 2 SDMSup = Chem.SDMolSupplier(test_gz)

ArgumentError: Python argument types in
    SDMolSupplier.__init__(SDMolSupplier, GzipFile)
did not match C++ signature:
    __init__(_object*, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > fileName, bool sanitize=True, bool removeHs=True, bool strictParsing=True)
    __init__(_object*)

SDMolSupplierでは上記のエラーがでました。

#ForwardSDMolSupplierの場合
FSDMSup = Chem.ForwardSDMolSupplier(test_gz)
FSDMols = [x for x in FSDMSup if x is not None]
len(FSDMols)

「73」と出力されました。gzip.open()関数を使って圧縮ファイルを開いて得られたファイルライクオブジェクトは、SDMolSupllierでは読み込めず、FowardSDMolSupplierでは読み込むことができました。*2

②ランダムアクセス

次に、インデックスを使って好きな要素に自由にアクセスできるか検証します。

#SDMolSupplierの場合
SDMSup = Chem.SDMolSupplier('./test.sdf')
m10 = SDMSup[10]

forループを回してリストを作成しなくてもアクセスできました。

味気ないので構造を出してみます。

from rdkit.Chem import Draw
Draw.MolToImage(m10)

f:id:magattaca:20190119164523p:plain

構造もきちんと認識されています。次にForwardSDMolSupplierを試します。

#ForwardSDMolSupplierの場合
FSDMSup = Chem.ForwardSDMolSupplier('./test.sdf')
fm10 = FSDMSup[10]

#エラーが返ってきた
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-45-bb94ea24d000> in <module>()
      1 #ForwardSDMolSupplierの場合
      2 FSDMSup = Chem.ForwardSDMolSupplier('./test.sdf')
----> 3 fm10 = FSDMSup[10]

TypeError: 'ForwardSDMolSupplier' object does not support indexing

怒られました。indexによる呼び出しはサポートされていないそうです。

ひょっとしてSDMolSupllierはそのままリストととして扱えるのでしょうか?

読み込まれた分子の総数を確認するため、長さを取得できるか検証します。

len(SDMSup)
#73

おお! forループを回さなくても長さを確認できました。

複数の分子を一度に取り出すことはできるでしょうか?

SDMSup[1:5]

#エラーが返ってきた
---------------------------------------------------------------------------
ArgumentError                             Traceback (most recent call last)
<ipython-input-56-66d817f86e32> in <module>()
----> 1 SDMSup[1:5]

ArgumentError: Python argument types in
    SDMolSupplier.__getitem__(SDMolSupplier, slice)
did not match C++ signature:
    __getitem__(RDKit::SDMolSupplier*, int)

怒られました。スライスでの取り出しには対応していないみたいです。SDMolSupplierそのものでリスト型と同じというわけではないみたいです。

結局、どういった操作ができるのでしょうか? APIに果敢にアクセスしましたがさっぱりわかりません。多分こんな感じ。

SDMolSupplier FowardSDMolSupplier
Python API SDMolSupplier FowardSDMolSupplier
C++ API SDMolSupplier FowardSDMolSupplier
next できる できる
length できる できない
[ ] operator ("idx") できる できない

以上のようです。

SDMolSupplierならindexによる任意の場所のアクセスと、長さの取得はできますが、ForwardSDMolSupplierではできません。

イテレータ

上記の表にnextという関数があります。こちらは一つずつ順番に取り出していくようです。

SDMSup = Chem.SDMolSupplier('./test.sdf')
m0 = next(SDMSup)
m1 = next(SDMSup)
m2 = next(SDMSup)

Draw.MolsToImage([m0, m1, m2])

以下の構造が取り出されたようです。

f:id:magattaca:20190119164836p:plain

indexで取り出したものと比較します。

Draw.MolsToImage([SDMSup[0], SDMSup[1], SDMSup[2]])

f:id:magattaca:20190119164836p:plain

indexでとりだしたものも、nextで取り出したものと同じ結果を返しました。

こちらの組み込み関数 next()Pythonイテレータと深く関連しているそうです。

上記の「参考記事2」の説明によるとイテレータは「要素を一つずつ取り出ことのできるオブジェクト」であり「for 文こそ最も身近なイテレータ」とのことだそうです。

イテレータ?そんな意味不明なもの一生使わんやろう。」と思っていましたが、forループを使ってSDMolSupplierをMolオブジェクトのリストに変換している時点でとてもお世話になっていたみたいです。

知らなかった・・・

いずれにせよSDMolSupplierのままでもいくつかの操作は行えるみたいですが、リストに変換しておいた方が後々便利そうです。

コンテナ? ジェネレータ?

forループはイテレータということでしたが、「イテレータオブジェクトをつくるもと」はなんなのか? というとコンテナオブジェクトとジェネレータオブジェクトということになるようです。

・・・やっと冒頭のテーブルに話が戻ってきました。

それではコンテナとジェネレータの性質の違いを順番に見ていきたいと思います。

記憶力(③)と反復利用(④)の可否

まずはSDMolSupplierで読み込んだ場合

#一回め
SDMSup = Chem.SDMolSupplier('./test.sdf')
SDMols = [x for x in SDMSup if x is not None]
len(SDMols)
#73

イテレータの作成に使用したオブジェクト、SDMSupはもう一度使用できるでしょうか?

#2回め
SDMols_2 = [x for x in SDMSup if x is not None]
len(SDMols_2)
#73

2回めも同数の結果が返ってきました。SDMolSupplierによるオブジェクトは元の状態を覚えており反復利用できました。(コンテナ

では、ForwardSDMolSupplierではどうでしょうか?

#1回め
FSDMSup = Chem.ForwardSDMolSupplier('./test.sdf')
FSDMols = [x for x in FSDMSup if x is not None]
len(FSDMols)
# 73

#2回め
FSDMols_2 = [x for x in FSDMSup if x is not None]
len(FSDMols_2)
# 0

0個! 

一度イテレータに渡して全ての分子を読み込むと、FSDMSup は空っぽになってしまいました。ForwardSDMolSupplierによるオブジェクトは、使用の度に忘れ、同じ内容は一度しかつかえないようです。(ジェネレータ

⑤処理後の状態

先の処理ではイテレータに一度にSDFの73個全ての分子を渡してしまいました。もし、一度途中で渡すのをやめたらどうなるでしょうか?

まずはSDMolSupplierの場合です。

#念のため全部再読み込み
SDMSup = Chem.SDMolSupplier('./test.sdf')
#最初の30個を読み込み
SDMol_list_1 = []
for i, j in zip(range(30), SDMSup):
    SDMol_list_1.append(j)
len(SDMol_list_1)
#30

さらに10個取り出して見ます。

#続けて10個取り出し新しいリストに格納
SDMol_list_2 = []
for i, j in zip(range(10), SDMSup):
    SDMol_list_2.append(j)
len(SDMol_list_2)
#10

それぞれ最初の分子の構造を描画します。

Draw.MolsToImage([SDMSup[0], SDMol_list_1[0], SDMol_list_2[0]], 
                 legends=['SDMSup[0]', 'SDMol_list_1[0]', 'SDMol_list_2[0]'])

f:id:magattaca:20190119165436p:plain

すべて同じ構造となりました。一度構造を取り出したあと、次に取り出すときも最初に戻って取り出しているようです。

次に同様の処理をFowardSDMolSupplierで行います。

#念のため全部再読み込み
FSDMSup = Chem.ForwardSDMolSupplier('./test.sdf')
#最初の30個を読み込み
FSDMol_list_1 = []
for i, j in zip(range(30), FSDMSup):
    FSDMol_list_1.append(j)
len(FSDMol_list_1)
#30

#続けて10個取り出し新しいリストに格納
FSDMol_list_2 = []
for i, j in zip(range(10), FSDMSup):
    FSDMol_list_2.append(j)
len(FSDMol_list_2)
#10

#それぞれ最初の分子の構造を描画
Draw.MolsToImage([FSDMol_list_1[0], FSDMol_list_2[0], SDMSup[30]], 
                 legends=['FSDMol_list_1[0]', 'FSDMol_list_2[0]', 'SDMSup[30]'])

f:id:magattaca:20190119165551p:plain

2度めに呼び出した10個の構造のリスト(FSDMol_list_2)の最初の分子は、初めの30個のリストの(FSDMol_list_1)の最初の分子と異なりました。元々のSDFの31番目の分子(SDMSup[30])と構造が一致していることから、30個呼び出したあとで、2度めは31番めからの呼び出しが始まっていることがわかります。

SDMolSupplierは処理後、最初の場所に戻り、FSDMolSupplierでは、処理ごとに順番に一つずつ進み、処理後の状態を覚えているということが確認できました。

メモリの消費

以上から、SDMolSupplierはコンテナ(シーケンス(?))の特徴をもっており、FSDMolSupplierはジェネレータの特徴を持っていることがなんとなくわかってきました。

では、後者を使うメリットの「メモリの消費」というのはどういうことなのでしょうか?

参考記事2を再度引用すると、コンテナオブジェクトではいつでも取り出せる状態にしておくため、「あらかじめ全ての要素をメモリに格納しなければならない」、一方、「そのつど生成した値を使い終わったら後は覚えておく必要はない」場合、「変数から参照されなくなったら要素はガーベジコレクションの対象となるためメモリの節約につながる」とのことだそうです。

ガベージコレクション(garbage collection、GC)というのは、参照されなくなったと判断されたメモリ領域を自動的に解放する仕組み、だそうです。C言語などでは、プログラムの作成者がメモリのマネジメントも行う必要があるそうですが、Pythonではその必要がなく、メモリの利用の宣言や、解放といったことを明示的にプログラミングする必要がない、とのことです。 (参考書籍2 (p328)

FowardSDMolSupplierを使う時の注意点

以上見てきた違いから、非常に多数の分子を取り扱う必要がある場合、

  • 圧縮ファイルを読み込める
  • メモリの消費が少ない

という点で、FowardSDMolSupplierを使う方が良さそうです。ただし、使用にあたって気をつけるべきこともあります。

FowardSDMolSupplierは読み込んだ順番に分子を忘れていきますが、これは処理に失敗した場合でも同じです。

どういうことか、わざと失敗して確かめて見ます。

FSDMSup = Chem.ForwardSDMolSupplier('./test.sdf')
#存在しないリストを指定してエラーを出す
for x in FSDMSup:
    dummy.append(x)

---------------------------------------------------------------------------
NameError: name 'dummy' is not defined

#間違いに気づき空のリストを用意する
dummy = []
for x in FSDMSup:
    dummy.append(x)
len(dummy)

#72

73個の分子を含むはずのSDFなのに、72個しかリストに格納されていません。

一番最初の分子を確認します。

Draw.MolsToImage([dummy[0], SDMSup[1]],
                 legends=['dummy[0]', 'SDMSup[1]'])

f:id:magattaca:20190119165837p:plain

dummyリストの最初の分子は、元のSDFの2番めの分子と構造が一致します。

元のSDFの最初の分子は、エラーを出した際に忘れ去られてしまい、あとから正しい処理を書き直しても、もうその処理には送られません。なので、再度もともとのSDFをFowardSDMolSupplierで読み込むところからやり直す必要が生じます。

私はこのことに気づかず、誤った処理を書いて直すたびに分子の総数が変化し、混乱するということに陥っていました。

まとめ

以上、今回RDKitからSDFを読み込むための二つの方法、SDMolSupplierとFowardSDMolSupplierの違いを検証することでPythonのコンテナ、イテレータ、ジェネレータといった用語の雰囲気をつかむことを試みました。Pythonの入門書を読んで用語は目にしたことがあっても結局なにがしたいのかよくわかっていなかったので、実際に色々触ってみるとなんとなくやりたいことがわかってきました。

私はRDKitもPythonも初心者なので、用語の使い方や理解など間違っていることも多いと思います。ご指摘いただければ幸いです。

~~~~~~~~~~~~~~~~~~~~~~~~~

本記事の作成にあたって参考にさせていただいページ、書籍

参考記事1: 「化学の新しいカタチ」 RDKitでケモインフォマティクスに入門

参考記事2: 「CUBE SUGAR CONTAINER」 Python: ジェネレータをイテレータから理解する - CUBE SUGAR CONTAINER

参考書籍1: 入門 Python 3 オライリージャパン Bill Lubanovic 著 斎藤 康毅 監訳 長尾 高弘 訳

参考書籍2: 科学技術計算のためのPython入門 技術評論社 中久喜 健司 著

*1:* ジェネレータと書くとジェネレータ関数かジェネレータオブジェクトかわからないと、ネットで言われていましたが、両者の違いを判別できるまで理解できていないので、以下ジェネレータとのみ表記します。

*2:gzipの使い方にしてはこちらの記事を参考にしました。

失われたIDを探せ!

先の2つの記事でrdkitを使った前処理を試みてきましたが、sdf出力の際の設定が間違っており、各エントリのidnumberが新しいsdfには出力できていませんでした。

SDWriterSetPropsの設定を修正することで、idnumberをsdfに含めることはできたのですが、そもそもMolオブジェクトからidnumberがなくなってしまっているものがありました。どうやら、複数のフラグメントをもつオブジェクトについて、脱塩処理(一番大きいサイズのフラグメントのみ残す)をした場合に、idnumberが失われている、というように見えました。

構造を頼りに化合物の数を絞りこんだとしても、もともとの化合物のidnumberがわからないと出所がわからず困ったことになりそうです。そこで、再度idnumberを保持した形で前処理を実施したいと思います。折角なのでついでに分子量(Moleculer weight)も計算して、情報を付け足して見たいと思います。

idnumberを保持したSDFの生成

懲りずにGoogle Colaboratoryでやりますが、処理までの操作は変わらないので割愛します。

from rdkit import Chem
import gzip

#分子の前処理のためのMolStandardize、
#および分子量計算のためのDescriptorsをimport
from rdkit.Chem import MolStandardize, Descriptors

EPc_gz = gzip.open('Enamine_Premium_collection.sdf.gz')
EPc_suppl = Chem.ForwardSDMolSupplier(EPc_gz)  
EPc_mols_pro = []

for x in EPc_suppl:
  if x is not None:
    mol = x
  
  #処理の前にidnumberを取り出しておく(前回との違い)
  ID = mol.GetProp('idnumber') 
  
  #構造の標準化
  normalizer =MolStandardize.normalize.Normalizer()
  mol_norm = normalizer.normalize(mol)
  
  #一番大きいサイズのフラグメントのみ残す
  lfc = MolStandardize.fragment.LargestFragmentChooser()
  mol_desalt = lfc.choose(mol_norm)
  
  #電荷の中和
  uc = MolStandardize.charge.Uncharger()
  mol_neu = uc.uncharge(mol_desalt)
  
  #以下は前回と異なる追加の処理
  
  #処理後のMolオブジェクトに、取り出しておいた元々のidnumberをoriginal_idとして付与
  mol_neu.SetProp('original_id', ID)
  
  #分子量を計算
  MW_value = Chem.Descriptors.MolWt(mol)
  
  #分子量をプロパティとして持たせる。
  #小数点を含むfloat型のプロパティなので SetDoubleProp を使って情報を付加する
  mol_neu.SetDoubleProp('MW', MW_value)
  
  #新しいリストに追加
  EPc_mols_pro.append(mol_neu)

かかった時間は + 1分くらいです。  CPU times: user 3min 47s, sys: 13.9 s, total: 4min 1s  Wall time: 4min 1s

念のため最初の分子でそれぞれのプロパティが格納されているか確認したいと思います。

test = EPc_mols_pro[0]
print('idnumber:', test.GetProp('idnumber'))
print('original_id:', test.GetProp('original_id'))
print('MW:', test.GetDoubleProp('MW'))

idnumber: Z1498649509
original_id: Z1498649509
MW: 210.28099999999995

と出力されました。うまくいってそうです。

Molオブジェクトのプロパティの設定で、idnumberと分子量では異なるものを使いました。 ケモインフォマティクスのオンライン入門書によると、

関数 データの型
SetProp('名前',値) str型
SetIntProp('名前',値) int型
SetUnsignedProp('名前',値) int型(非負)
SetDoubleProp('名前',値) float型
SetBoolProp('名前',値) bool型

と、なっているそうです。値を取り出すときはSetの部分をGetにすれば良さそうです。

sdfで出力し、自分のPCで確認します。

#構造とidnumber、original_id、MWをもつsdfファイルを作成
writer = Chem.SDWriter('Enamine_Premium_collection_pro_id_MW.sdf')

#プロパティとして持たせたいものをリストにしておく
prop_names = ['idnumber', 'original_id', 'MW']

writer.SetProps(prop_names)
for mol in EPc_mols_pro:
  writer.write(mol)
writer.close()

#大きいので圧縮
!gzip -c Enamine_Premium_collection_pro_id_MW.sdf > Enamine_Premium_collection_pro_id_MW.sdf.gz

#Googleドライブへ出力
upload_file = drive.CreateFile()
upload_file.SetContentFile('Enamine_Premium_collection_pro_id_MW.sdf.gz')
upload_file.Upload()

MarvinViewで開くとこのような見た目です。

f:id:magattaca:20190114233416p:plain

今度こそ上手くいったのではないでしょうか。

idnumberがなくなっていたものについて原因を検証

該当の構造の抜き出し

idnumberが失われてしまった構造は元々どのようなものだったのか、気になります。indexとして抜き出してみます。

lost_id_index = []

for i in range(len(EPc_mols_pro)):
    mol = EPc_mols_pro[i]
    
    #Molオブジェクトの持つプロパティの一覧を取得する
    props = mol.GetPropNames()
    
    #idnumberを持たない場合はそのindex(化合物idではない)をリストに加える
    if 'idnumber' not in props:
        lost_id_index.append(i)

idnumberが失われた構造の総数を確認してみます。

len(lost_id_index)

「2486」個と出ました。

これらの化合物について処理前と後で構造の変化を眺めてみたいと思います。

PandasToolsを用いた処理前後の構造の比較

比較のため再度、処理前のsdfからMolオブジェクトのリストを作成します。

EPc_gz2 = gzip.open('Enamine_Premium_collection.sdf.gz')
EPc_mols2 = [mol for mol in Chem.ForwardSDMolSupplier(EPc_gz2) if mol is not None]

構造の比較のためPandsToolsを利用してみたいと思います。

RDKitのPandasToolsについては「化学の新しいカタチ」さんのこちらの記事などを参考にしてください。

from rdkit.Chem import PandasTools
import pandas as pd

#処理前の構造から比較に使いたい構造を取り出したリストを作成する。
before_list = []

for i in lost_id_index:
    before_list.append(EPc_mols2[i])
    
#リストからSDFを作成したのち、PandasToolsのLoadSDFで読み込みDataFrameとする。
writer_b = Chem.SDWriter('before.sdf')
writer_b.SetProps(['idnumber'])
for mol in before_list:
  writer_b.write(mol)
writer_b.close()

df_before = PandasTools.LoadSDF('./before.sdf')

#処理後の構造についても同じことをする。
after_list =[]

writer_a = Chem.SDWriter('after.sdf')
writer_a.SetProps(['original_id'])
for mol in after_list:
  writer_a.write(mol)
writer_a.close()

df_after = PandasTools.LoadSDF('./after.sdf')

#2つのDataFrameを連結する。
df_compare = pd.concat([df_before, df_after], axis=1)

#全て取り込めているかを確認
df_compare.shape

(2486, 6) と返ってきました。無事2486個処理されたみたいです。

それでは早速構造の比較を見てみましょう

df_compare.head()

f:id:magattaca:20190114233756p:plain

塩酸塩となっていました。最初の考え通り、フラグメントを取り除く処理をしたものについてidnumberのプロパティが失われていた可能性が高そうです。

ついでに、最後の方の番号も見てみましょう。

df_compare.tail()

f:id:magattaca:20190114233904p:plain

後ろの方はカルボキシル基がイオン化してナトリウム塩となっているエントリーでした。 処理後の構造ではカルボキシル基は水素が付加されており、中和処理も問題なく行われているようです。

以上、idnumberが失われる場合の検証を行ってみました。予想外に処理が行われているものについて、実際に構造を比較し、処理の結果を検証することまでできました。欠損値(?)っていうのも大事なんですね。

たぶん、本当は処理の段階で、処理が必要になったものについては別のリストを作るとかそういう対応をするんだと思います。

すごい遠回りしてる感ある・・・

メモリの節約でColaboratoryの壁を越えようとする話

先の記事でGoogle colaboratoryの制限により、大きなファイルの処理ができなかったという結果になりました。

「先にMolオブジェクトをまとめて作るのではなくてforの中で一つずつ作ればメモリの節約にはなる」と教えていただいたので早速試してみたいと思います。 @yamasasaKit_先生ありがとうございました!

処理としては先の記事のままで、sdfファイルの読み込み以降の操作が異なります。

Google colaboratoryで諸々準備をした後に以下を実行しました。

#必要なモジュールの読み込み
from rdkit import Chem
import gzip
from rdkit.Chem import MolStandardize

#Googleドライブから取ってきた圧縮ファイルの読み込み
EPc_gz = gzip.open('Enamine_Premium_collection.sdf.gz')

今回もFowardSDMolSupplierで読み込みますが、Molオブジェクトをforの中でつくるため、リスト内包表記にはいれません。(←前回との違い)

#ForwardSDMolSupplierで読み込む。まだMolオブジェクトのリストにしない。
EPc_suppl = Chem.ForwardSDMolSupplier(EPc_gz)  

それでは脱塩処理してMolオブジェクトのリストを作成します。 時間も測ってみます。

%%time

#molオブジェクトのリストを作る段階で前処理を実行してメモリを節約する。
#空のリストを作成

EPc_mols_pro = []

#ループ!!!
for x in EPc_suppl:
  if x is not None:
    mol = x
  
  #構造の標準化
  normalizer =MolStandardize.normalize.Normalizer()
  mol_norm = normalizer.normalize(mol)
  
  #一番大きいサイズのフラグメントのみ残す
  lfc = MolStandardize.fragment.LargestFragmentChooser()
  mol_desalt = lfc.choose(mol_norm)
  
  #電荷の中和
  uc = MolStandardize.charge.Uncharger()
  mol_neu = uc.uncharge(mol_desalt)
  
  #新しいリストに追加
  EPc_mols_pro.append(mol_neu)

かかった時間・・・

CPU times: user 3min 37s, sys: 12.5 s, total: 3min 50s Wall time: 3min 50s

ここまでの「使用したRAM 1.85GB」でした。

念のため処理できた数を確認して見ます。

len(EPc_mols_pro)

「128816」とでました。前回も「128816」だったので、同じ数だけできているようです。

一応出力して、ローカルで確認します。

#構造とidnumberのみを残したsdfファイルを作成
writer = Chem.SDWriter('Enamine_Premium_collection_processed_2.sdf')
writer.SetProps(['idnumber'])        #(01/14)追記 SetPropsの引数をリストに変更
for mol in EPc_mols_pro:
  writer.write(mol)
writer.close()

#圧縮して出力
!gzip -c Enamine_Premium_collection_processed_2.sdf > Enamine_Premium_collection_processed_2.sdf.gz

upload_file = drive.CreateFile()
upload_file.SetContentFile('Enamine_Premium_collection_processed_2.sdf.gz')
upload_file.Upload()

出力まで行って、「使用したRAM 1.92GB」。前回は「3.91GB」だったので、約半分です。 Molオブジェクトのリストを2回作っていたのが、1回になったからでしょうか?

まさかこんなに簡単にメモリの節約ができるとは・・・・。

ちなみに自分のPCでsdfを確認すると前回と同じような構造が出ていました。

f:id:magattaca:20190114122649p:plain

) 上の図は修正前のものです。見た目は変わってしまいますが、こちらの方がわかりやすいので残しておきます。

より大きなファイルで同じ処理をした結果は以下の通り。
ファイルごとにセッションを新しくして実行しています。

Enamine_Advanced_collectionの場合

  • 処理の時間 CPU times: user 13min 27s, sys: 54.8 s, total: 14min 22s Wall time: 14min 22s
  • 化合物総数 486322
  • 全体のメモリ 6.21GB

UOS_HTS_collectionの場合

  • 処理の時間 CPU times: user 15min 17s, sys: 54.5 s, total: 16min 11s Wall time: 16min 12s
  • 化合物総数 516664
  • 全体のメモリ 7.34GB

Enamine_HTS_collectionの場合

残念ながら最後まで至らず・・・ファイルサイズがEnamine_Advanced_collectionの5倍くらいあるので仕方ないかもしれません。

私のパソコンでやると総数「1921120」でした。

創薬ちゃんさんによると、ライブラリの構成は以下のようになっているそうです。

About 2.5 million・・・・ここからどう絞り込めば・・・