SAR interpretation

Matched molecular pairs, activity cliffs, SALI, SARI, Free-Wilson analysis and R-group decomposition.

This is the part of the workflow that tells you why a model performs as it does, and — more usefully — predicts before you fit anything whether a regression model can work on this series at all.

Activity cliffs

An activity cliff is a pair of near-identical structures with very different activity. Cliffs are where QSAR fails by construction: any model built on a smooth similarity assumption must predict them wrong.

>>> from qsarkit.sar import activity_cliff_report
>>> report = activity_cliff_report(demo_mols, DEMO_Y, similarity_threshold=0.5)
>>> report["n_cliffs"], round(report["cliff_ratio"], 4)
(4, 0.0145)

The report names the substituent changes responsible, which is the part a chemist can act on:

>>> sorted(report["top_transformations"])
['[1*]C>>[1*]Cl', '[1*]Cl>>[1*]Br', '[1*]Cl>>[1*]N']

and the specific pairs:

>>> cliff = report["top_cliffs"][0]
>>> cliff.index_a, cliff.index_b, round(cliff.delta, 2)
(2, 5, 2.9)

Note

The default similarity threshold is 0.85, the figure usually quoted in the cliff literature — calibrated for drug-sized molecules with a large shared core. The demo set is small molecules, where a single-atom change alters every atom environment within the fingerprint radius and similarity scores run far below intuition. Threshold to your data, not to the paper.

SALI and the activity landscape

SALI ranks pairs by how steep the cliff is: activity difference over structural distance.

>>> from qsarkit.sar import SALIAnalyzer
>>> sali = SALIAnalyzer().sali_matrix(demo_mols, DEMO_Y)
>>> sali.shape
(24, 24)
>>> round(float(sali.max()), 2)
6.38

SARI condenses the whole landscape into one number, separating the continuous component (smooth SAR, which a model can learn) from the discontinuous one (cliffs, which it cannot):

>>> from qsarkit.sar import SARIAnalyzer
>>> scores = SARIAnalyzer().analyze(demo_mols, DEMO_Y)
>>> round(scores["sari"], 3), round(scores["discontinuity"], 3)
(0.481, 0.065)

Matched molecular pairs

MMPs are the formalization of “change one thing and see what happens”, which is how medicinal chemistry is actually done.

>>> from qsarkit.sar import MatchedMolecularPairs
>>> pairs = MatchedMolecularPairs().find_pairs(demo_mols[:8])
>>> len(pairs)
12
>>> print(pairs[0])
MatchedPair([1*]C(=O)O>>[1*]NC(C)=O)

The transformation is recorded as a SMIRKS-like rule, so identical changes across different cores aggregate — which is what turns a list of pairs into a transferable design rule.

Free-Wilson

Free-Wilson analysis fits activity as a sum of substituent contributions: the oldest QSAR method still in use, and still the most interpretable when the series shares one core.

>>> from qsarkit.sar import FreeWilsonAnalysis
>>> analysis = FreeWilsonAnalysis()
>>> hasattr(analysis, "fit")
True

Its assumption — that substituent effects are additive and independent — is exactly what an activity cliff violates. A high cliff ratio is a warning that Free-Wilson will mislead here.

API

Structure-activity relationship analysis: MMPs, activity cliffs, SAR tables.

This module answers the interpretation questions that come after a QSAR model is fitted: which structural changes drive activity, where the SAR is smooth versus discontinuous, and which compound pairs form activity cliffs that any similarity-based model will struggle with.

Examples

>>> from rdkit import Chem
>>> from qsarkit.sar import ActivityCliffDetector, activity_cliff_report
>>> mols = [Chem.MolFromSmiles(s) for s in
...         ("CC(=O)Nc1ccc(Cl)cc1", "CC(=O)Nc1ccc(Br)cc1")]
>>> cliffs = ActivityCliffDetector(similarity_threshold=0.6).detect(mols, [9.0, 5.0])
>>> len(cliffs)
1
>>> activity_cliff_report(mols, [9.0, 5.0], similarity_threshold=0.6)["cliff_ratio"]
1.0
class qsarkit.sar.MatchedPair(mol_a, mol_b, core, transformation, delta_activity=None, index_a=-1, index_b=-1)[source]

Bases: object

One matched molecular pair: two molecules sharing a common core.

Variables:
  • mol_b (mol_a,) – The paired molecules.

  • core (str) – Canonical SMILES of the shared context (with attachment points).

  • transformation (str) – The change, written "<frag_a>>>frag_b>" in SMIRKS-like form.

  • delta_activity (float or None) – activity_b - activity_a when activities were supplied.

  • index_b (index_a,) – Positions of the two molecules in the input sequence.

mol_a: Mol
mol_b: Mol
core: str
transformation: str
delta_activity: float | None
index_a: int
index_b: int
class qsarkit.sar.MatchedMolecularPairs(max_cuts=1, max_fragment_heavy_atoms=13)[source]

Bases: object

Identify matched molecular pairs by the fragment-index algorithm.

Implements the Hussain & Rea approach: every molecule is fragmented at each acyclic single bond (and optionally at pairs or triples of such bonds); each (context, fragment) split is indexed by its context; and any two molecules sharing a context — but differing in the attached fragment — constitute a matched pair whose transformation is fragment_a >> fragment_b.

This is what turns a flat activity table into interpretable SAR: the effect of a substituent change, measured across every pair in which it occurs.

Parameters:
  • max_cuts (int) – Number of bonds cut simultaneously. 1 finds single-substituent changes (the vast majority of useful MMPs); 2-3 also finds linker and multi-point changes at rapidly growing cost.

  • max_fragment_heavy_atoms (Optional[int]) – Discard splits whose variable fragment is larger than this, which keeps pairs interpretable (a “pair” differing by half the molecule is not a useful MMP). None disables the filter.

Examples

>>> from rdkit import Chem
>>> mols = [Chem.MolFromSmiles(s) for s in ("c1ccccc1Cl", "c1ccccc1Br")]
>>> pairs = MatchedMolecularPairs().find_pairs(mols)
>>> len(pairs) >= 1
True

References

find_pairs(mols, activities=None)[source]

Find all matched molecular pairs in a set of molecules.

Parameters:
  • mols (Sequence[Mol]) – Molecules to pair up.

  • activities (Optional[Sequence[float]]) – Parallel activity values (e.g. pIC50). When given, each pair carries delta_activity = activity_b - activity_a.

Returns:

All pairs found, de-duplicated by (index_a, index_b, core).

Return type:

List[MatchedPair]

class qsarkit.sar.MMPAnalyzer(max_cuts=1, max_fragment_heavy_atoms=13)[source]

Bases: object

Summarize the SAR encoded by a set of matched molecular pairs.

Aggregates pairs by transformation to answer the medicinal-chemistry question “what does this substituent change usually do to potency?”, which is the basis of MMP-derived design rules.

Parameters:

Examples

>>> from rdkit import Chem
>>> mols = [Chem.MolFromSmiles(s) for s in ("c1ccccc1Cl", "c1ccccc1Br")]
>>> analyzer = MMPAnalyzer()
>>> pairs = analyzer.find_pairs(mols, [5.0, 6.0])
>>> len(pairs) >= 1
True

References

find_pairs(mols, activities=None)[source]

Delegate to MatchedMolecularPairs.find_pairs().

Return type:

List[MatchedPair]

transformation_summary(pairs)[source]

Aggregate activity change per transformation.

Parameters:

pairs (Sequence[MatchedPair]) – Pairs carrying delta_activity.

Returns:

Columns transformation, count, mean_delta, median_delta, std_delta, sorted by descending count then descending mean_delta. Transformations whose pairs have no activity data are omitted.

Return type:

DataFrame

to_dataframe(pairs)[source]

Render pairs as a table.

Parameters:

pairs (Sequence[MatchedPair])

Returns:

Columns smiles_a, smiles_b, core, transformation, delta_activity, index_a, index_b.

Return type:

DataFrame

class qsarkit.sar.ActivityCliff(index_a, index_b, mol_a, mol_b, similarity, activity_a, activity_b, delta, sali)[source]

Bases: object

A pair of similar molecules with a large activity difference.

Variables:
  • index_b (index_a,) – Positions of the two molecules in the input sequence.

  • mol_b (mol_a,) – The two molecules.

  • similarity (float) – Structural similarity in [0, 1].

  • activity_b (activity_a,) – Their activities on a logarithmic scale (e.g. pIC50).

  • delta (float) – abs(activity_a - activity_b).

  • sali (float) – Structure-Activity Landscape Index for the pair.

index_a: int
index_b: int
mol_a: Mol
mol_b: Mol
similarity: float
activity_a: float
activity_b: float
delta: float
sali: float
class qsarkit.sar.ActivityCliffDetector(similarity_threshold=0.85, activity_threshold=2.0, method='fingerprint', radius=2, n_bits=2048)[source]

Bases: object

Detect activity cliffs: similar structures with very different activity.

Activity cliffs are the single biggest obstacle to QSAR: they violate the similarity-property principle that regression models rely on, and a model that cannot reproduce them will systematically mispredict the most interesting compounds in a series. Detecting them tells you both where a model will fail and where the SAR carries real information.

A pair (i, j) is a cliff when similarity(i, j) >= similarity_threshold and |activity_i - activity_j| >= activity_threshold.

Parameters:
  • similarity_threshold (float) – Minimum structural similarity. 0.85 on ECFP4 is the conventional cutoff in the activity-cliff literature.

  • activity_threshold (float) – Minimum absolute activity difference, in log units. 2.0 means a 100-fold potency change.

  • method (Literal['fingerprint', 'scaffold', 'mmp']) – How structural similarity is measured. "scaffold" and "mmp" give binary similarity (1.0 for same scaffold / a matched pair), so with those the similarity threshold acts as a simple on/off test.

  • radius (int) – Morgan radius (ECFP4 = radius 2) for the fingerprint method.

  • n_bits (int) – Fingerprint length for the fingerprint method.

Examples

A 4-Cl / 4-Br swap on the same anilide core, four log units apart:

>>> from rdkit import Chem
>>> mols = [Chem.MolFromSmiles(s) for s in
...         ("CC(=O)Nc1ccc(Cl)cc1", "CC(=O)Nc1ccc(Br)cc1")]
>>> detector = ActivityCliffDetector(similarity_threshold=0.6)
>>> cliffs = detector.detect(mols, [9.0, 5.0])
>>> len(cliffs)
1
>>> round(cliffs[0].similarity, 3), cliffs[0].delta
(0.615, 4.0)

Note how low that similarity is for a single-atom change. Morgan fingerprints of small molecules score far below intuition, because one substituent alters every atom environment within radius bonds of it. A threshold of 0.85 – the usual figure quoted for cliff analysis, and this class’s default – is calibrated for drug-sized molecules with a large shared core, and will find nothing in a set of fragments.

References

similarity_matrix(mols)[source]

Pairwise structural similarity under the configured method.

Parameters:

mols (Sequence[Mol])

Return type:

ndarray[tuple[Any, ...], dtype[double]]

detect(mols, activities)[source]

Find every activity cliff in a dataset.

Parameters:
  • mols (Sequence[Mol]) – Molecules, all non-None.

  • activities (Sequence[float]) – Activities on a logarithmic scale (pIC50, pKi, …). Using a linear scale here would make the threshold meaningless.

Returns:

Sorted by descending SALI, so the sharpest cliffs come first.

Return type:

List[ActivityCliff]

to_dataframe(cliffs)[source]

Render detected cliffs as a table.

Parameters:

cliffs (Sequence[ActivityCliff])

Return type:

DataFrame

class qsarkit.sar.SALIAnalyzer(method='fingerprint', radius=2, n_bits=2048)[source]

Bases: object

Structure-Activity Landscape Index (SALI) analysis.

SALI quantifies how sharply activity changes with structure:

SALI(i, j) = |A_i - A_j| / (1 - sim(i, j))

Large values mark cliffs — small structural change, large activity change. Beyond the pairwise matrix, the SALI curve scores how well a model reproduces the landscape: pairs are ranked by true SALI and by predicted SALI, and the fraction of top-ranked true pairs the model also ranks highly gives a curve whose area (in [0, 1], 1 = perfect) is a landscape-aware model-quality metric that ordinary RMSE/R2 completely miss.

Parameters:
  • method (Literal['fingerprint', 'scaffold', 'mmp']) – Similarity backend.

  • radius (int) – Morgan radius for the fingerprint method.

  • n_bits (int) – Fingerprint length for the fingerprint method.

Examples

>>> from rdkit import Chem
>>> mols = [Chem.MolFromSmiles(s) for s in ("CCO", "CCC", "CCN")]
>>> analyzer = SALIAnalyzer()
>>> S = analyzer.sali_matrix(mols, [5.0, 6.0, 7.0])
>>> S.shape
(3, 3)

References

  • Guha, R. & Van Drie, J. H. (2008). “Structure-Activity Landscape Index: Identifying and Quantifying Activity Cliffs.” J. Chem. Inf. Model., 48(3), 646-658. https://doi.org/10.1021/ci7004093

  • Guha, R. (2012). “Exploring Structure-Activity Data Using the Landscape Paradigm.” WIREs Comput. Mol. Sci. / J. Chem. Inf. Model., 52(8), 2181-2191. https://doi.org/10.1021/ci300047k

  • Guha, R. & Van Drie, J. H. (2008). “Assessing How Well a Modeling Protocol Captures a Structure-Activity Landscape.” J. Chem. Inf. Model., 48(8), 1716-1728. https://doi.org/10.1021/ci8001414

sali_matrix(mols, activities)[source]

Pairwise SALI matrix.

Parameters:
Returns:

Symmetric, zero diagonal, inf where two distinct molecules have identical structure fingerprints.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

sali_network(mols, activities, percentile=95.0)[source]

Build a graph of the highest-SALI pairs.

Parameters:
  • mols (Sequence[Mol])

  • activities (Sequence[float])

  • percentile (float) – Keep edges whose SALI is at or above this percentile of the finite SALI values.

Returns:

Nodes carry activity; edges carry sali.

Return type:

Any

sali_curve(mols, y_true, y_pred, n_points=50)[source]

SALI curve comparing true and predicted activity landscapes.

For each cutoff X (fraction of the highest-SALI true pairs), the curve reports the fraction of those pairs whose activity ordering the model reproduces.

Parameters:
  • mols (Sequence[Mol])

  • y_true (Sequence[float]) – Observed activities.

  • y_pred (Sequence[float]) – Predicted activities.

  • n_points (int) – Number of cutoffs sampled along the curve.

Return type:

Tuple[ndarray[tuple[Any, ...], dtype[double]], ndarray[tuple[Any, ...], dtype[double]]]

Returns:

  • x (ndarray of shape (n_points,)) – Fraction of top-SALI pairs considered, in (0, 1].

  • y (ndarray of shape (n_points,)) – Fraction of those pairs ordered correctly, in [0, 1].

sali_auc(mols, y_true, y_pred, n_points=50)[source]

Area under the SALI curve — a landscape-aware model score.

Parameters:
Returns:

Area in [0, 1]; 1.0 means every cliff’s direction is predicted correctly, 0.5 is chance.

Return type:

float

class qsarkit.sar.SARIAnalyzer(similarity_threshold=0.6, reference_delta=3.0, radius=2, n_bits=2048)[source]

Bases: object

Structure-Activity Relationship Index (SARI): continuity vs discontinuity.

SARI scores a compound set on two orthogonal axes and combines them:

SARI = 0.5 * ((1 - continuity_norm) + discontinuity_norm)

The continuity score reflects smooth, gradual SAR (similar molecules with similar potency, weighted by potency); the discontinuity score reflects cliffs (similar molecules with very different potency). A series can be high in both — a “heterogeneous” SAR that is smooth in one region and cliff-ridden in another.

Parameters:
  • similarity_threshold (float) – Minimum similarity for a pair to contribute to the discontinuity term.

  • reference_delta (float) – Activity difference, in log units, treated as maximally discontinuous when normalizing the discontinuity score. Fixing this on an absolute scale (rather than the dataset’s own range) is what makes SARI comparable between series.

  • radius (int) – Morgan radius.

  • n_bits (int) – Fingerprint length.

Examples

>>> from rdkit import Chem
>>> mols = [Chem.MolFromSmiles(s) for s in ("CCO", "CCC", "CCN", "CCCl")]
>>> scores = SARIAnalyzer().analyze(mols, [5.0, 5.2, 5.1, 8.0])
>>> set(scores) == {"continuity", "discontinuity", "sari"}
True

References

  • Peltason, L. & Bajorath, J. (2007). “SAR Index: Quantifying the Nature of Structure-Activity Relationships.” J. Med. Chem., 50(23), 5571-5578. https://doi.org/10.1021/jm070562u

  • Wassermann, A. M., Wawer, M. & Bajorath, J. (2010). “Activity Landscape Representations for Structure-Activity Relationship Analysis.” J. Med. Chem., 53(23), 8209-8223. https://doi.org/10.1021/jm100933w

analyze(mols, activities)[source]

Compute continuity, discontinuity and the combined SARI score.

Parameters:
Returns:

Keys continuity, discontinuity, sari.

Return type:

Dict[str, float]

class qsarkit.sar.ActivityLandscapePlotter(similarity_threshold=0.6, activity_threshold=0.6, radius=2, n_bits=2048)[source]

Bases: object

Structure-Activity Similarity (SAS) map data and Plotly figure.

A SAS map plots every compound pair as (structure similarity, activity similarity) and reads the four quadrants as distinct SAR regimes:

Structure sim.

Activity sim.

Interpretation

high

high

smooth / continuous SAR

high

low

activity cliff

low

high

scaffold hop

low

low

nondescript

Parameters:
  • similarity_threshold (float) – Structure-similarity boundary between the left and right halves.

  • activity_threshold (float) – Activity-similarity boundary between the top and bottom halves.

  • radius (int) – Morgan radius.

  • n_bits (int) – Fingerprint length.

Examples

>>> from rdkit import Chem
>>> mols = [Chem.MolFromSmiles(s) for s in ("CCO", "CCC", "CCN")]
>>> df = ActivityLandscapePlotter().sas_data(mols, [5.0, 7.0, 5.1])
>>> sorted(df.columns)
['activity_similarity', 'delta_activity', 'index_a', 'index_b',
 'quadrant', 'structure_similarity']

One row per pair, each assigned to a quadrant of the SAS map:

>>> len(df)               # three pairs from three molecules
3
>>> sorted(set(df["quadrant"]))
['nondescript', 'scaffold hop']

References

  • Shanmugasundaram, V. & Maggiora, G. M. (2001). “Characterizing Property and Activity Landscapes Using an Information-Theoretic Approach.” 222nd ACS National Meeting, CINF 77.

  • Wassermann, A. M., Wawer, M. & Bajorath, J. (2010). J. Med. Chem., 53(23), 8209-8223. https://doi.org/10.1021/jm100933w

  • Perez-Villanueva, J. et al. (2011). “Comparison of Multiple 2D Representations for the Activity Landscape Modeling.” Bioorg. Med. Chem., 19(21), 6183-6193. https://doi.org/10.1016/j.bmc.2011.09.024

sas_data(mols, activities)[source]

Compute the SAS-map table (one row per compound pair).

Parameters:
Returns:

Columns index_a, index_b, structure_similarity, activity_similarity, delta_activity, quadrant. Activity similarity is 1 - |dA| / max|dA|.

Return type:

DataFrame

plot(mols, activities)[source]

Render the SAS map as a Plotly scatter with quadrant guides.

Parameters:
Return type:

Figure

qsarkit.sar.activity_cliff_report(mols, activities, similarity_threshold=0.85, activity_threshold=2.0, top_n=10)[source]

Summarize the activity-cliff content of a dataset.

A one-call diagnostic to run before modeling: a high cliff ratio predicts that a regression model will underperform on this series no matter how it is tuned, and points at which scaffolds and which substituent changes are responsible.

Parameters:
Returns:

n_compounds, n_pairs, n_cliffs, cliff_ratio (cliffs / all pairs), cliff_compound_fraction (fraction of compounds involved in at least one cliff), max_sali, top_cliffs, top_scaffolds (scaffold SMILES -> cliff count), top_transformations (MMP transformation -> cliff count), and sari.

Return type:

Dict[str, Any]

Examples

>>> from rdkit import Chem
>>> mols = [Chem.MolFromSmiles(s) for s in
...         ("CC(=O)Nc1ccc(Cl)cc1", "CC(=O)Nc1ccc(Br)cc1")]
>>> report = activity_cliff_report(mols, [9.0, 5.0], similarity_threshold=0.6)
>>> report["n_cliffs"]
1
>>> report["cliff_ratio"]        # one cliff out of one pair
1.0

References

class qsarkit.sar.RGroupAnalyzer(core=None)[source]

Bases: object

Decompose a congeneric series into a core plus R-group substituents.

Wraps RDKit’s rdRGroupDecomposition to turn a set of analogues into the R-group table medicinal chemists actually reason with: one row per compound, one column per substitution point.

Parameters:

core (Optional[Any]) – The scaffold, as a Mol or SMARTS/SMILES string. When None, the most common Bemis-Murcko scaffold in the series is used.

Examples

>>> from rdkit import Chem
>>> mols = [Chem.MolFromSmiles(s) for s in ("c1ccccc1Cl", "c1ccccc1Br")]
>>> analyzer = RGroupAnalyzer(core="c1ccccc1")
>>> table = analyzer.decompose(mols)
>>> "Core" in table.columns
True

References

decompose(mols)[source]

Run R-group decomposition over a series.

Parameters:

mols (Sequence[Mol]) – Analogues sharing a common core.

Returns:

One row per successfully decomposed molecule; a Core column plus one R1, R2, … column per attachment point, all as SMILES. Molecules that do not match the core are omitted, and their positions are recorded in the frame’s attrs["unmatched"].

Return type:

DataFrame

r_group_positions(table)[source]

List the R-group column names present in a decomposition table.

Parameters:

table (DataFrame) – Output of decompose().

Returns:

e.g. ["R1", "R2"].

Return type:

List[str]

class qsarkit.sar.SARTable(core=None)[source]

Bases: object

R-group x activity table for a congeneric series.

Joins an R-group decomposition to measured activities so that the contribution of each substituent at each position can be read directly, and pivoted into the classic two-position SAR grid.

Parameters:

core (Optional[Any]) – Passed to RGroupAnalyzer.

Examples

>>> from rdkit import Chem
>>> mols = [Chem.MolFromSmiles(s) for s in ("c1ccccc1Cl", "c1ccccc1Br")]
>>> table = SARTable(core="c1ccccc1").build(mols, [5.0, 6.0])
>>> "activity" in table.columns
True

References

build(mols, activities)[source]

Build the R-group + activity table.

Parameters:
Returns:

The decomposition table with an activity column added.

Return type:

DataFrame

pivot(table, row='R1', column='R2', aggfunc='mean')[source]

Pivot into the classic two-position SAR grid.

Parameters:
  • table (DataFrame) – Output of build().

  • row (str) – R-group columns to use as the grid axes.

  • column (str) – R-group columns to use as the grid axes.

  • aggfunc (str) – Aggregation for duplicate cells.

Returns:

Activity grid indexed by row with column as columns.

Return type:

DataFrame

substituent_effects(table, position='R1')[source]

Mean activity and count per substituent at one position.

Parameters:
Returns:

Columns substituent, count, mean_activity, std_activity, sorted by descending mean activity.

Return type:

DataFrame

class qsarkit.sar.FreeWilsonAnalysis(core=None, fit_intercept=True, alpha=0.0)[source]

Bases: object

Free-Wilson additive SAR model over R-group indicator variables.

The original QSAR method: activity is modelled as a baseline plus an additive contribution from each substituent at each position,

\[A = \mu + \sum_{p} \sum_{s} a_{p,s} X_{p,s}\]

fitted by linear regression on one-hot indicators. It is exactly interpretable — each coefficient is “what this substituent is worth at this position, in log units” — and its residuals are themselves informative: large ones mark non-additive SAR, i.e. activity cliffs and substituent interactions the additive model cannot represent.

Parameters:
  • core (Optional[Any]) – Passed to RGroupAnalyzer.

  • fit_intercept (bool) – Whether to fit the baseline term.

  • alpha (float) – Ridge penalty. Free-Wilson designs are often rank-deficient (a substituent appearing once is perfectly confounded with its compound), so a small positive alpha is frequently needed.

Variables:
  • contributions (dict[str, dict[str, float]]) – {position: {substituent: contribution}}.

  • intercept (float) – Baseline activity.

  • r2 (float) – Coefficient of determination on the training series.

  • feature_names (list[str]) – Names of the indicator columns, as "R1=Cl".

Examples

>>> from rdkit import Chem
>>> mols = [Chem.MolFromSmiles(s) for s in
...         ("c1ccccc1Cl", "c1ccccc1Br", "c1ccccc1F")]
>>> fw = FreeWilsonAnalysis(core="c1ccccc1", alpha=0.1)
>>> _ = fw.fit(mols, [5.0, 6.0, 4.0])
>>> isinstance(fw.r2_, float)
True

References

contributions_: Dict[str, Dict[str, float]]
intercept_: float
r2_: float
feature_names_: List[str]
fit(mols, activities)[source]

Fit substituent contributions by linear regression.

Parameters:
Returns:

The fitted analysis.

Return type:

FreeWilsonAnalysis

predict(mols)[source]

Predict activity for new analogues of the same core.

Parameters:

mols (Sequence[Mol]) – Molecules sharing the fitted core.

Returns:

Predictions for the molecules that matched the core.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

residuals()[source]

Training residuals — large values flag non-additive SAR.

Returns:

Columns molecule_index, observed, predicted, residual, sorted by descending absolute residual.

Return type:

DataFrame

to_dataframe()[source]

Substituent contributions as a tidy table.

Returns:

Columns position, substituent, contribution, sorted by descending contribution.

Return type:

DataFrame

References

  • Maggiora, G. M. (2006). “On Outliers and Activity Cliffs — Why QSAR Often Disappoints.” J. Chem. Inf. Model., 46(4), 1535. doi:10.1021/ci060117s

  • Guha, R. & Van Drie, J. H. (2008). “Structure-Activity Landscape Index.” J. Chem. Inf. Model., 48(3), 646-658. doi:10.1021/ci7004093

  • Peltason, L. & Bajorath, J. (2007). “SAR Index: Quantifying the Nature of Structure-Activity Relationships.” J. Med. Chem., 50(23), 5571-5578. doi:10.1021/jm070562u

  • Hussain, J. & Rea, C. (2010). “Computationally Efficient Algorithm to Identify Matched Molecular Pairs (MMPs) in Large Data Sets.” J. Chem. Inf. Model., 50(3), 339-348. doi:10.1021/ci900450m

  • Free, S. M. & Wilson, J. W. (1964). “A Mathematical Contribution to Structure-Activity Studies.” J. Med. Chem., 7(4), 395-399. doi:10.1021/jm00334a001

  • van Tilborg, D., Alenicheva, A. & Grisoni, F. (2022). “Exposing the Limitations of Molecular Machine Learning with Activity Cliffs.” J. Chem. Inf. Model., 62(23), 5938-5951. doi:10.1021/acs.jcim.2c01073