Home▸Articles▸Foundations

Molecular Fingerprints and Descriptors

How to represent molecules as numeric vectors for machine learning: bit-vector fingerprints, physicochemical descriptors, RDKit implementation, and practical guidance on when to use each representation.

mysimulator teamUpdated January 2026≈ 11 min read▶ Open the simulation

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:

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:

  1. Each atom gets an initial identifier based on its atomic number, charge, degree, number of hydrogens, and aromaticity.
  2. At each iteration, each atom's identifier is updated by hashing together its current identifier and the identifiers of its neighbors.
  3. After r iterations, each atom's identifier encodes the circular neighborhood of radius r around that atom.
  4. 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:

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:

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

Further Reading

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

What did you find?

Add reproduction steps (optional)