hscredit.core.selectors.rfe_selector 源代码

"""递归特征消除筛选器.

递归特征消除(Recursive Feature Elimination)通过递归方式逐步剔除
最不重要的特征,直到达到目标数量。适用于任何有 feature_importances_
或 coef_ 属性的模型。基于 sklearn.feature_selection.RFE 实现。

**参考样例**

>>> from hscredit.core.selectors import RFESelector
>>> from sklearn.ensemble import RandomForestClassifier
>>> import pandas as pd
>>> import numpy as np
>>> np.random.seed(42)
>>> X = pd.DataFrame(np.random.randn(200, 10), columns=[f'f{i}' for i in range(10)])  # 10个特征
>>> y = np.random.randint(0, 2, 200)  # 目标变量
>>> selector = RFESelector(
...     RandomForestClassifier(n_estimators=100, random_state=42),
...     n_features_to_select=5  # 递归消除至剩余5个特征
... )
>>> selector.fit(X, y)
>>> print(selector.selected_features_)
"""

from typing import Union, List, Optional, Dict, Any
import numpy as np
import pandas as pd
from .base import BaseFeatureSelector, get_feature_importances
from ._selection_history import initialize_history, record_event


[文档] class RFESelector(BaseFeatureSelector): """递归特征消除筛选器. 通过递归方式逐步剔除最不重要的特征。 适用于任何有feature_importances_或coef_属性的模型。 **参数** :param estimator: 评估器 :param n_features_to_select: 保留的特征数,默认为10 - 整数: 保留的特征数量 - 浮点数: 保留的特征比例 数量预算包含 include;强制保留字段始终参与候选模型拟合。 :param step: 每次剔除的特征数,默认为1 :param target: 目标变量列名,默认为'target' **参考样例** :: >>> from hscredit.core.selectors import RFESelector >>> from sklearn.ensemble import RandomForestClassifier >>> import pandas as pd >>> import numpy as np >>> np.random.seed(42) >>> X = pd.DataFrame(np.random.randn(200, 10), columns=[f'f{i}' for i in range(10)]) >>> y = np.random.randint(0, 2, 200) >>> selector = RFESelector( ... RandomForestClassifier(n_estimators=100, random_state=42), ... n_features_to_select=5 ... ) >>> selector.fit(X, y) >>> print(selector.selected_features_) **引用** 递归特征消除(RFE)出自 Guyon, I. et al. (2002). *Gene Selection for Cancer Classification using Support Vector Machines.* Machine Learning, 46. https://doi.org/10.1023/A:1012487302797 ;实现对齐 sklearn ``RFE`` https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.RFE.html """ method_name = "RFE筛选" def __init__( self, estimator, n_features_to_select: Union[int, float] = 10, step: int = 1, target: str = "target", include: Optional[List[str]] = None, exclude: Optional[List[str]] = None, force_drop: Optional[List[str]] = None, n_jobs: Optional[Union[int, float]] = -1, binner: Optional[Any] = None, binning_params: Optional[Dict[str, Any]] = None, parallel_backend: Optional[str] = None, parallel_config: Optional[Dict[str, Any]] = None, target_rm: bool = False, report_history: str = "summary", max_report_events: int = 10000, max_report_bytes: int = 8 * 1024 * 1024, ): super().__init__( target=target, threshold=n_features_to_select, include=include, exclude=exclude, force_drop=force_drop, n_jobs=n_jobs, binner=binner, binning_params=binning_params, parallel_backend=parallel_backend, parallel_config=parallel_config, target_rm=target_rm, ) self.estimator = estimator self.n_features_to_select = n_features_to_select self.step = step self.report_history = report_history self.max_report_events = max_report_events self.max_report_bytes = max_report_bytes def _included_features_participate_in_selection(self) -> bool: """固定保留变量参与每轮模型,数量包含在总保留预算内。""" return True def _fit_impl( self, X: pd.DataFrame, y: Optional[Union[pd.Series, np.ndarray]], ) -> None: """拟合RFE筛选器。 :param X: 输入特征DataFrame :param y: 目标变量 """ if y is None: if self.target not in X.columns: raise ValueError(f"需要传入y或X中包含{self.target}列") y = X[self.target].values X = X.drop(columns=self.target) self._get_feature_names(X) initialize_history(self) value = self.n_features_to_select if isinstance(value, (bool, np.bool_)): raise ValueError("n_features_to_select 不能是布尔值") if isinstance(value, (int, np.integer)) and value > 0: budget = min(int(value), X.shape[1]) elif isinstance(value, (float, np.floating)) and 0 < value <= 1: budget = max(1, int(X.shape[1] * value)) else: raise ValueError("n_features_to_select 必须为正整数或 (0, 1] 范围内的比例") forced = set(self.include_).intersection(X.columns) if len(forced) > budget: raise ValueError("include 特征数不能超过 n_features_to_select 总预算") if isinstance(self.step, (bool, np.bool_)): raise ValueError("step 不能是布尔值") if isinstance(self.step, (int, np.integer)) and self.step > 0: step = int(self.step) elif isinstance(self.step, (float, np.floating)) and 0 < self.step < 1: step = max(1, int(self.step * X.shape[1])) else: raise ValueError("step 必须为正整数或 (0, 1) 范围内的比例") active = X.columns.tolist() ranking = pd.Series(1, index=X.columns, dtype=int) fixed_budget = len(forced) == budget self.selection_reasons_ = pd.Series(index=X.columns, dtype=object) if fixed_budget: active = [feature for feature in X.columns if feature in forced] ranking = ranking.astype(float) ranking.loc[~ranking.index.isin(active)] = np.nan self.selection_input_features_ = list(active) for feature in X.columns: if feature not in forced: self.selection_reasons_[feature] = "总数量预算已由强制保留字段占满,未参与递归筛选" record_event( self, { "轮次": 0, "特征": feature, "动作": "预算剔除", "指标名称": None, "指标值": np.nan, "有效阈值": None, "是否有效": False, "原因": self.selection_reasons_[feature], "补充信息": {"总保留数量预算": budget}, }, ) self.elimination_round_ = pd.Series(0, index=X.columns, dtype=int) self.decision_importances_ = pd.Series(np.nan, index=X.columns, dtype=float) iteration = 0 while True: iteration += 1 model = self._clone_estimator_for_parallel(self.estimator) with self._estimator_parallel_context(): model.fit(X[active], y) importances = np.asarray(get_feature_importances(model), dtype=float).reshape(-1) if importances.size != len(active) or not np.isfinite(importances).all(): raise ValueError("RFE 模型必须返回与当前特征数相等的有限重要性") current = pd.Series(importances, index=active) final = len(active) <= budget removable = [name for name in active if name not in forced] # 与 sklearn RFE 一样按重要性平方排序;保留稳定的输入顺序平局规则。 order = np.argsort(np.square(current.reindex(removable).to_numpy()), kind="stable") count = min(step, len(active) - budget) removed = [] if final else [removable[index] for index in order[:count]] for feature in active: decided = final or feature in removed record_event( self, { "轮次": iteration, "特征": feature, "动作": "保留" if final else ("剔除" if feature in removed else "候选评估"), "指标名称": "模型特征重要性", "指标值": float(current[feature]), "有效阈值": None, "是否有效": True, "原因": "强制保留" if feature in forced else "递归重要性排名与总数量预算", "补充信息": {"总保留数量预算": budget}, }, diagnostic=not decided, ) if decided: self.decision_importances_[feature] = current[feature] if final: self.estimator_ = model break self.elimination_round_.loc[removed] = iteration active = [feature for feature in active if feature not in removed] ranking.loc[~ranking.index.isin(active)] += 1 self.selected_features_ = active self.ranking_ = ranking self.scores_ = ranking.copy() self.score_name_ = "RFE排名" self.score_direction_ = "越小越好" self.effective_threshold_ = budget self.selection_stopping_reason_ = "达到包含强制保留字段的总数量预算" self.n_iterations_ = iteration self.importance_history_ = pd.DataFrame(self.selection_events_) self._drop_reason = ( "总数量预算已由强制保留字段占满,未参与递归筛选" if fixed_budget else "递归消除后未进入总保留数量预算" )