Source code for qsarkit.feature_selection._variance

"""Near-zero-variance descriptor filtering."""

from __future__ import annotations

from typing import Optional

import numpy as np
import numpy.typing as npt
from sklearn.base import BaseEstimator
from sklearn.feature_selection import SelectorMixin
from sklearn.utils.validation import check_is_fitted

__all__ = ["VarianceFilter"]


[docs] class VarianceFilter(SelectorMixin, BaseEstimator): """Drop 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 what ``sklearn.feature_selection.VarianceThreshold`` implements, reproduced directly here so this estimator shares the ``qsarkit`` selector contract (``support_``, ``get_support()``, ``transform()`` via :class:`sklearn.feature_selection.SelectorMixin`). Parameters ---------- threshold : float, default 0.0 Descriptors with a training-set variance at or below this value are dropped. The default removes only exactly-constant columns. Attributes ---------- variances_ : ndarray of shape (n_features,) Per-descriptor variance computed on the training data. support_ : ndarray of bool of shape (n_features,) True for descriptors kept (``variances_ > threshold``). n_features_in_ : int Number of descriptors seen during ``fit``. 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 """ variances_: npt.NDArray[np.float64] support_: npt.NDArray[np.bool_] n_features_in_: int def __init__(self, threshold: float = 0.0) -> None: self.threshold = threshold
[docs] def fit( self, X: npt.ArrayLike, y: Optional[npt.ArrayLike] = None ) -> "VarianceFilter": """Learn per-descriptor variances and the resulting support mask. Parameters ---------- X : array-like of shape (n_samples, n_features) Descriptor matrix. y : array-like, optional Ignored. Present for API consistency. Returns ------- VarianceFilter The fitted selector. """ arr = np.asarray(X, dtype=np.float64) if arr.ndim != 2: raise ValueError(f"X must be 2-dimensional, got shape {arr.shape}.") self.n_features_in_ = arr.shape[1] self.variances_ = np.asarray(arr.var(axis=0), dtype=np.float64) self.support_ = np.asarray(self.variances_ > self.threshold, dtype=np.bool_) return self
def _get_support_mask(self) -> npt.NDArray[np.bool_]: check_is_fitted(self) return self.support_