Before any ML model can learn from molecules, you need a way to represent them as numbers. Molecular fingerprints and descriptors are the foundational featurization tools of computational drug discovery. They convert a chemical structure, typically encoded as a SMILES string, into a fixed-length numeric vector that a classifier, regressor, or neural network can process.
This guide covers the main fingerprint families, physicochemical descriptors, how to compute them efficiently with RDKit, and the practical decision of when fingerprints are the right tool versus graph-based representations.
What Molecular Fingerprints Are and Why They Matter
A molecular fingerprint is a binary (or occasionally count-based) vector where each bit encodes the presence or absence of a specific structural feature. The idea goes back to the 1980s when Daylight Chemical Information Systems developed the first hashed fingerprints for substructure searching. The core insight is simple: two molecules with similar fingerprints are likely to have similar properties, a principle known as the molecular similarity principle.
In drug discovery, fingerprints are used for:
- Similarity search: finding analogs of a hit compound in a chemical library
- QSAR modeling: quantitative structure-activity relationship models predicting biological activity
- ADMET prediction: predicting solubility, permeability, metabolic stability
- Clustering: grouping compounds into chemical series for medicinal chemistry
- Diversity selection: picking a maximally diverse subset from a large library
Fingerprints are fast to compute (microseconds per molecule), compact (512 to 2048 bits), and work directly with all classical ML models without any architectural changes. For datasets up to roughly 100,000 compounds, a well-tuned Random Forest or XGBoost on Morgan fingerprints often matches or beats more complex approaches.
Types of Molecular Fingerprints
Morgan Fingerprints (ECFP)
Morgan fingerprints, also called ECFP (Extended Connectivity Fingerprints), are the most widely used fingerprints in drug discovery ML. They were introduced by Rogers and Hahn in 2010 and are based on the Morgan circular algorithm.
The algorithm works in iterations called radii:
- Each atom gets an initial identifier based on its atomic number, charge, degree, number of hydrogens, and aromaticity.
- At each iteration, each atom's identifier is updated by hashing together its current identifier and the identifiers of its neighbors.
- After r iterations, each atom's identifier encodes the circular neighborhood of radius r around that atom.
- All atom identifiers across all radii are folded (hashed) into a bit vector of the target length.
ECFP naming convention: ECFP4 means radius 2 (diameter 4). ECFP6 means radius 3 (diameter 6). Higher radius captures larger structural contexts but also increases sparsity.
| Fingerprint | Radius / Diameter | Captures | Typical Use |
|---|---|---|---|
| ECFP2 / ECFP(r=1) | Radius 1, Diameter 2 | Immediate bonded neighbors | Very local features |
| ECFP4 / ECFP(r=2) | Radius 2, Diameter 4 | 2-bond neighborhoods | Default for most tasks |
| ECFP6 / ECFP(r=3) | Radius 3, Diameter 6 | 3-bond neighborhoods | When global structure matters |
| FCFP4 | Radius 2 | Pharmacophoric features | Scaffold hopping |
FCFP (Feature Circular Fingerprints) replace atom identity with pharmacophoric feature classes (hydrogen bond donor, acceptor, aromatic, positive/negative charge), making them more tolerant of scaffold changes while preserving binding pharmacophore patterns.
MACCS Keys
MACCS (MDL Drug Data Report) keys are a 166-bit substructure-based fingerprint where each bit corresponds to a predefined SMARTS pattern (a chemical substructure query). Bit 160, for example, encodes whether the molecule contains a ring with more than two nitrogen atoms.
MACCS keys are highly interpretable because the meaning of each bit is fixed and human-readable. They are widely used in:
- Scaffold analysis and lead series classification
- Pharmacophore-based similarity
- Interpretability-critical applications where you need to explain which structural features drive predictions
The limitation is the fixed 166-bit length, which cannot be increased, and the substructures were designed for drug-like small molecules. They may miss features important for some target classes.
Topological Torsion and Atom Pairs
Topological torsion fingerprints encode four-atom paths through the molecule, capturing connectivity patterns at a different scale than ECFP. Atom pair fingerprints encode all pairs of atoms along with the shortest path length between them. Both are available in RDKit and are sometimes combined with Morgan fingerprints to improve scaffold-diverse predictions.
RDKit Fingerprints (Topological)
The RDKit native topological fingerprint is based on hashed linear paths through the molecular graph. It is similar in spirit to Daylight fingerprints and is fast and effective for similarity searching. Unlike Morgan fingerprints, it is path-based rather than sphere-based, making it sensitive to path length and connectivity patterns.
Physicochemical Descriptors
Beyond fingerprints, molecules can be characterized by numeric descriptors that capture global properties: molecular weight, number of rotatable bonds, polar surface area, LogP (lipophilicity), and hundreds more. RDKit exposes over 200 descriptors through its Descriptors and Chem.rdMolDescriptors modules.
| Descriptor | Description | Drug Discovery Relevance |
|---|---|---|
| MolWt | Molecular weight (Da) | Lipinski rule (below 500 Da) |
| MolLogP | Wildman-Crippen partition coefficient | Membrane permeability, solubility |
| NumHDonors | Count of hydrogen bond donors | Lipinski rule (5 or fewer) |
| NumHAcceptors | Count of hydrogen bond acceptors | Lipinski rule (10 or fewer) |
| TPSA | Topological polar surface area | Oral bioavailability, BBB penetration |
| NumRotatableBonds | Count of rotatable bonds | Oral bioavailability (below 10) |
| FractionCSP3 | Fraction of sp3 carbons | Complexity, aqueous solubility |
| RingCount | Number of rings | Lead-likeness assessment |
| BertzCT | Bertz complexity index | Structural complexity |
Descriptors are particularly useful as:
- Standalone features for simple ADMET models (LogP, TPSA for solubility prediction)
- Supplements to fingerprints to add global context
- Filters before expensive computation (Lipinski rule-of-five filtering)
For high-throughput modeling, consider using rdkit.Chem.Descriptors.CalcMolDescriptors which returns all descriptors as a dictionary in a single call.
Computing Fingerprints with RDKit
RDKit is the standard open-source cheminformatics toolkit for Python. All fingerprint computations start from a molecule object created by parsing a SMILES string.
Installation and Basic Setup
# Install via conda (recommended) or pip
# conda install -c conda-forge rdkit
# pip install rdkit
from rdkit import Chem
from rdkit.Chem import AllChem, MACCSkeys, Descriptors, rdMolDescriptors
import numpy as np
# Parse a SMILES string into a molecule object
smiles = "CC(=O)Oc1ccccc1C(=O)O" # Aspirin
mol = Chem.MolFromSmiles(smiles)
if mol is None:
raise ValueError(f"Invalid SMILES: {smiles}")
print(f"Molecule has {mol.GetNumAtoms()} atoms and {mol.GetNumBonds()} bonds")
Morgan Fingerprints
from rdkit.Chem import AllChem
import numpy as np
def morgan_fingerprint(smiles: str, radius: int = 2, n_bits: int = 2048) -> np.ndarray:
"""
Compute Morgan (ECFP) fingerprint as a numpy bit array.
Args:
smiles: SMILES string of the molecule
radius: Morgan radius (2 = ECFP4, 3 = ECFP6)
n_bits: Length of the bit vector
Returns:
numpy array of shape (n_bits,) with dtype uint8
"""
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return np.zeros(n_bits, dtype=np.uint8)
fp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=radius, nBits=n_bits)
arr = np.zeros(n_bits, dtype=np.uint8)
fp.ToNumpyArray(arr)
return arr
# Example
fp = morgan_fingerprint("CC(=O)Oc1ccccc1C(=O)O")
print(f"Fingerprint shape: {fp.shape}")
print(f"Number of set bits: {fp.sum()}") # Typically 30-80 out of 2048
Computing Fingerprints for a Dataset
import pandas as pd
from rdkit import Chem
from rdkit.Chem import AllChem
import numpy as np
from typing import List, Optional
def smiles_to_fingerprints(
smiles_list: List[str],
radius: int = 2,
n_bits: int = 2048,
use_counts: bool = False
) -> np.ndarray:
"""
Vectorize a list of SMILES strings into a fingerprint matrix.
Returns array of shape (n_molecules, n_bits).
Invalid SMILES are represented as all-zero rows.
"""
X = np.zeros((len(smiles_list), n_bits), dtype=np.uint8)
for i, smi in enumerate(smiles_list):
mol = Chem.MolFromSmiles(smi)
if mol is None:
continue # Leave as zero row
if use_counts:
fp = AllChem.GetMorganFingerprint(mol, radius=radius)
for bit_idx, count in fp.GetNonzeroElements().items():
# Fold into n_bits
X[i, bit_idx % n_bits] = min(count, 255)
else:
fp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=radius, nBits=n_bits)
fp.ToNumpyArray(X[i])
return X
# Load a dataset
df = pd.read_csv("compounds.csv") # Must have a 'smiles' column
X = smiles_to_fingerprints(df["smiles"].tolist(), radius=2, n_bits=2048)
print(f"Feature matrix shape: {X.shape}")
MACCS Keys
from rdkit.Chem import MACCSkeys
def maccs_fingerprint(smiles: str) -> np.ndarray:
"""
Compute 166-bit MACCS keys fingerprint.
Note: RDKit MACCS keys have 167 bits (index 0 is unused), so the
effective length is 166 meaningful bits at indices 1-166.
"""
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return np.zeros(167, dtype=np.uint8)
fp = MACCSkeys.GenMACCSKeys(mol)
arr = np.zeros(167, dtype=np.uint8)
fp.ToNumpyArray(arr)
return arr
print(maccs_fingerprint("CC(=O)Oc1ccccc1C(=O)O").sum(), "bits set")
Physicochemical Descriptors
from rdkit.Chem import Descriptors
from rdkit.Chem import rdMolDescriptors
def lipinski_descriptors(smiles: str) -> dict:
"""
Compute the Lipinski Rule-of-Five descriptors plus TPSA.
Returns a dict with keys: MW, LogP, HBD, HBA, TPSA, RotBonds.
"""
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return {}
return {
"MW": Descriptors.MolWt(mol),
"LogP": Descriptors.MolLogP(mol),
"HBD": rdMolDescriptors.CalcNumHBD(mol),
"HBA": rdMolDescriptors.CalcNumHBA(mol),
"TPSA": rdMolDescriptors.CalcTPSA(mol),
"RotBonds": rdMolDescriptors.CalcNumRotatableBonds(mol),
}
aspirin_props = lipinski_descriptors("CC(=O)Oc1ccccc1C(=O)O")
print(aspirin_props)
# {'MW': 180.16, 'LogP': 1.31, 'HBD': 1, 'HBA': 4, 'TPSA': 63.6, 'RotBonds': 3}
def check_lipinski(props: dict) -> bool:
"""Lipinski Rule of Five filter."""
return (
props.get("MW", 9999) <= 500 and
props.get("LogP", 9999) <= 5 and
props.get("HBD", 9999) <= 5 and
props.get("HBA", 9999) <= 10
)
Combining Fingerprints and Descriptors
def full_featurization(smiles: str, n_bits: int = 1024) -> Optional[np.ndarray]:
"""
Concatenate ECFP4 (1024 bits) + MACCS (167 bits) + 6 Lipinski descriptors.
Returns array of shape (1197,) or None for invalid SMILES.
"""
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
# Morgan fingerprint
ecfp = np.zeros(n_bits, dtype=np.float32)
AllChem.GetMorganFingerprintAsBitVect(mol, radius=2, nBits=n_bits).ToNumpyArray(ecfp)
# MACCS keys
maccs = np.zeros(167, dtype=np.float32)
MACCSkeys.GenMACCSKeys(mol).ToNumpyArray(maccs)
# Physicochemical descriptors (normalized manually)
props = np.array([
Descriptors.MolWt(mol) / 500.0,
Descriptors.MolLogP(mol) / 5.0,
rdMolDescriptors.CalcNumHBD(mol) / 5.0,
rdMolDescriptors.CalcNumHBA(mol) / 10.0,
rdMolDescriptors.CalcTPSA(mol) / 150.0,
rdMolDescriptors.CalcNumRotatableBonds(mol) / 10.0,
], dtype=np.float32)
return np.concatenate([ecfp, maccs, props])
When to Use Fingerprints vs. Graph Representations
A common question in molecular ML is whether to use fingerprints or graph neural networks (GNNs). The answer depends on your dataset size, compute budget, and the complexity of the structure-activity relationship.
| Factor | Fingerprints + Classical ML | Graph Neural Networks |
|---|---|---|
| Dataset size | Works well from 100 to 100K | Needs 10K+ to outperform fingerprints reliably |
| Training time | Seconds to minutes | Minutes to hours |
| Interpretability | SHAP / feature importance on bits | Attention maps, but harder to interpret |
| Stereochemistry | Partial (chiral bits available) | Full (3D GNNs handle conformers) |
| Scaffold hopping | Limited by bit collisions | Better generalization across scaffolds |
| Production serving | Trivial (numpy array) | Requires graph batching infrastructure |
| Activity cliffs | Often misses fine-grained SAR | Can capture local structural changes |
Recommendation: always start with ECFP4 (2048 bits) + Random Forest or XGBoost as your baseline. If the dataset has more than 50,000 compounds and the task benefits from local structural sensitivity (e.g., potency prediction for a single target), then benchmark a GNN. For ADMET tasks with smaller datasets, fingerprints are often competitive or superior.
Common Pitfalls
Bit Collision
Morgan fingerprints hash atom environments into a fixed-length bit vector. When two different structural fragments hash to the same bit index, a collision occurs. For example, with n_bits=512, the collision rate is high enough to degrade model performance on structurally diverse datasets. A standard choice is 2048 bits; going lower than 1024 is rarely justified.
You can diagnose collision rate by comparing folded vs. unfolded (count-based) fingerprints and checking whether bit density changes significantly with the number of bits.
Radius Selection
A higher radius (ECFP6 vs. ECFP4) captures larger chemical contexts but makes the fingerprint sparser. For lead optimization within a scaffold series, ECFP4 is usually sufficient. For more diverse libraries or when you need to capture ring system context, ECFP6 may help. In practice, try both and cross-validate.
Information Loss from Binarization
Binary fingerprints lose count information. A molecule with three occurrences of a ring system looks identical to one with a single occurrence. Count-based Morgan fingerprints (GetMorganFingerprint without converting to bit vectors) preserve this information and sometimes improve performance, particularly for repetitive structures like polymers or multi-ring systems.
Ignoring Invalid SMILES
Datasets often contain malformed or non-standard SMILES. Chem.MolFromSmiles returns None for invalid inputs. You must handle these explicitly, either by filtering them out before training or by assigning zero vectors (which can introduce bias). Always count the invalid fraction before proceeding.
No Normalization for Descriptors
Unlike fingerprint bits (which are bounded to 0 or 1), physicochemical descriptors span very different ranges: molecular weight in hundreds, TPSA in tens to hundreds, ring count in single digits. Tree-based models (Random Forest, XGBoost) are invariant to scale, but linear models and neural networks require standardization. Use sklearn's StandardScaler or MinMaxScaler when combining descriptors with linear models.
Practical Tips for Drug Discovery ML
- Start simple: ECFP4 (2048 bits) + Random Forest is a strong baseline for almost any molecular property prediction task. Do not skip this step.
- Scaffold split for honest evaluation: Random splits inflate performance by allowing nearly identical molecules in train and test. Use Bemis-Murcko scaffold splits with RDKit's
MurckoDecomposeto get realistic estimates of model generalization. - Batch computation for large libraries: For screening libraries of 1M+ molecules, use multiprocessing. RDKit is not thread-safe for molecule parsing but is safe for fingerprint computation on pre-parsed objects.
- Cache fingerprints to disk: Fingerprint computation is reproducible and relatively slow at scale. Compute once, save as a numpy .npy file or HDF5, and reload for each experiment.
- Use SMILES canonicalization: Always canonicalize SMILES with
Chem.MolToSmiles(mol)before storing or deduplicating, since the same molecule can have multiple valid SMILES representations. - Log invalid SMILES rate: In a clean drug-like dataset (ChEMBL, ZINC), the invalid SMILES rate should be below 1%. A higher rate suggests data quality issues or parser version mismatches.
Further Reading
- RDKit Getting Started in Python
- Rogers and Hahn (2010): Extended-Connectivity Fingerprints
- Daylight Theory: Fingerprints
- RDKit Blog by Greg Landrum
- ChEMBL Interface Documentation
Try it live
Explore a related model hands-on: open Molecule Viewer and change its parameters while it runs. Everything stays in your browser.
▶ Open Molecule Viewer simulation