hscredit.core.selectors.lift_selector 源代码

"""LIFT筛选器.

使用LIFT@ratio值进行特征筛选,支持自定义覆盖率和方向。

**参考样例**

>>> from hscredit.core.selectors import LiftSelector
>>> 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))  # 目标变量
>>> selector = LiftSelector(threshold=0.5, ratio=0.10)  # 筛选LIFT>0.5且覆盖率10%的特征
>>> selector.fit(X, y)
>>> print(selector.selected_features_)
"""

from typing import Union, List, Optional, Literal, Tuple, Dict, Any
import numpy as np
import pandas as pd

from .base import BaseFeatureSelector
from ...utils.parallel import ParallelWorkload


def _compute_lift_single(
    x: np.ndarray,
    y: np.ndarray,
    ratio: float = 0.10,
    ascending: bool = False,
) -> float:
    """计算单个特征在指定排序方向下的LIFT@ratio值.

    将样本按特征值排序后,取头部 ratio 比例的样本,
    计算该子群的坏样本率与整体坏样本率的比值。

    :param x: 特征值数组
    :param y: 目标变量数组
    :param ratio: 覆盖率,默认0.10(LIFT@10%)
    :param ascending: 排序方向,默认False(降序,取最大值头部)
    :return: LIFT值
    """
    n = len(x)
    if n == 0:
        return 1.0

    # 特征无变异,无区分能力
    if len(np.unique(x)) <= 1:
        return 1.0

    base_bad_rate = np.mean(y)
    if base_bad_rate == 0 or base_bad_rate == 1:
        return 1.0

    # 头部样本数量(至少1个)
    k = max(1, int(np.ceil(n * ratio)))

    # 按特征值排序
    if ascending:
        order = np.argsort(x, kind="stable")  # 升序:最小值在前
    else:
        order = np.argsort(x, kind="stable")[::-1]  # 降序:最大值在前

    # 取头部 k 个样本
    top_idx = order[:k]
    top_bad_rate = np.mean(y[top_idx])

    lift = top_bad_rate / base_bad_rate
    return float(lift)


def _compute_lift_with_direction(
    x: np.ndarray,
    y: np.ndarray,
    ratio: float = 0.10,
    direction: str = "auto",
) -> Tuple[float, float, float, str]:
    """计算单个特征的LIFT得分(支持方向判断).

    :param x: 特征值数组
    :param y: 目标变量数组
    :param ratio: 覆盖率
    :param direction: 方向模式
        - 'auto': 同时计算两个方向,只比较各自目标方向上的改善
        - 'bad': 仅计算找坏人的LIFT(降序取头部,LIFT越高越好)
        - 'good': 仅计算找好人的LIFT(升序取头部,LIFT越低越好)
    :return: (score, lift_bad, lift_good, best_direction)
        - score: 指定方向相对 LIFT=1 的有效改善,越大区分力越强
        - lift_bad: 降序LIFT值(找坏人方向)
        - lift_good: 升序LIFT值(找好人方向)
        - best_direction: 最优方向 'bad' 或 'good'
    """
    if direction == "bad":
        lift_bad = _compute_lift_single(x, y, ratio, ascending=False)
        score = max(lift_bad - 1.0, 0.0)
        return score, lift_bad, np.nan, "bad"

    if direction == "good":
        lift_good = _compute_lift_single(x, y, ratio, ascending=True)
        score = max(1.0 - lift_good, 0.0)
        return score, np.nan, lift_good, "good"

    # auto: 同时计算两个方向,只奖励方向正确的改善。
    lift_bad = _compute_lift_single(x, y, ratio, ascending=False)
    lift_good = _compute_lift_single(x, y, ratio, ascending=True)

    dist_bad = max(lift_bad - 1.0, 0.0)
    dist_good = max(1.0 - lift_good, 0.0)

    if dist_bad >= dist_good:
        return dist_bad, lift_bad, lift_good, "bad"
    else:
        return dist_good, lift_bad, lift_good, "good"


def _compute_lift_feature(task):
    """计算单个特征的 LIFT 详情。"""
    feature, values, y, ratio, direction = task
    return (feature,) + _compute_lift_with_direction(values, y, ratio, direction)


[文档] class LiftSelector(BaseFeatureSelector): """LIFT筛选器. 使用LIFT@ratio值筛选特征,支持找坏人、找好人、自动三种方向模式。 LIFT衡量特征在头部覆盖率下对目标群体的提升程度。 **LIFT@ratio% 计算方式** 1. 将样本按特征值排序 2. 取头部 ratio 比例的样本 3. LIFT = 该子群坏样本率 / 整体坏样本率 **方向模式** | direction | 含义 | 评分方式 | |-----------|------|----------| | auto | 自动选择最优方向(默认) | score = max(LIFT_bad-1, 1-LIFT_good, 0) | | bad | 仅评估找坏人能力 | score = max(LIFT_bad - 1, 0) | | good | 仅评估找好人能力 | score = max(1 - LIFT_good, 0) | **评分含义** score 只度量目标方向相对基准 LIFT=1 的改善,反向偏离按 0 计: - score = 0: 无区分能力(LIFT = 1) - score = 4.0: 强找坏人能力(LIFT_bad=5.0);找好人得分上限为 1(LIFT_good=0) - 内部经验: score >= 0.5 通常认为有一定区分力 **参数** :param threshold: 目标方向的改善得分阈值,默认0.5 - 仅保留 score >= threshold 的特征 - threshold=0.5 等价于旧版 LIFT >= 1.5(找坏人方向) - 内部经验: 风控场景常用 0.5~1.0 :param ratio: LIFT计算的覆盖率,默认0.10(即LIFT@10%) - 内部经验: 风控场景常用 lift@5% 或 lift@10% :param direction: 方向模式,默认'auto' - 'auto': 同时计算两个方向,取最优(推荐) - 'bad': 仅评估找坏人能力(降序取头部,LIFT > 1) - 'good': 仅评估找好人能力(升序取头部,LIFT < 1) :param target: 目标变量列名,默认为'target' :param include: 强制保留的特征列表 :param exclude: 强制剔除的特征列表 :param n_jobs: 并行计算的任务数 **属性** - scores\_: 各特征在目标方向上的改善得分,pd.Series - lift_detail\_: 各特征的LIFT详情表,pd.DataFrame 包含列: LIFT_bad, LIFT_good, best_direction, score **参考样例** >>> from hscredit.core.selectors import LiftSelector >>> 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 = LiftSelector(threshold=0.5, ratio=0.10) >>> selector.fit(X, y) >>> print(selector.lift_detail_) # 查看各特征两个方向的LIFT >>> >>> # 仅评估找坏人能力 >>> selector = LiftSelector(direction='bad', threshold=0.5) >>> selector.fit(X, y) >>> >>> # 仅评估找好人能力 >>> selector = LiftSelector(direction='good', threshold=0.5) >>> selector.fit(X, y) **引用** LIFT@k%(头部覆盖率下的提升度)是响应/风险模型的标准评估口径,参见 lift chart https://en.wikipedia.org/wiki/Lift_(data_mining) 及 Siddiqi, N. (2006). *Credit Risk Scorecards.* Wiley。 """ method_name = "LIFT筛选" def __init__( self, threshold: float = 0.5, ratio: float = 0.10, direction: Literal["auto", "bad", "good"] = "auto", 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, ): 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.ratio = ratio self.direction = direction def _fit_impl( self, X: pd.DataFrame, y: Optional[Union[pd.Series, np.ndarray]], ) -> None: """拟合LIFT筛选器。 :param X: 输入特征DataFrame :param y: 目标变量 """ self._get_feature_names(X) if y is None: raise ValueError("LiftSelector 需要目标变量 y") if not 0 < float(self.ratio) <= 1: raise ValueError("ratio 必须在 (0, 1] 范围内") if self.direction not in {"auto", "bad", "good"}: raise ValueError("direction 必须是 'auto'、'bad' 或 'good'") y = np.asarray(y) results = self._parallel_execute( _compute_lift_feature, ((col, X[col].values, y, self.ratio, self.direction) 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=8.0, capability="thread_safe", releases_gil=True, operation="LIFT字段排序", ), ) # 解包结果 scores = np.array([r[1] for r in results]) lift_bad = np.array([r[2] for r in results]) lift_good = np.array([r[3] for r in results]) best_dirs = [r[4] for r in results] # 评分只奖励目标方向上的改善 self.scores_ = pd.Series(scores, index=X.columns) # LIFT详情表 self.lift_detail_ = pd.DataFrame( { "LIFT_bad": lift_bad, "LIFT_good": lift_good, "best_direction": best_dirs, "score": scores, }, index=X.columns, ) # 选择 score >= threshold 的特征 selected_mask = scores >= self.threshold self.selected_features_ = X.columns[selected_mask].tolist() # 生成剔除原因 dir_label = {"auto": "自动", "bad": "找坏人", "good": "找好人"} self._drop_reason = f"LIFT@{self.ratio:.0%} 方向改善得分 < {self.threshold}" f"(方向: {dir_label.get(self.direction, self.direction)})"