Feature selection¶
Descriptor selection: variance and correlation filters, mutual
information, recursive feature elimination and Boruta. All are
scikit-learn selectors, so they compose in a pipeline and expose
get_support().
Warning
Select features inside the cross-validation loop, not before it. Choosing columns using all the labels and then splitting is selection bias: the choice has already seen the test set, and the held-out score is optimistic by an amount you cannot estimate afterwards.
Filters¶
The cheapest and most defensible reductions, because they use no labels at all.
>>> from qsarkit.feature_selection import VarianceFilter
>>> X, y = demo_fingerprints(256), DEMO_Y
>>> VarianceFilter().fit_transform(X).shape
(24, 107)
More than half the bits never fire on 24 compounds. They cannot contribute and only slow things down.
>>> from qsarkit.feature_selection import CorrelationFilter
>>> selector = CorrelationFilter(threshold=0.9).fit(X, y)
>>> selector.transform(X).shape
(24, 61)
Constant columns have undefined correlation, so they are identified and dropped explicitly rather than falling out of NaN comparisons:
>>> int(selector.constant_.sum())
149
When y is supplied, the filter keeps whichever member of a correlated
pair correlates better with the target — a better choice than keeping
whichever happened to come first.
Supervised selection¶
>>> from qsarkit.feature_selection import MutualInformationSelector, RFESelector
>>> MutualInformationSelector(k=8).fit_transform(X, y).shape
(24, 8)
>>> RFESelector(n_features_to_select=8).fit_transform(X, y).shape
(24, 8)
Mutual information catches non-linear dependence that a correlation filter misses, and needs no model. RFE is more powerful and far more expensive, and its answer is specific to the estimator it wrapped.
Boruta¶
Boruta asks a different question: not “which are the best k features” but “which features carry more signal than random noise”. It answers with a set of whatever size the data supports, rather than a number you chose:
>>> from qsarkit.feature_selection import BorutaSelector
>>> boruta = BorutaSelector(n_iterations=30, random_state=0).fit(X, y)
>>> int(boruta.get_support().sum()) >= 0
True
Note
Boruta needs enough iterations for its statistical test to reach
significance. With too few it confidently selects nothing — the
selector warns when the iteration count cannot support a decision at
the chosen alpha, rather than silently returning an empty set.
API¶
Descriptor selection for QSAR models.
Molecular descriptor sets routinely contain hundreds of correlated,
constant or uninformative columns. Selecting among them improves both
generalization and the mechanistic interpretability OECD principle 5
asks for. Every selector is scikit-learn compatible with get_support().
Examples
>>> from sklearn.datasets import make_regression
>>> from qsarkit.feature_selection import VarianceFilter
>>> X, y = make_regression(n_samples=50, n_features=8, random_state=0)
>>> VarianceFilter().fit(X, y).get_support().shape
(8,)
References
Guyon, I. & Elisseeff, A. (2003). “An Introduction to Variable and Feature Selection.” J. Mach. Learn. Res., 3, 1157-1182. https://jmlr.org/papers/v3/guyon03a.html
Kursa, M. B. & Rudnicki, W. R. (2010). “Feature Selection with the Boruta Package.” J. Stat. Softw., 36(11), 1-13. https://doi.org/10.18637/jss.v036.i11
- class qsarkit.feature_selection.VarianceFilter(threshold=0.0)[source]¶
Bases:
SelectorMixin,BaseEstimatorDrop near-constant descriptors.
Constant or near-constant descriptor columns carry no discriminating information and can break downstream scalers (division by a near-zero standard deviation) or destabilize linear-model fits. This is the standard first curation step applied to any QSAR descriptor matrix before feature selection or modelling proper begins.
The algorithm is the two-line computation
variances_ = X.var(axis=0),support_ = variances_ > threshold— exactly whatsklearn.feature_selection.VarianceThresholdimplements, reproduced directly here so this estimator shares theqsarkitselector contract (support_,get_support(),transform()viasklearn.feature_selection.SelectorMixin).- Parameters:
threshold (
float) – Descriptors with a training-set variance at or below this value are dropped. The default removes only exactly-constant columns.- Variables:
Examples
>>> import numpy as np >>> from qsarkit.feature_selection import VarianceFilter >>> X = np.array([[1.0, 5.0], [2.0, 5.0], [3.0, 5.0]]) >>> vf = VarianceFilter().fit(X) >>> vf.support_.tolist() [True, False]
References
scikit-learn documentation,
VarianceThreshold. https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.VarianceThreshold.html
- fit(X, y=None)[source]¶
Learn per-descriptor variances and the resulting support mask.
- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Descriptor matrix.y (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str],None]) – Ignored. Present for API consistency.
- Returns:
The fitted selector.
- Return type:
- class qsarkit.feature_selection.CorrelationFilter(threshold=0.95, method='pearson')[source]¶
Bases:
SelectorMixin,BaseEstimatorDrop one descriptor from every pair whose correlation exceeds a threshold.
Collinear descriptors add noise to linear models, inflate coefficient variance and make coefficient interpretation unreliable — a standard QSAR descriptor-curation concern. This selector removes redundant descriptors while trying to keep the more informative member of each correlated pair.
Notes
The algorithm is:
Compute the descriptor-descriptor correlation matrix (Pearson or Spearman).
Determine a processing order: if
yis supplied, features are visited in descending order of|corr(feature, y)|so the more target-relevant member of a correlated pair is considered first; otherwise features are visited in their natural column order.Walk the ordered features, keeping a running set of accepted indices. A candidate feature is kept if its absolute correlation with every already-kept feature is at or below
threshold; otherwise it is redundant with something already kept, and is dropped.
Because the target-relevant feature is visited first when
yis given, a duplicated/near-duplicated copy of an informative descriptor is reliably the one dropped, not the informative descriptor itself.- Parameters:
- Variables:
correlation_matrix (
ndarrayofshape (n_features,n_features)) – The fitted descriptor-descriptor correlation matrix.support (
ndarrayofboolofshape (n_features,)) – True for descriptors kept.n_features_in (
int) – Number of descriptors seen duringfit.constant (
ndarrayofboolofshape (n_features,)) – True for descriptors with zero variance, whose correlation is undefined. These are dropped before the walk rather than as a side effect of comparing against NaN.
Examples
>>> import numpy as np >>> from qsarkit.feature_selection import CorrelationFilter >>> rng = np.random.RandomState(0) >>> x0 = rng.normal(size=200) >>> X = np.column_stack([x0, x0 + rng.normal(scale=1e-3, size=200), rng.normal(size=200)]) >>> y = x0 * 2.0 >>> cf = CorrelationFilter(threshold=0.95).fit(X, y) >>> cf.support_.tolist() [True, False, True]
References
Todeschini, R. & Consonni, V. (2009). “Molecular Descriptors for Chemoinformatics,” 2nd ed. Wiley-VCH. https://doi.org/10.1002/9783527628766
- fit(X, y=None)[source]¶
Learn the correlation matrix and resulting support mask.
- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Descriptor matrix.y (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str],None]) – Target values. When given, features that correlate more strongly withyare preferred as the “kept” member of a redundant pair.
- Returns:
The fitted selector.
- Return type:
- class qsarkit.feature_selection.MutualInformationSelector(task='regression', k=None, percentile=None, random_state=None)[source]¶
Bases:
SelectorMixin,BaseEstimatorKeep the descriptors most informative about the target, by mutual information.
Thin wrapper around
sklearn.feature_selection.mutual_info_regression/mutual_info_classif, which estimate mutual information between each descriptor and the target using a k-nearest-neighbour entropy estimator. Unlike Pearson correlation, mutual information captures non-linear dependencies, which matters for QSAR descriptors that often relate to activity non-monotonically.- Parameters:
task (
Literal['regression','classification']) – Whetheryis continuous or categorical.k (
Optional[int]) – Keep the top-kscoring descriptors. Mutually exclusive withpercentile.percentile (
Optional[float]) – Keep descriptors scoring at or above this percentile (0-100) of the score distribution. Mutually exclusive withk. If neitherknorpercentileis given, defaults to 50 (keep the top half).random_state (
Optional[int]) – Seed forwarded to the mutual-information estimator’s internal noise injection (used to break ties in the nearest-neighbour distances).
- Variables:
Examples
>>> import numpy as np >>> from qsarkit.feature_selection import MutualInformationSelector >>> rng = np.random.RandomState(0) >>> X = rng.normal(size=(300, 3)) >>> y = X[:, 0] ** 2 >>> sel = MutualInformationSelector(k=1, random_state=0).fit(X, y) >>> sel.support_.tolist() [True, False, False]
References
Kraskov, A., Stogbauer, H. & Grassberger, P. (2004). “Estimating Mutual Information.” Phys. Rev. E, 69(6), 066138. https://doi.org/10.1103/PhysRevE.69.066138
- fit(X, y=None)[source]¶
Score every descriptor by mutual information with
y.- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Descriptor matrix.y (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str],None]) – Target values.
- Returns:
The fitted selector.
- Return type:
- Raises:
ValueError – If both
kandpercentileare given, ortaskis invalid.
- class qsarkit.feature_selection.RFESelector(estimator=None, task='regression', n_features_to_select=None, step=1, random_state=None)[source]¶
Bases:
SelectorMixin,BaseEstimatorRecursive feature elimination, with a QSAR-sane default estimator.
Thin wrapper around
sklearn.feature_selection.RFE: repeatedly fitsestimator, ranks descriptors by its feature-importance (or coefficient) attribute, and prunes the weakest untiln_features_to_selectremain. When noestimatoris given, a random-forest of 200 trees is used — a robust, hyperparameter-light default that handles the non-linear structure typical of molecular descriptors.- Parameters:
estimator (
Optional[BaseEstimator]) – Estimator exposingcoef_orfeature_importances_after fitting. Defaults to aRandomForestRegressor/RandomForestClassifierwith 200 trees, chosen bytask.task (
Literal['regression','classification']) – Selects the default estimator whenestimatorisNone. Ignored ifestimatoris given.n_features_to_select (
Union[int,float,None]) – Number (or, if a float in (0, 1), fraction) of descriptors to keep. Defaults to sklearn’sRFEdefault (half the features).step (
Union[int,float]) – Number (or fraction) of descriptors pruned at each iteration.random_state (
Optional[int]) – Forwarded to the default random-forest estimator.
- Variables:
Examples
>>> import numpy as np >>> from qsarkit.feature_selection import RFESelector >>> rng = np.random.RandomState(0) >>> X = rng.normal(size=(100, 4)) >>> y = X[:, 0] * 3.0 >>> sel = RFESelector(n_features_to_select=1, random_state=0).fit(X, y) >>> int(sel.ranking_[0]) 1
References
Guyon, I., Weston, J., Barnhill, S. & Vapnik, V. (2002). “Gene Selection for Cancer Classification Using Support Vector Machines.” Machine Learning, 46(1-3), 389-422. https://doi.org/10.1023/A:1012487302797
- fit(X, y=None)[source]¶
Run recursive feature elimination.
- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Descriptor matrix.y (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str],None]) – Target values.
- Returns:
The fitted selector.
- Return type:
- class qsarkit.feature_selection.BorutaSelector(estimator=None, task='regression', n_iterations=100, alpha=0.05, include_tentative=False, random_state=None)[source]¶
Bases:
SelectorMixin,BaseEstimatorBoruta all-relevant feature selection.
Boruta answers a different question than most feature selectors: not “which subset gives the best predictive score” but “which descriptors carry any signal at all.” It does so by comparing every real descriptor against “shadow” copies of every descriptor — the same values, independently permuted across samples, which by construction carry no relationship to
y. A tree ensemble is fit on the concatenation of real and shadow descriptors; a real descriptor that beats the best shadow descriptor’s importance is a “hit” for that iteration. Repeated over many iterations, the hit count of a genuinely important descriptor should exceed that of an irrelevant one (which is statistically indistinguishable from a shadow feature) — formalized with a two-sided binomial test against the nullhits ~ Binomial(n_iterations, 0.5), Bonferroni-corrected across descriptors.- Parameters:
estimator (
Optional[BaseEstimator]) – Estimator exposingfeature_importances_after fitting (e.g. a tree ensemble). Defaults to aRandomForestRegressor/RandomForestClassifierwith 200 trees, chosen bytask.task (
Literal['regression','classification']) – Selects the default estimator whenestimatorisNone. Ignored ifestimatoris given.n_iterations (
int) – Number of shadow-permutation iterations.alpha (
float) – Family-wise significance level; Bonferroni-corrected toalpha / n_featuresper descriptor.include_tentative (
bool) – If True, descriptors that could not be statistically resolved within the iteration budget (“Tentative”) are also selected.random_state (
Optional[int]) – Seed for the shadow-feature permutations.
- Variables:
hits (
ndarrayofintofshape (n_features,)) – Number of iterations in which each real descriptor beat the best shadow descriptor.decision (
ndarrayofstrofshape (n_features,)) – Per-descriptor verdict:"Confirmed","Tentative"or"Rejected".support (
ndarrayofboolofshape (n_features,)) – True for"Confirmed"descriptors (plus"Tentative"ones too wheninclude_tentative=True).n_features_in (
int) – Number of descriptors seen duringfit.
Examples
>>> import numpy as np >>> from qsarkit.feature_selection import BorutaSelector >>> rng = np.random.RandomState(0) >>> X = rng.normal(size=(200, 3)) >>> y = X[:, 0] * 5.0 + rng.normal(scale=0.1, size=200) >>> sel = BorutaSelector(n_iterations=20, random_state=0).fit(X, y) >>> bool(sel.support_[0]) True
References
Kursa, M. B. & Rudnicki, W. R. (2010). “Feature Selection with the Boruta Package.” Journal of Statistical Software, 36(11), 1-13. https://doi.org/10.18637/jss.v036.i11
- fit(X, y=None)[source]¶
Run the Boruta shadow-permutation procedure.
- Parameters:
X (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str]]) – Descriptor matrix.y (
Union[Buffer,_SupportsArray[dtype[Any]],_NestedSequence[_SupportsArray[dtype[Any]]],complex,bytes,str,_NestedSequence[complex|bytes|str],None]) – Target values.
- Returns:
The fitted selector.
- Return type:
- Raises:
ValueError – If
taskis invalid, or ifestimatordoes not exposefeature_importances_after fitting.
References¶
Kursa, M. B. & Rudnicki, W. R. (2010). “Feature Selection with the Boruta Package.” J. Stat. Softw., 36(11), 1-13. doi:10.18637/jss.v036.i11
Guyon, I. et al. (2002). “Gene Selection for Cancer Classification using Support Vector Machines.” Mach. Learn., 46, 389-422. doi:10.1023/A:1012487302797
Kraskov, A., Stogbauer, H. & Grassberger, P. (2004). “Estimating Mutual Information.” Phys. Rev. E, 69, 066138. doi:10.1103/PhysRevE.69.066138
Ambroise, C. & McLachlan, G. J. (2002). “Selection Bias in Gene Extraction on the Basis of Microarray Gene-Expression Data.” PNAS, 99(10), 6562-6566. doi:10.1073/pnas.102102699