【コード付】化合物の類似度評価方法について

インフォマティクス

こんにちは!ぼりたそです!

今回は化合物の類似度を評価する手法について記事にまとめてみました。

現在、この世界で発見された化合物は2000万種を越えており、数多の構造が存在しています。
そんなたくさんある化合物ですが、中には構造的に類似している化合物も存在しています。

でもどのような指標で化合物が類似していると判断していいのか迷うかと思います。形なのか、官能基なのか、組成式なのか、いくつか指標が考えられると思いますが、今回はこの類似度を定量的に評価する方法をご紹介します。

この記事は以下のポイントでまとめています。

Point
  • 化合物の類似度について
     
  • Tanimoto係数
     
  • Dice係数
     
  • コサイン類似度
     
  • ユークリッド距離
     
スポンサーリンク

化合物の類似度

まずは化合物の類似度について説明します。

化合物の類似度と言っても一体どの基準で似ていると判断すればいいかわかりませんよね。

構造式で描画した時の形なのか、共通する官能基か、構成する組成式なのか、様々な指標が存在するかと思います。

そんな中で今回はフィンガープリントを使用した定量的な類似度評価手法についてご紹介します。

フィンガープリントとは化合物の構造をバイナリービット(0 or 1)の並びで表現する手法であり、詳細は以下の記事を参照いただければと思います。

紹介する化合物の類似度評価手法については以下の通りとなります。

  • Tanimoto係数
     
  • コサイン類似度
     
  • ユークリッド距離
     
  • Dice係数

以下、順に解説していきます。

Tanimoto係数

まずはTanimoto係数についてご説明します。

Tanimoto係数は化合物の類似度評価においてはよく使用される手法になり、以下の式に従って算出されます。

$$\text{Tanimoto係数} = \frac{|A \cap B|}{|A| + |B| – |A \cap B|}$$

もう少し具体的に説明すると:

  • $ |A| $ は、化合物 A のフィンガープリントにおいて 1 になっているビットの数です。
  • $ |B| $ は、化合物 B のフィンガープリントにおいて 1 になっているビットの数です。
  • $ |A \cap B| $ は、化合物 A と B の両方で 1 になっているビットの数です。

仮に、以下のようなバイナリフィンガープリントを持つ化合物 A と B があるとします:

  • 化合物 A のフィンガープリント: 101100
  • 化合物 B のフィンガープリント: 110010

この場合、$|A| = 3$ 、 $|B| = 3$ 、$|A \cap B| = 2$となるので、

$$\text{Tanimoto係数} = \frac{|A \cap B|}{|A| + |B| – |A \cap B|} = \frac{2}{3 + 3 – 2} = \frac{2}{4} = 0.5$$

となります。

Tanimoto係数を使用する上でのメリット、デメリットは以下の通りです。

■メリット

  1. 計算が簡単で高速。
     
  2. 二値データ(二進数ベクトル)に特化しているため、化合物の存在パターンを効果的に捉えられる

■デメリット

  1. 指紋の生成方法に依存するため、異なる指紋生成方法では異なる結果が得られる。
     
  2. 部分構造のサイズや多様性に敏感でないことがある。

それでは、実際にPythonで化合物のTanimoto係数を計算してみましょう。

今回はベンゼンに対してトルエン、シクロヘキサン、n-ヘキサンのTanimoto係数を計算してみました。
実行コードは以下の通りです。

from rdkit import Chem
from rdkit.Chem import MACCSkeys, Draw
from rdkit.DataStructs import FingerprintSimilarity

# ベンゼン、トルエン、シクロヘキサン、ヘキサンのSMILES表現
smiles = {
    'Benzene': 'c1ccccc1',
    'Toluene': 'Cc1ccccc1',
    'Cyclohexane': 'C1CCCCC1',
    'Hexane': 'CCCCCC'
}

# 化合物の名前と分子を格納するリスト
molecules = [(name, Chem.MolFromSmiles(smile)) for name, smile in smiles.items()]

# ベンゼンの分子オブジェクトとMACCSフィンガープリント
benzene_fp = MACCSkeys.GenMACCSKeys(molecules[0][1])

# Tanimoto係数を計算する関数
def calculate_tanimoto(smiles, reference_fp):
    mol = Chem.MolFromSmiles(smiles)
    fp = MACCSkeys.GenMACCSKeys(mol)
    return FingerprintSimilarity(reference_fp, fp)

# 各化合物のTanimoto係数を計算し、タイトルに追加
for i, (name, mol) in enumerate(molecules):
    if name != 'Benzene':
        similarity = calculate_tanimoto(smiles[name], benzene_fp)
        molecules[i] = (f'{name}\nTanimoto: {similarity:.2f}', mol)
    else:
        molecules[i] = (f'{name}\nReference', mol)

# 分子を描画し、タイトルを追加
img = Draw.MolsToGridImage([mol for name, mol in molecules], 
                           legends=[name for name, mol in molecules], 
                           molsPerRow=4, 
                           subImgSize=(300, 300))

結果を出力すると以下のように計算されていました。トルエン→シクロヘキサン→n-ヘキサンの順でTanimoto係数が大きく、類似していることになります。n-ヘキサンは0なので、全く似ていないという結果になっていますね。

Dice係数

次はDice係数について説明していきます。

Dice係数はTanimoto係数と似ていますが、少し計算手法が異なっており、以下の計算式から導かれます。

$$\text{Dice係数} = \frac{2 |A \cap B|}{|A| + |B|}$$

もう少し具体的に説明すると:

  • $ |A| $ は、化合物 A のフィンガープリントにおいて 1 になっているビットの数です。
  • $ |B| $ は、化合物 B のフィンガープリントにおいて 1 になっているビットの数です。
  • $ |A \cap B| $ は、化合物 A と B の両方で 1 になっているビットの数です。

仮に、以下のようなバイナリフィンガープリントを持つ化合物 A と B があるとします:

  • 化合物 A のフィンガープリント: 101100
  • 化合物 B のフィンガープリント: 110010

この場合、$|A| = 3$ 、 $|B| = 3$ 、$|A \cap B| = 2$となるので、

$$\text{Dice係数} = \frac{2 |A \cap B|}{|A| + |B|} = \frac{4}{3 + 3} = \frac{4}{6} = 0.66$$

となります。

Dice係数を使用する上でのメリット、デメリットは以下の通りです。

■メリット

  1. 部分構造の重複を強調するため、小さな共通部分が大きく影響する場合に有効。
     
  2. Tanimoto係数よりも部分構造の重複が少ない場合に効果的。

■デメリット

  1. 分母が小さくなると類似度が過大評価されることがある
     
  2. フィンガープリントの生成方法に依存する。

それでは、Dice係数についてもPythonで類似度を評価していきます。

先ほどと同じようにベンゼンに対してトルエン、シクロヘキサン、n-ヘキサンについて計算していきます。

from rdkit import Chem
from rdkit.Chem import MACCSkeys, Draw
from rdkit.DataStructs import DiceSimilarity
from PIL import Image, ImageDraw

# ベンゼン、トルエン、シクロヘキサン、ヘキサンのSMILES表現
smiles = {
    'Benzene': 'c1ccccc1',
    'Toluene': 'Cc1ccccc1',
    'Cyclohexane': 'C1CCCCC1',
    'Hexane': 'CCCCCC'
}

# 化合物の名前と分子を格納するリスト
molecules = [(name, Chem.MolFromSmiles(smile)) for name, smile in smiles.items()]

# ベンゼンの分子オブジェクトとMACCSフィンガープリント
benzene_fp = MACCSkeys.GenMACCSKeys(molecules[0][1])

# Dice係数を計算する関数
def calculate_dice(smiles, reference_fp):
    mol = Chem.MolFromSmiles(smiles)
    fp = MACCSkeys.GenMACCSKeys(mol)
    return DiceSimilarity(reference_fp, fp)

# 各化合物のDice係数を計算し、タイトルに追加
for i, (name, mol) in enumerate(molecules):
    if name != 'Benzene':
        similarity = calculate_dice(smiles[name], benzene_fp)
        molecules[i] = (f'{name}\nDice: {similarity:.2f}', mol)
    else:
        molecules[i] = (f'{name}\nReference', mol)

# 分子を描画し、タイトルを追加
img = Draw.MolsToGridImage([mol for name, mol in molecules], 
                           legends=[name for name, mol in molecules], 
                           molsPerRow=4, 
                           subImgSize=(300, 300))

結果を出力してみると以下のように計算されており、Tanimoto係数と同様にトルエン→シクロヘキサン→n-ヘキサンの順に係数が大きく、類似している結果となっています。

Tanimoto係数よりも若干係数が大きく計算されており、共通構造が大きく評価されているようです。

スポンサーリンク

コサイン類似度

次にコサイン類似度を紹介していきます。

コサイン類似度は以下の式より計算することができます。

$$\text{Cosine Similarity} = \frac{A \cdot B}{\lVert A \rVert \cdot \lVert B \rVert}$$

もう少し具体的に説明すると:

  • $ \lVert A \rVert $ は、化合物 A のフィンガープリントにおけるノルム(ベクトルの大きさ)です。
  • $ \lVert B \rVert $ は、化合物 B のフィンガープリントにおけるノルム(ベクトルの大きさ)です。
  • $ A \cdot B $ は、化合物 A と B のフィンガープリントの内積です。。

仮に、以下のようなバイナリフィンガープリントを持つ化合物 A と B があるとします:

  • 化合物 A のフィンガープリント: 10110100
  • 化合物 B のフィンガープリント: 01010101

\begin{align*}
\text{内積} & : A \cdot B\\&
= (1 \times 0) + (0 \times 1) + (1 \times 0) + (1 \times 1) + (0 \times 0) + (1 \times 1) + (0 \times 0) + (0 \times 1) \\ & = 1 + 0 + 0 + 1 + 0 + 1 + 0 + 0 \\ & = 3 \\
\\
\text{ノルム} & : \lVert A \rVert\\&
= \sqrt{1^2 + 0^2 + 1^2 + 1^2 + 0^2 + 1^2 + 0^2 + 0^2} \\ & = \sqrt{4} = 2 \\ \text{ノルム} & : \lVert B \rVert\\&
= \sqrt{0^2 + 1^2 + 0^2 + 1^2 + 0^2 + 1^2 + 0^2 + 1^2} \\ & = \sqrt{4} = 2 \\
\\
\text{コサイン類似度} & : \text{Cosine Similarity} = \frac{A \cdot B}{\lVert A \rVert \cdot \lVert B \rVert} \\ &
= \frac{3}{2 \times 2} = \frac{3}{4} = 0.75
\end{align*}

コサイン類似度を使用する上でのメリット、デメリットは以下の通りです。

■メリット

  1. 高次元ベクトルに対して効果的。
     
  2. ベクトル間の角度を基にするため、ベクトルの長さ(スケール)に影響されない。

■デメリット

  1. ベクトルのスパース性(希薄性)に敏感で、スパースベクトルでの性能が低下することがある。

それではコサイン類似度についてもPythonを使用して化合物の類似度を評価していきます。

使用したコードは以下の通りです。

from rdkit import Chem
from rdkit.Chem import AllChem, Draw
from rdkit.DataStructs import FingerprintSimilarity

# ベンゼン、トルエン、シクロヘキサン、n-ヘキサンのSMILES表現
smiles = {
    'Benzene': 'c1ccccc1',
    'Toluene': 'Cc1ccccc1',
    'Cyclohexane': 'C1CCCCC1',
    'n-Hexane': 'CCCCCC'
}

# 化合物の名前と分子を格納するリスト
molecules = [(name, Chem.MolFromSmiles(smile)) for name, smile in smiles.items()]

# ベンゼンのフィンガープリント
benzene_fp = AllChem.GetMorganFingerprintAsBitVect(molecules[0][1], 2)

# コサイン類似度を計算する関数
def calculate_cosine_similarity(fp1, fp2):
    return FingerprintSimilarity(fp1, fp2)

# 各化合物のコサイン類似度を計算し、表示
for i, (name, mol) in enumerate(molecules):
    if name != 'Benzene':
        fp = AllChem.GetMorganFingerprintAsBitVect(mol, 2)
        similarity = calculate_cosine_similarity(benzene_fp, fp)
        molecules[i] = (f'{name}\nCosine Similarity: {similarity:.2f}', mol)
    else:
        molecules[i] = (f'{name}\nReference', mol)

# 分子を描画し、タイトルを追加
img = Draw.MolsToGridImage([mol for name, mol in molecules], 
                           legends=[name for name, mol in molecules], 
                           molsPerRow=4, 
                           subImgSize=(300, 300))

結果を出力すると以下の通りとなり、Tanimoto係数やDice係数と異なり、係数が大分小さくなっていることがわかります。コサイン類似度はフィンガープリントのベクトル同士の角度が小さいほど類似していると判断するので、フィンガープリントが長いほど値が小さくなる傾向にあるかもしれません。

ユークリッド距離

最後にユークリッド距離についてご紹介します。

ユークリッド距離は、多次元空間内の2つの点間の距離を測るための一般的な方法の1つです。化合物の類似度を評価するために使用されることがあります。ユークリッド距離は、2つの点 $P = (p_1, p_2, p_3, \ldots, p_n)$ と $Q = (q_1, q_2, q_3, \ldots, q_n)$ の間の距離を次の式で計算します:

$$\sqrt{(p_1 – q_1)^2 + (p_2 – q_2)^2 + \ldots + (p_n – q_n)^2}$$

化合物の場合、類似度はユークリッド距離が小さいほど類似していると見なされます。

ユークリッド距離を使用する上でのメリット、デメリットは以下の通りです。

■メリット

  1. 直感的に理解しやすい距離尺度。
     
  2. フィンガープリント以外の様々なタイプのデータに適用可能。

■デメリット

  1. 高次元データにおいて、距離が増大しやすく、類似性がうまく反映されない場合がある

ユークリッド距離についてもPythonで化合物の類似度を評価してみました。

実際のコードは以下の通りです。

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

# ベンゼン、トルエン、シクロヘキサン、n-ヘキサンのSMILES表現
smiles = {
    'Benzene': 'c1ccccc1',
    'Toluene': 'Cc1ccccc1',
    'Cyclohexane': 'C1CCCCC1',
    'n-Hexane': 'CCCCCC'
}

# 化合物の名前と分子を格納するリスト
molecules = [(name, Chem.MolFromSmiles(smile)) for name, smile in smiles.items()]

# ベンゼンのフィンガープリント
benzene_fp = AllChem.GetMorganFingerprintAsBitVect(molecules[0][1], 2)

# ユークリッド距離を計算する関数
def calculate_euclidean_distance(fp1, fp2):
    v1 = np.asarray(fp1)
    v2 = np.asarray(fp2)
    return np.linalg.norm(v1 - v2)

# 各化合物のユークリッド距離を計算し、表示
for i, (name, mol) in enumerate(molecules):
    if name != 'Benzene':
        fp = AllChem.GetMorganFingerprintAsBitVect(mol, 2)
        similarity = calculate_euclidean_distance(benzene_fp, fp)
        molecules[i] = (f'{name}\nEuclidean distance: {similarity:.2f}', mol)
    else:
        molecules[i] = (f'{name}\nReference', mol)

# 分子を描画し、タイトルを追加
img = Draw.MolsToGridImage([mol for name, mol in molecules], 
                           legends=[name for name, mol in molecules], 
                           molsPerRow=4, 
                           subImgSize=(300, 300))

結果を出力すると以下のようにシクロヘキサン→トルエン→n-ヘキサンの順にユークリッド距離が小さくなり類似度が高いことがわかります。他の評価手法と異なり、トルエンよりもシクロヘキサンの類似度の方が高く計算されていますね。

ユークリッド距離は単純なフィンガープリントベクトルの2点間距離を計算しているだけなので、違った結果になるのでしょう。

終わりに

以上がフィンガープリントを使用した化合物の類似度評価手法になります。本記事で紹介した方法以外にも評価手法は存在しており、それぞれで指標が異なるため、目的に合わせて評価指標を選択することが重要になってきます。他の評価方法についてはお時間があればまた紹介していきたいと思います。

スポンサーリンク
タイトルとURLをコピーしました