hscredit.core.selectors.iv_selector 源代码

"""IV值筛选器.

使用信息价值(IV)进行特征筛选,是金融风控场景的核心筛选方法。

**参考样例**

>>> from hscredit.core.selectors import IVSelector
>>> import pandas as pd
>>> import numpy as np
>>> np.random.seed(42)
>>> X = pd.DataFrame(np.random.randn(1000, 5), columns=[f'f{i}' for i in range(5)])  # 5个特征
>>> y = pd.Series(np.random.randint(0, 2, 1000))  # 目标变量(0=好,1=坏)
>>> selector = IVSelector(threshold=0.02)  # 筛选IV>0.02的特征
>>> 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 ...utils.parallel import ParallelWorkload


def _compute_iv_single(x: np.ndarray, y: np.ndarray, regularization: float = 1.0) -> float:
    """计算单个特征的IV值。

    :param x: 特征值数组
    :param y: 目标变量数组
    :param regularization: 正则化参数,避免除零
    :return: IV值
    """
    if regularization <= 0:
        raise ValueError("regularization 必须大于 0")

    x = np.asarray(x)
    y = np.asarray(y)
    if x.shape[0] != y.shape[0]:
        raise ValueError("特征与目标变量长度不一致")

    # 处理缺失值 - 兼容category和object类型
    # 先转换为object类型,然后用isnull判断
    if isinstance(x, pd.Series):
        has_missing = x.isnull().values
    else:
        # 如果是numpy数组,尝试转换为Series以使用isnull
        try:
            has_missing = pd.Series(x).isnull().values
        except Exception:
            # 如果转换失败,使用pd.isnull直接判断
            has_missing = pd.isnull(x)

    valid = ~has_missing
    x_valid = x[valid]
    y_valid = y[valid]

    if len(x_valid) == 0:
        return 0.0

    # 获取唯一值
    uniques = np.unique(x_valid)
    n_cats = len(uniques)

    if n_cats <= 1:
        return 0.0

    labels = set(np.unique(y_valid).tolist())
    if not labels.issubset({0, 1}):
        raise ValueError("IV 计算要求目标变量只包含 0 和 1")
    if labels != {0, 1}:
        return 0.0

    # 统计好坏样本
    event_mask = y_valid == 1
    nonevent_mask = y_valid == 0

    event_tot = np.count_nonzero(event_mask) + n_cats * regularization
    nonevent_tot = np.count_nonzero(nonevent_mask) + n_cats * regularization

    event_rates = np.zeros(n_cats, dtype=np.float64)
    nonevent_rates = np.zeros(n_cats, dtype=np.float64)

    for i, cat in enumerate(uniques):
        mask = x_valid == cat
        event_rates[i] = np.count_nonzero(mask & event_mask) + regularization
        nonevent_rates[i] = np.count_nonzero(mask & nonevent_mask) + regularization

    event_rates /= event_tot
    nonevent_rates /= nonevent_tot

    # 计算IV
    ivs = (event_rates - nonevent_rates) * np.log(np.maximum(event_rates, 1e-10) / np.maximum(nonevent_rates, 1e-10))
    return np.sum(ivs).item()


def _compute_iv_feature(task):
    """编码并计算单个特征 IV。"""
    feature, series, y, regularization = task
    if series.dtype.name in ["object", "category"]:
        values = pd.factorize(series)[0].astype(float)
        values[pd.isna(series).to_numpy()] = np.nan
    else:
        values = series.values
    return feature, _compute_iv_single(values, y, regularization)


[文档] class IVSelector(BaseFeatureSelector): """IV值筛选器. 使用信息价值(Information Value)筛选特征。 IV是金融风控中衡量特征预测能力的核心指标。 IV值解释: - < 0.02: 无预测能力 - 0.02 - 0.1: 弱预测能力 - 0.1 - 0.3: 中等预测能力 - 0.3 - 0.5: 强预测能力 - > 0.5: 极强预测能力(可能过拟合) **支持的数据类型:** - 数值型特征(int, float) - 类别型特征(object, category) **参数** :param threshold: IV阈值,默认为0.02 - 0.02: 仅保留IV值大于0.02的特征 :param target: 目标变量列名,默认为'target' :param regularization: 正则化参数,默认为1.0 :param n_jobs: 并行计算的任务数 **参考样例** :: >>> from hscredit.core.selectors import IVSelector >>> import pandas as pd >>> import numpy as np >>> np.random.seed(42) >>> X = pd.DataFrame(np.random.randn(1000, 5), columns=[f'f{i}' for i in range(5)]) >>> y = pd.Series(np.random.randint(0, 2, 1000)) >>> selector = IVSelector(threshold=0.02) >>> selector.fit(X, y) >>> print(selector.selected_features_) >>> print(selector.scores_) # 查看IV值 >>> # 仅传分箱参数:内部创建并训练 OptimalBinning >>> selector = IVSelector( ... threshold=0.02, ... binning_params={'method': 'best_iv', 'max_n_bins': 5}, ... ) >>> selector.fit(X, y) >>> # 传入配置好的未训练实例:筛选器自动训练 >>> from hscredit.core.binning import OptimalBinning >>> binner = OptimalBinning(method='best_iv', max_n_bins=5) >>> selector = IVSelector(threshold=0.02, binner=binner).fit(X, y) >>> # 传入已训练实例:直接复用规则,不重新训练 >>> trained_binner = OptimalBinning(method='best_iv').fit(X, y) >>> selector = IVSelector( ... threshold=0.02, ... binner=trained_binner, ... binning_params={'method': 'uniform'}, # binner 优先,本参数被忽略 ... ).fit(X, y) **参数** 除继承自 :class:`~hscredit.core.selectors.base.BaseFeatureSelector` 的通用参数 (``target`` / ``include`` / ``exclude`` / ``force_drop`` / ``n_jobs``)外: :param threshold: IV 保留阈值,``IV >= threshold`` 的特征被保留,默认为 ``0.02`` :param regularization: 计算 WOE/IV 时的加性平滑系数,避免某类别好/坏样本数为 0 导致取对数发散,默认为 ``1.0`` :param binner: 可选的已配置分箱器实例。未训练实例会自动训练,已训练实例直接复用 :param binning_params: 可选的 ``OptimalBinning`` 构造参数字典。未传 ``binner`` 时, 内部创建分箱器并将原始数据转换为分箱 index 后计算 IV .. note:: 本筛选器按特征的**唯一取值**直接计算 IV(类别型先 ``factorize``),适合已分箱 或基数较低的特征;连续特征建议先用 :class:`~hscredit.core.binning.OptimalBinning` 分箱后再筛选,或传入 ``binner`` 参数。 **引用** Information Value 用于变量筛选见 Siddiqi, N. (2006). *Credit Risk Scorecards.* Wiley;阈值经验区间(0.02/0.1/0.3/0.5)为业界通行标准。 """ method_name = "IV值筛选" def __init__( self, threshold: float = 0.02, target: str = "target", regularization: float = 1.0, 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, ): super().__init__( target=target, threshold=threshold, 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, ) self.regularization = regularization def _fit_impl( self, X: pd.DataFrame, y: Optional[Union[pd.Series, np.ndarray]], ) -> None: """拟合IV值筛选器。 :param X: 输入特征DataFrame :param y: 目标变量 """ self._get_feature_names(X) if y is None: raise ValueError("IVSelector 需要目标变量 y") y = np.asarray(y) results = self._parallel_execute( _compute_iv_feature, ((col, X[col], y, self.regularization) for col in X.columns), task_labels=X.columns, default_backend="threading", workload=ParallelWorkload( task_count=X.shape[1], rows=X.shape[0], columns=X.shape[1], data_bytes=int(X.memory_usage(deep=True).sum()), cost_per_item=5.0, capability="thread_safe", releases_gil=True, operation="IV字段计算", ), ) iv_values = np.array([score for _, score in results]) self.scores_ = pd.Series(iv_values, index=X.columns) # 选择IV值大于等于阈值的特征 selected_mask = iv_values >= self.threshold self.selected_features_ = X.columns[selected_mask].tolist() # 构建详细的dropped_记录,包含IV值 dropped_cols = X.columns[~selected_mask].tolist() if len(dropped_cols) > 0: self.dropped_ = pd.DataFrame( { "特征": dropped_cols, "剔除原因": [f"IV值({self.scores_[col]:.4f}) <= 阈值({self.threshold})" for col in dropped_cols], "IV值": [self.scores_[col] for col in dropped_cols], "阈值": [self.threshold] * len(dropped_cols), } ) else: self.dropped_ = pd.DataFrame(columns=["特征", "剔除原因", "IV值", "阈值"])
[文档] def get_iv_interpretation(self) -> pd.DataFrame: """获取IV值的中文解释。 :return: 包含IV值及解释的DataFrame """ if not hasattr(self, "scores_"): return pd.DataFrame() def interpret_iv(iv): if iv < 0.02: return "无预测能力" elif iv < 0.1: return "弱预测能力" elif iv < 0.3: return "中等预测能力" elif iv < 0.5: return "强预测能力" else: return "极强预测能力(可能过拟合)" df = pd.DataFrame({"特征": self.scores_.index, "IV值": self.scores_.values, "预测能力": [interpret_iv(iv) for iv in self.scores_.values]}) return df.sort_values("IV值", ascending=False)