Source code for qsarkit.feature_selection._rfe

"""Recursive feature elimination."""

from __future__ import annotations

from typing import Literal, Optional, Union

import numpy as np
import numpy.typing as npt
from sklearn.base import BaseEstimator
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.feature_selection import RFE, SelectorMixin
from sklearn.utils.validation import check_is_fitted

__all__ = ["RFESelector"]


[docs] class RFESelector(SelectorMixin, BaseEstimator): """Recursive feature elimination, with a QSAR-sane default estimator. Thin wrapper around ``sklearn.feature_selection.RFE``: repeatedly fits ``estimator``, ranks descriptors by its feature-importance (or coefficient) attribute, and prunes the weakest until ``n_features_to_select`` remain. When no ``estimator`` is 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 : BaseEstimator, optional Estimator exposing ``coef_`` or ``feature_importances_`` after fitting. Defaults to a ``RandomForestRegressor``/ ``RandomForestClassifier`` with 200 trees, chosen by ``task``. task : {"regression", "classification"}, default "regression" Selects the default estimator when ``estimator`` is ``None``. Ignored if ``estimator`` is given. n_features_to_select : int or float, optional Number (or, if a float in (0, 1), fraction) of descriptors to keep. Defaults to sklearn's ``RFE`` default (half the features). step : int or float, default 1 Number (or fraction) of descriptors pruned at each iteration. random_state : int, optional Forwarded to the default random-forest estimator. Attributes ---------- support_ : ndarray of bool of shape (n_features,) True for descriptors kept. ranking_ : ndarray of int of shape (n_features,) Selection ranking; selected descriptors are ranked 1. n_features_in_ : int Number of descriptors seen during ``fit``. 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 """ support_: npt.NDArray[np.bool_] ranking_: npt.NDArray[np.intp] n_features_in_: int def __init__( self, estimator: Optional[BaseEstimator] = None, task: Literal["regression", "classification"] = "regression", n_features_to_select: Optional[Union[int, float]] = None, step: Union[int, float] = 1, random_state: Optional[int] = None, ) -> None: self.estimator = estimator self.task = task self.n_features_to_select = n_features_to_select self.step = step self.random_state = random_state def _default_estimator(self) -> BaseEstimator: if self.task == "classification": return RandomForestClassifier(n_estimators=200, random_state=self.random_state) return RandomForestRegressor(n_estimators=200, random_state=self.random_state)
[docs] def fit(self, X: npt.ArrayLike, y: Optional[npt.ArrayLike] = None) -> "RFESelector": """Run recursive feature elimination. Parameters ---------- X : array-like of shape (n_samples, n_features) Descriptor matrix. y : array-like of shape (n_samples,) Target values. Returns ------- RFESelector 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}.") if self.task not in ("regression", "classification"): raise ValueError( f"task must be 'regression' or 'classification', got {self.task!r}." ) self.n_features_in_ = arr.shape[1] estimator = self.estimator if self.estimator is not None else self._default_estimator() rfe = RFE( estimator=estimator, n_features_to_select=self.n_features_to_select, step=self.step, ) rfe.fit(arr, y) self.support_ = np.asarray(rfe.support_, dtype=np.bool_) self.ranking_ = np.asarray(rfe.ranking_, dtype=np.intp) return self
def _get_support_mask(self) -> npt.NDArray[np.bool_]: check_is_fitted(self) return self.support_