RDKitのフィンガープリントをPandasのデータフレームに取り込んで使い倒そう

#はじめに
化学構造を用いた機械学習手法では、説明変数としてRDKitやmordred等で提供される記述子が良く用いられているが、これ以外にフィンガープリントも用いられる。

フィンガープリントについて詳しくは[参考文献](https://future-chem.com/rdkit-fingerprint/)を参照してほしい。

フィンガープリントには多くの種類があり、RDKitやmordredの記述子と組み合わせることで予測モデルの精度向上が期待できる。また、手法によってはフィンガープリントのビットから部分構造が特定できるものがあるため、どの部分構造が予測に寄与しているのかを知ることも可能となる。

フィンガープリントを用いた化合物の類似度の計算については記事が多くあるが、計算結果を機械学習の説明変数に利用する事例は少ない(気がする)。

本記事ではRDkitで用意されている様々な種類のフィンガープリントについて、主に機械学習の説明変数として用いるためにPandasデータフレームに取り込む方法について解説する。


# 環境
- RDKit 2020.09.3

# 前準備
本記事のソースを動かすにあたり、以下のモジュールをインポートしておく必要がある。

```python
import numpy as np
import pandas as pd
from rdkit import rdBase, Chem
from rdkit import DataStructs
from rdkit.DataStructs import ExplicitBitVect
from rdkit.Chem import AllChem, Descriptors
from rdkit.ML.Descriptors import MoleculeDescriptors
```

# フィンガープリントの説明

ここでは各フィンガープリントのビット数、ビットの種類、フィンガープリントを求めてpandasに取り込む方法について説明する。
なお、以下のコード例は全て前提として```mols```という変数にRDkitのMolオブジェクトのリストが格納されていることを前提としている。
また、Pandasのデータフレームのindexは指定していないので、化合物名のリスト等、適宜補ってほしい。

## MACCS Keys Fingerprint
- ビット数 167ビット
- ビットの種類 0, 1

### Pandasに取り込む方法

こんな感じ。

```python 
maccs_fps = []

for mol in mols:
    fp = AllChem.GetMACCSKeysFingerprint(mol) 
    maccs_fps.append(np.array(fp, int))

df = pd.DataFrame(data=maccs_fps)
```

## Morgan Fingerprint
- ビット数 不定または指定可能
- ビットの種類 0, 1

### Pandasに取り込む方法
ビット数を増やすとハッシュ化時の衝突を減らすことができる。
GetMorganFingerprint関数を用いるとビット数の衝突は生じないが、ビット数が不定となるためPandasでの取り扱いが面倒になる。
ここでは、GetMorganFingerprintAsBitVect関数を使ってビット数2048を指定した例を示す。

```python 
morgan_fps = []
for mol in mols:
    fp = AllChem.GetMorganFingerprintAsBitVect(mol, 2, 2048)
    print(np.array(fp, int))
    morgan_fps.append(np.array(fp, int))
    
df = pd.DataFrame(data=morgan_fps)
```


## RDKit Fingerprint

- ビット数 2048
- ビットの種類 0, 1

### Pandasに取り込む方法

```python
rdkit_fps = []
for mol in mols:
    fp = AllChem.RDKFingerprint(mol)
    rdkit_fps.append(np.array(list(fp.ToBitString()), int))

df = pd.DataFrame(data=rdkit_fps)  
```

## TopologicalTorsion Fingerprint

- ビット数 不定
- ビットの種類 int

### Pandasに取り込む方法
ビット数は不定となっており、フィンガープリントの計算を行うまでどんなビットがでてくるか分からないという特徴がある。化合物によって得られるビットも異なってくる。
従って計算しながらビット名をキーとして保持し、後から全化合物に対し存在しないビットの値を0で補完する処理が必要となる。
取り扱いが面倒なフィンガープリントである。
以下はPandasに取り込むコードである。

```python
from collections import defaultdict
topo_fps_tmp = []
keys = set()
for mol in mols:
    fp = AllChem.GetTopologicalTorsionFingerprint(mol)
    topo_fps_tmp.append(fp.GetNonzeroElements())
    for key in fp.GetNonzeroElements():
        keys.add(key)

topo_fps = defaultdict(list)
for fp in topo_fps_tmp:
    for key in keys:
        if key in fp:
            topo_fps[key].append(fp[key])
        else:
            topo_fps[key].append(0)

df = pd.DataFrame(data=topo_fps)
```

## AtomPair Fingerprint(ハッシュ版)
- ビット数 2048
- ビットの種類 int(0/1?)

### Pandasに取り込む方法
今回GetHashedAtomPairFingerprintAsBitVectというハッシュ版を使ったが、GetAtomPairFingerprint関数を用いれば、ビット数不定のものが得られると思われる。ただその場合、Morgan Fingerprintで述べたように取り扱いが面倒となる。

```python
from collections import defaultdict
atp_fps = []

for mol in mols:
    fp = AllChem.GetHashedAtomPairFingerprintAsBitVect(mol)
    atp_fps.append(np.array(fp, int))

#print(atp_fps)    
df = pd.DataFrame(data=atp_fps)
```

## Avalon Fingerprint

- ビット数 512
- ビットの種類 0/1

### Pandasに取り込む方法
```python
from rdkit.Avalon.pyAvalonTools import GetAvalonCountFP, GetAvalonFP

avalon_fps = []

for mol in mols:
    fp =GetAvalonFP(mol)
    avalon_fps.append(np.array(fp, int))

df = pd.DataFrame(data=avalon_fps)
```

## AvalonCount Fingerprint
- ビット数 不定
- ビットの種類 int

### Pandasに取り込む方法
存在するビットを数え上げるタイプのフィンガープリントであるためビット数が不定となり、Pandasに取り込むには TopologicalTorsion Fingerprintと同様の処理が必要となる。

```python
from rdkit.Avalon.pyAvalonTools import GetAvalonCountFP, GetAvalonFP

from collections import defaultdict
avalon_fps_tmp = []
keys = set()
for mol in mols:
    fp = GetAvalonCountFP(mol)
    avalon_fps_tmp.append(fp.GetNonzeroElements())
    for key in fp.GetNonzeroElements():
        keys.add(key)

avalon_fps = defaultdict(list)
for fp in avalon_fps_tmp:
    for key in keys:
        if key in fp:
            avalon_fps[key].append(fp[key])
        else:
            avalon_fps[key].append(0)

df = pd.DataFrame(data=avalon_fps)

```

## ErG Fingerprint
- ビット数 315
- ビットの種類 float型

細かい計算原理はわからないがビットが0/1ではなく、```3.0```等のfloat型になることが特徴である。

### Pandasに取り込む方法

```python
erg_fps = []
for mol in mols:
    fp = AllChem.GetErGFingerprint(mol)
    erg_fps.append(fp)

df = pd.DataFrame(data=erg_fps)    
```

# Pandasに取り込んだデータでTanimoto係数を計算する方法

RDKkitのDataStructs.FingerprintSimilarityでフィンガープリントを計算する場合、ExplicitBitVect型の変数にしておく必要がある。
pandasに取り込んだ化合物同士の比較を行う際に、ExplicitBitVect型に変換してRDKitのTanimoto係数を計算する関数を以下に掲載する


```python
def Tanimoto(fp1, fp2, metric=DataStructs.TanimotoSimilarity):
    
    ebv1 =ExplicitBitVect(len(fp1))
    ebv2 =ExplicitBitVect(len(fp2))
    
    for i, bit in enumerate(fp1):
        if bit > 0:
            ebv1.SetBit(i)

    for i, bit in enumerate(fp2):
        if bit > 0:
            ebv2.SetBit(i)            
        
    result = DataStructs.FingerprintSimilarity(ebv1, ebv2, metric=metric)
        
    return result
```

使い方はこんな感じだ。

```python 
Tanimoto(df.iloc[0, :].values, df.iloc[1, :].values)
```

データフレームに格納された0行目と1行目の化合物のTanimoto係数を計算している。
metric引数を変えることでTanimoto係数以外の類似度も計算できる。詳しくはRDKitのマニュアルを見てほしい。

# おわりに
一旦Pandasに変換してしまえば、記述子だろうがフィンガープリントだろうが同じ扱いができるので、バシバシこれでモデルを作ってみよう。
なお、説明変数が大量にでてくるので、モデルを作成する前の段階での特徴選択が重要になるだろう。

# 参考
https://future-chem.com/rdkit-fingerprint/