hscredit.core.selectors.scorecard_feature_selection 源代码

"""评分卡风格特征粗筛器.

参考 scorecardpipeline.FeatureSelection 的常用粗筛流程,
按缺失率、IV、相关性、单一值占比顺序筛选特征,
但完整复用 hscredit 现有筛选器体系与双风格 API。

**参考样例**

>>> from hscredit.core.selectors import ScorecardFeatureSelection
>>> import pandas as pd
>>> import numpy as np
>>> np.random.seed(42)
>>> X = pd.DataFrame(np.random.randn(1000, 10), columns=[f'f{i}' for i in range(10)])  # 10个特征
>>> y = pd.Series(np.random.randint(0, 2, 1000))  # 目标变量
>>> selector = ScorecardFeatureSelection(
...     null_threshold=0.95,  # 移除缺失率>95%的特征
...     iv_threshold=0.02,    # 移除IV<0.02的特征
...     corr_threshold=0.7,   # 移除相关性>0.7的特征
...     mode_threshold=0.95,  # 移除众数占比>95%的特征
... )
>>> 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
from .null_selector import NullSelector
from .iv_selector import IVSelector
from .corr_selector import CorrSelector
from .mode_selector import ModeSelector


[文档] class ScorecardFeatureSelection(BaseFeatureSelector): """评分卡风格特征粗筛器. 按如下顺序执行特征筛选: 1. 缺失率筛选(NullSelector) 2. IV值筛选(IVSelector) 3. 相关性筛选(CorrSelector) 4. 单一值筛选(ModeSelector) 与 scorecardpipeline.FeatureSelection 的思路一致, 但参数命名和基础行为遵循 hscredit 风格: - 缺失率阈值使用 ``null_threshold`` - IV阈值使用 ``iv_threshold`` - 相关性阈值使用 ``corr_threshold`` - 单一值占比阈值使用 ``mode_threshold`` - 强制保留使用 ``include`` - 强制剔除使用 ``exclude`` / ``force_drop`` - 支持 sklearn 风格 ``fit(X, y)`` - 支持 scorecardpipeline 风格 ``fit(df)`` **参数** :param null_threshold: 缺失率阈值,默认为0.95;设为None可关闭该阶段 :param iv_threshold: IV阈值,默认为0.02;设为None可关闭该阶段 :param corr_threshold: 相关性阈值,默认为0.7;设为None可关闭该阶段 :param mode_threshold: 单一值占比阈值,默认为0.95;设为None可关闭该阶段 :param corr_method: 相关系数计算方法,默认为'pearson' - 'pearson': 皮尔逊相关系数 - 'spearman': 斯皮尔曼等级相关系数 - 'kendall': 肯德尔相关系数 :param corr_metric: 相关性筛选的保留指标,默认为'iv' - 'iv': 信息值(需要目标变量y) - 'ks': KS统计量 - 'lift': LIFT值 - 'bad_rate': 坏样本率 指标通过分箱后的bin_tables_计算得到。 :param corr_weights: 自定义相关性筛选权重,优先级高于corr_metric :param corr_binning_params: 透传给CorrSelector的分箱参数 :param iv_regularization: IV计算正则项,默认为1.0 :param mode_dropna: 计算单一值占比时是否排除缺失值,默认为True :param target: 目标变量列名,默认为'target' :param include: 强制保留特征列表 :param exclude: 强制剔除特征列表 :param force_drop: 强制剔除特征列表,最终会合并到exclude :param target_rm: transform时是否移除目标列,默认为False :param n_jobs: 并行任务数 **参考样例** >>> from hscredit.core.selectors import ScorecardFeatureSelection >>> import pandas as pd >>> import numpy as np >>> np.random.seed(42) >>> X = pd.DataFrame(np.random.randn(1000, 10), columns=[f'f{i}' for i in range(10)]) >>> y = pd.Series(np.random.randint(0, 2, 1000)) >>> selector = ScorecardFeatureSelection( ... null_threshold=0.95, ... iv_threshold=0.02, ... corr_threshold=0.7, ... mode_threshold=0.95, ... ) >>> selector.fit(X, y) >>> print(selector.selected_features_) >>> print(selector.stage_report_df_) """ method_name = '评分卡特征粗筛' def __init__( self, null_threshold: Optional[float] = 0.95, iv_threshold: Optional[float] = 0.02, corr_threshold: Optional[float] = 0.7, mode_threshold: Optional[float] = 0.95, corr_method: str = 'pearson', corr_metric: str = 'iv', corr_weights: Optional[Union[pd.Series, Dict[str, float], List[float]]] = None, corr_binning_params: Optional[Dict[str, Any]] = None, iv_regularization: float = 1.0, mode_dropna: bool = True, target: str = 'target', include: Optional[List[str]] = None, exclude: Optional[List[str]] = None, force_drop: Optional[List[str]] = None, target_rm: bool = False, 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, ): super().__init__( target=target, include=include, exclude=exclude, force_drop=force_drop, threshold='multi-stage', n_jobs=n_jobs, binner=binner, binning_params=binning_params, parallel_backend=parallel_backend, parallel_config=parallel_config, ) self.null_threshold = null_threshold self.iv_threshold = iv_threshold self.corr_threshold = corr_threshold self.mode_threshold = mode_threshold self.corr_method = corr_method self.corr_metric = corr_metric self.corr_weights = corr_weights self.corr_binning_params = corr_binning_params self.iv_regularization = iv_regularization self.mode_dropna = mode_dropna self.target_rm = target_rm
[文档] def fit( self, X: Union[pd.DataFrame, np.ndarray], y: Optional[Union[pd.Series, np.ndarray]] = None, ) -> 'ScorecardFeatureSelection': """拟合评分卡风格筛选器并同步兼容属性。""" return super().fit(X, y)
def _finalize_fit(self) -> None: """在隔离候选对象上整理最终选择结果。""" self._finalize_selection_result()
[文档] def transform( self, X: Union[pd.DataFrame, np.ndarray, List[str]], ) -> Union[pd.DataFrame, np.ndarray, List[str]]: """根据筛选结果转换数据。""" transformed = super().transform(X) if self.target_rm and isinstance(transformed, pd.DataFrame) and self.target in transformed.columns: return transformed.drop(columns=[self.target]) return transformed
def _fit_impl( self, X: pd.DataFrame, y: Optional[Union[pd.Series, np.ndarray]], ) -> None: """按评分卡粗筛顺序执行筛选。""" self._validate_configuration(y) self._get_feature_names(X) # 所有阶段只读输入;首阶段不复制完整高维 DataFrame。 current_X = X self.stage_selectors_ = {} self.stage_reports_ = [] self.stage_report_df_ = pd.DataFrame() self.corr_iv_scores_ = None all_dropped = [] if self._is_stage_enabled(self.null_threshold) and len(current_X.columns) > 0: current_X = self._run_stage( stage_key='empty', stage_name='缺失率筛选', selector=NullSelector( threshold=self.null_threshold, target=self.target, n_jobs=self.n_jobs, parallel_backend=self.parallel_backend, parallel_config=self.parallel_config, ), current_X=current_X, y=y, all_dropped=all_dropped, ) iv_scores = None if self._is_stage_enabled(self.iv_threshold) and len(current_X.columns) > 0: iv_selector = IVSelector( threshold=self.iv_threshold, target=self.target, regularization=self.iv_regularization, n_jobs=self.n_jobs, parallel_backend=self.parallel_backend, parallel_config=self.parallel_config, ) current_X = self._run_stage( stage_key='iv', stage_name='IV值筛选', selector=iv_selector, current_X=current_X, y=y, all_dropped=all_dropped, ) iv_scores = getattr(iv_selector, 'scores_', None) if self._is_stage_enabled(self.corr_threshold) and len(current_X.columns) > 0: corr_weights = self._resolve_corr_weights(current_X, y, iv_scores) corr_binning_kwargs = {} if self.corr_binning_params is not None: corr_binning_kwargs['binning_params'] = self.corr_binning_params elif ( getattr(self, '_binner_instance', None) is not None or corr_weights is not None ): # 外层已分箱或已有明确权重时,关闭内部 CorrSelector 的构造默认分箱。 corr_binning_kwargs['binning_params'] = None corr_selector = CorrSelector( threshold=self.corr_threshold, method=self.corr_method, metric=self.corr_metric, weights=corr_weights, target=self.target, n_jobs=self.n_jobs, parallel_backend=self.parallel_backend, parallel_config=self.parallel_config, **corr_binning_kwargs, ) current_X = self._run_stage( stage_key='corr', stage_name='相关性筛选', selector=corr_selector, current_X=current_X, y=y, all_dropped=all_dropped, ) if self._is_stage_enabled(self.mode_threshold) and len(current_X.columns) > 0: current_X = self._run_stage( stage_key='identical', stage_name='单一值筛选', selector=ModeSelector( threshold=self.mode_threshold, dropna=self.mode_dropna, target=self.target, n_jobs=self.n_jobs, parallel_backend=self.parallel_backend, parallel_config=self.parallel_config, ), current_X=current_X, y=y, all_dropped=all_dropped, ) self.selected_features_ = current_X.columns.tolist() self.scores_ = None if len(all_dropped) > 0: self.dropped_ = pd.concat(all_dropped, ignore_index=True) else: self.dropped_ = pd.DataFrame( columns=['特征', '剔除原因', '筛选阶段', '筛选阶段名称', '筛选器'] ) self.removed_features_ = self.dropped_['特征'].tolist() if len(self.dropped_) > 0 else [] if self.stage_reports_: self.stage_report_df_ = pd.DataFrame(self.stage_reports_) def _run_stage( self, stage_key: str, stage_name: str, selector: BaseFeatureSelector, current_X: pd.DataFrame, y: Optional[Union[pd.Series, np.ndarray]], all_dropped: List[pd.DataFrame], ) -> pd.DataFrame: """执行单个筛选阶段并记录结果。""" input_count = current_X.shape[1] selector.fit(current_X, y) self.stage_selectors_[stage_key] = selector self.stage_reports_.append({ '阶段键': stage_key, '阶段名称': stage_name, '筛选器': selector.__class__.__name__, '输入特征数': input_count, '选中特征数': len(selector.selected_features_), '剔除特征数': len(getattr(selector, 'removed_features_', [])), '阈值': getattr(selector, 'threshold', None), }) if hasattr(selector, 'dropped_') and selector.dropped_ is not None and len(selector.dropped_) > 0: dropped = selector.dropped_.copy(deep=False) dropped['筛选阶段'] = stage_key dropped['筛选阶段名称'] = stage_name dropped['筛选器'] = selector.__class__.__name__ all_dropped.append(dropped) selected = selector.selected_features_ if len(selected) == 0: return current_X.iloc[:, 0:0] if selected == list(current_X.columns): return current_X return current_X.loc[:, selected] def _resolve_corr_weights( self, X: pd.DataFrame, y: Optional[Union[pd.Series, np.ndarray]], iv_scores: Optional[pd.Series], ) -> Optional[Union[pd.Series, Dict[str, float], List[float]]]: """为相关性筛选解析保留权重。""" if self.corr_weights is not None: return self.corr_weights if iv_scores is not None and str(self.corr_metric).lower() == 'iv': return iv_scores.reindex(X.columns).fillna(0.0) if str(self.corr_metric).lower() == 'iv': if y is None: raise ValueError('启用 corr_threshold 且使用 IV 作为保留指标时,需要传入 y 或 target 列') iv_selector = IVSelector( threshold=float('-inf'), target=self.target, regularization=self.iv_regularization, n_jobs=self.n_jobs, parallel_backend=self.parallel_backend, parallel_config=self.parallel_config, ) iv_selector.fit(X, y) self.corr_iv_scores_ = iv_selector.scores_.reindex(X.columns).fillna(0.0) return self.corr_iv_scores_ return None def _validate_configuration( self, y: Optional[Union[pd.Series, np.ndarray]], ) -> None: """校验配置合法性。""" self._validate_ratio_threshold(self.null_threshold, 'null_threshold') self._validate_ratio_threshold(self.corr_threshold, 'corr_threshold') self._validate_ratio_threshold(self.mode_threshold, 'mode_threshold') if self._is_stage_enabled(self.iv_threshold) and self.iv_threshold < 0: raise ValueError('iv_threshold 不能小于 0') requires_target = self._is_stage_enabled(self.iv_threshold) requires_target = requires_target or ( self._is_stage_enabled(self.corr_threshold) and self.corr_weights is None and str(self.corr_metric).lower() == 'iv' ) requires_target = requires_target or ( self._is_stage_enabled(self.corr_threshold) and self.corr_weights is None and str(self.corr_metric).lower() != 'iv' ) if requires_target and y is None: raise ValueError('当前筛选配置需要目标变量,请使用 fit(X, y) 或 fit(df) 且 df 包含 target 列') @staticmethod def _is_stage_enabled(threshold: Optional[float]) -> bool: """判断筛选阶段是否启用。""" return threshold is not None and threshold is not False @staticmethod def _validate_ratio_threshold(threshold: Optional[float], name: str) -> None: """校验 0-1 比例阈值。""" if threshold is None or threshold is False: return if not 0 <= threshold <= 1: raise ValueError(f'{name} 必须在 [0, 1] 范围内') def _finalize_selection_result(self) -> None: """整理最终选择结果并补齐兼容属性。""" selected_set = set(self.selected_features_) self.selected_features_ = [c for c in self._feature_names if c in selected_set] self.n_features_ = len(self.selected_features_) if hasattr(self, 'dropped_') and self.dropped_ is not None and len(self.dropped_) > 0: dropped_df = self.dropped_.copy() if '筛选阶段' not in dropped_df.columns: dropped_df['筛选阶段'] = None if '筛选阶段名称' not in dropped_df.columns: dropped_df['筛选阶段名称'] = None if '筛选器' not in dropped_df.columns: dropped_df['筛选器'] = None force_drop_mask = dropped_df['剔除原因'].astype(str).str.contains('强制剔除', na=False) dropped_df.loc[force_drop_mask, '筛选阶段'] = 'force_drop' dropped_df.loc[force_drop_mask, '筛选阶段名称'] = '强制剔除' dropped_df = dropped_df.loc[~dropped_df['特征'].isin(self.selected_features_)].copy() dropped_df = dropped_df.drop_duplicates(subset=['特征'], keep='first').reset_index(drop=True) self.dropped_ = dropped_df self.removed_features_ = dropped_df['特征'].tolist() else: self.dropped_ = pd.DataFrame( columns=['特征', '剔除原因', '筛选阶段', '筛选阶段名称', '筛选器'] ) self.removed_features_ = [] self.select_columns = list(self.selected_features_) if not self.target_rm and self.target not in self.select_columns: self.select_columns.append(self.target) self.dropped = pd.DataFrame({ 'variable': self.dropped_['特征'] if len(self.dropped_) > 0 else pd.Series(dtype=object), 'rm_reason': self.dropped_['筛选阶段'] if len(self.dropped_) > 0 else pd.Series(dtype=object), })
[文档] def get_selection_report(self) -> Dict[str, Any]: """获取包含阶段明细的筛选报告。""" report = super().get_selection_report() if report.get('状态') == '未拟合': return report report['阈值'] = { 'null_threshold': self.null_threshold, 'iv_threshold': self.iv_threshold, 'corr_threshold': self.corr_threshold, 'mode_threshold': self.mode_threshold, } if hasattr(self, 'stage_reports_'): report['阶段明细'] = self.stage_reports_ return report