はじめに
溶解度予測や創薬インフォマティクスシリーズで Morgan フィンガープリント(2048ビット)を導入し,機械学習モデルの作成やクラスタリングなどを行いました.
前者のデータセットでは,2048ビットのうち平均 21.5 個しか ON にならない(約1%)ことが分かりました.
そこでON ビット数は分子によってどう違うのか,実際に構造を見て確かめたくなったので調べてみました.
ON ビット数の分布(Delaney データセット)
まず,溶解度予測で使った Delaney データセット(1128分子)で,
各分子の ON ビット数を計算し,分布を見てみます.
#データの読み込み
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from rdkit import Chem
from rdkit.Chem import Draw, AllChem, Descriptors
url = "https://raw.githubusercontent.com/deepchem/deepchem/master/datasets/delaney-processed.csv"
df = pd.read_csv(url)
df["mol"] = df["smiles"].apply(Chem.MolFromSmiles)
df = df[df["mol"].notnull()].reset_index(drop=True)
# molオブジェクトからfpを返す関数
def count_on_bits(mol, radius=2, n_bits=2048):
fp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=radius, nBits=n_bits)
return sum(fp)
df["n_on_bits"] = df["mol"].apply(count_on_bits)
print(df["n_on_bits"].describe())
plt.figure(figsize=(8, 5))
plt.hist(df["n_on_bits"], bins=40, edgecolor="black")
plt.xlabel("Number of ON bits")
plt.ylabel("Count")
plt.title("Distribution of ON bits per molecule (Morgan FP, 2048 bit)")
plt.show()
min=1,max=68,平均=21.5 でした.
ON ビット数と分子構造の関係
ON ビット数が異なる代表的な分子を並べてみます.
各 ON ビット数に最も近い分子を1つずつ選んで描画しました.
# 表示したい ON ビット数の目標値
targets = [1, 5, 10, 15, 20, 30, 40, 50, 60, 68]
# 各目標値に最も近い ON ビット数を持つ分子のインデックスを取得
selected = [(df["n_on_bits"] - t).abs().idxmin() for t in targets]
# (df["n_on_bits"] - t).abs() → 各分子の ON ビット数と目標値の差の絶対値
# .idxmin() → その差が最も小さい行のインデックス
# 選ばれた分子の Mol オブジェクトとレジェンドを準備
mols_sel = [df.loc[i, "mol"] for i in selected]
legends_sel = [
f"{df.loc[i, 'Compound ID']}\nON={df.loc[i, 'n_on_bits']:.0f}, MolWt={Descriptors.MolWt(df.loc[i, 'mol']):.0f}"
for i in selected
]
# グリッド画像として描画
img = Draw.MolsToGridImage(mols_sel, molsPerRow=5, subImgSize=(300, 250),
legends=legends_sel)
display(img)
ON=1 のメタン(CH4)から ON=68 の Digoxin まで,
ON ビット数が増えるにつれて構造が複雑になっていくのが分かります.
もっと大きな分子ではどうなるか
Delaney データセットは小〜中規模の分子が中心なので,
もっと大きな有名分子でも ON ビット数を調べてみました.
# 有名な大きい分子の SMILES を辞書で定義
big_molecules = {
"Aspirin": "CC(=O)OC1=CC=CC=C1C(=O)O",
"Caffeine": "CN1C=NC2=C1C(=O)N(C(=O)N2C)C",
"Cholesterol": "CC(C)CCCC(C)C1CCC2C1(CCC3C2CC=C4C3(CCC(C4)O)C)C",
"Gefitinib": "COC1=C(C=C2C(=C1)C(=NC=N2)NC3=CC(=C(C=C3)F)Cl)OCCCN4CCOCC4",
"Rifampicin": "CC1C=CC=CC(C(CC(C(C(C=CC(C(=O)NC2=C(C3=C(C4=C(C(=C3O)C)OC(C4=O)(OC=CC(C(C(C1O)C)O)C)C)C(=O)C2=O)O)C)OC)C)OC(=O)C)C)O",
"Taxol": "CC1=C2C(C(=O)C3(C(CC4C(C3C(C(C2(C)C)(CC1OC(=O)C(C5=CC=CC=C5)NC(=O)C6=CC=CC=C6)O)OC(=O)C7=CC=CC=C7)(CO4)OC(=O)C)O)C)OC(=O)C",
"Erythromycin": "CCC1C(C(C(C(=O)C(CC(C(C(C(C(C(=O)O1)C)OC2CC(C(C(O2)C)O)(C)OC)C)OC3C(C(CC(O3)C)N(C)C)O)(C)O)C)C)O)(C)O",
"Ivermectin": "CCC(CC)COC(=O)C1CC(C(C(O1)CC2CCC(CC2)OC3CC4(CCC(O4)C(C3OC)OC5CC(C(C(O5)C)OC(=O)CC6CC(C(C(O6)C)O)OC)OC)CC(=O)OC)C)C",
"Rapamycin": "CC1CCC2CC(C=CC=CC(CC(C(=O)C(C(C(=CC(C(=O)CC(OC(=O)C3CCCCN3C(=O)C(=O)C1(O2)O)C(C)CC4CCC(C(C4)OC)O)C)C)O)OC)C)C)OC",
"Cyclosporin A": "CCC1C(=O)N(CC(=O)N(C(CC(=O)N(C(CC(=O)N(C(CC(=O)N(C(CC(=O)N(C(CC(=O)N(C(CC(=O)N1C)C(C)C)CC(C)C)C)CC(C)C)C)CC(C)C)C)C(C)C)C)CC(C)C)C)C(C)C)C",
"Vancomycin": "CC1C(C(CC(O1)OC2C(C(C(OC2OC3=C4C=C5C=C3OC6=C(C=C(C=C6)C(C(C(=O)NC(C(=O)NC5C(=O)NC7C8=CC(=C(C=C8)O)C9=C(C=C(C=C9C(NC(=O)C(C(C1=CC(=C(O4)C=C1)Cl)O)NC7=O)C(=O)O)O)O)CC(=O)N)NC(=O)C(CC(C)C)NC)O)Cl)CO)O)O)(C)N)O",
}
# 各分子の ON ビット数と分子量を計算
valid_mols = {}
for name, smi in big_molecules.items():
mol = Chem.MolFromSmiles(smi)
fp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=2, nBits=2048)
n_on = sum(fp)
mw = Descriptors.MolWt(mol)
print(f" {name:20s}: ON={n_on:3d}, MolWt={mw:.0f}")
valid_mols[name] = (mol, n_on, mw) # 名前をキーに (Mol, ONビット数, 分子量) を保存
# ON ビット数の昇順でソートして描画
sorted_mols = sorted(valid_mols.items(), key=lambda x: x[1][1])
# x[1][1] → 辞書の値タプル (mol, n_on, mw) の2番目 = n_on でソート
mols_list = [v[0] for _, v in sorted_mols] # Mol オブジェクトのリスト
legends_list = [f"{name}\nON={v[1]}, MolWt={v[2]:.0f}"
for name, v in sorted_mols] # レジェンド
img2 = Draw.MolsToGridImage(mols_list, molsPerRow=3, subImgSize=(400, 300),
legends=legends_list)
display(img2)
最大は Vancomycin(抗生物質)の ON=145 でした.
分子量 1449 という巨大分子ですが,それでも 2048 ビット中 145 個(約7%)しか ON になりません.
ちなみに,非ペプチド性天然物として最大級のマイトトキシン(分子量 3426)も試そうとしましたが,PubChem から SMILES を取得できず断念しました.
ON ビット数 vs 分子量
分子の大きさと ON ビット数の関係を,散布図で確認します.
plt.figure(figsize=(10, 7))
# Delaney データ
plt.scatter(df["Molecular Weight"], df["n_on_bits"],
alpha=0.3, s=15, label="Delaney dataset")
# 有名分子
for name, (mol, n_on, mw) in valid_mols.items():
plt.scatter(mw, n_on, s=100, zorder=5, edgecolor="black")
plt.annotate(name, (mw, n_on), textcoords="offset points",
xytext=(5, 5), fontsize=8)
plt.xlabel("Molecular Weight")
plt.ylabel("Number of ON bits")
plt.title("ON bits vs Molecular Weight")
plt.legend()
plt.show()
分子量と ON ビット数には正の相関がありますが,完璧ではありません.
また,同じ分子量でも ON ビット数が異なる分子があります.
これは Morgan フィンガープリントが「分子の大きさ」ではなく「構造的多様性」を反映しているためです.
例えば環状ペプチドのように同じアミノ酸単位の繰り返しを持つ分子(例:Cyclosporin A)は,
分子量が大きくても部分構造のバリエーションが少ないため ON ビット数が増えにくいようです.
ちなみに,Vancomycinは25年以上前に全合成が達成されています.
合成ルートも労力も想像もできない...
なぜ ON ビット数に上限があるか:ビット衝突
*この項目はAIからの情報をかなり参照しています
2048ビットあるのに最大でも 145 しか ON にならないのはビットの衝突が理由です.
Morgan FP は各部分構造をハッシュ関数で 0〜2047 のビットに割り当てます.
分子が大きくなると部分構造は増えますが,
異なる部分構造が同じビットに割り当てられる(衝突する)ことが増えるため,
ON ビット数は頭打ちになります.
衝突が起きると,別々の部分構造が同じビットに割り当てられ,
モデルから見ると区別できなくなります.
本来「ベンゼン環を持つ」という情報が「ベンゼン環かピリジン環のどちらか」に劣化してしまうイメージです.
特に大きい分子ほど部分構造が多く衝突しやすいため,
表現が「潰れて」予測が難しくなる可能性があります.
ただし実務で扱う分子の多くは中規模(分子量200〜600)で,
部分構造も20〜50個程度なので,2048ビットあれば衝突は限定的です.
溶解度予測で MACCS(167ビット)が健闘したように,
ビット数が少なくても実用上は足りることも多いです.
根本的に衝突を避けたい場合は,分子グラフを直接扱うGNNが有効だそうです.
これは今後学んでいきたいテーマです.
まとめ
- Delaney データセットでは平均 21.5,最大 68
- 有名な大きい分子でも最大 145(Vancomycin)程度
- ON ビット数は分子の大きさよりも「構造的多様性」を反映
- 2048ビットでも衝突があり,ON ビット数には実質的な上限がある
- ビット数を増やせば衝突は減るが,計算コストとのトレードオフ
興味から始めましたが,フィンガープリントと構造の相関を構造レベルで実感でき,良い勉強になりました.





