hscredit.core.models.losses.custom_metrics 源代码

"""自定义评估指标.

提供金融风控场景常用的评估指标,如KS、Gini、PSI等。
"""

import numpy as np
from typing import Optional
from .base import BaseMetric
from ...metrics import auc as auc_metric, ks as ks_metric


[文档] class KSMetric(BaseMetric): """KS (Kolmogorov-Smirnov) 指标. 衡量模型区分好坏客户的能力,KS值越大表示模型区分能力越强。 ``KS = max(|累积好客户比例 - 累积坏客户比例|)`` :param name: 指标名称,默认为"ks" **参考样例** >>> import numpy as np >>> from hscredit.core.models.losses import KSMetric >>> >>> ks_metric = KSMetric() >>> y_true = np.array([0, 0, 1, 1, 0, 1, 0, 1]) >>> y_pred = np.array([0.1, 0.2, 0.8, 0.9, 0.3, 0.7, 0.4, 0.6]) >>> ks_value = ks_metric(y_true, y_pred) >>> print(f"KS: {ks_value:.4f}") >>> # 在LightGBM中使用 >>> import lightgbm as lgb >>> train_data = lgb.Dataset(X_train, label=y_train) >>> bst = lgb.train( ... params={'objective': 'binary'}, ... train_set=train_data, ... feval=ks_metric.to_lightgbm(api="native"), ... num_boost_round=100 ... ) **引用** KS(Kolmogorov–Smirnov)统计量:https://en.wikipedia.org/wiki/Kolmogorov–Smirnov_test ; 其在信用评分中的应用见 Siddiqi, N. (2006). *Credit Risk Scorecards.* Wiley。 """ def __init__(self, name: str = "ks"): super().__init__(name, greater_is_better=True) def __call__( self, y_true: np.ndarray, y_pred: np.ndarray, sample_weight=None, ) -> float: """计算KS值. :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率;统一输入校验请使用 evaluate。 :param sample_weight: 可选非负有限权重,须与标签等长且两类都有正权重。 :return: KS值,范围[0, 1],越大越好 >>> value = KSMetric().evaluate([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ # 确保输入是一维数组 y_true = np.ravel(y_true) y_pred = np.ravel(y_pred) if sample_weight is not None: from sklearn.metrics import roc_curve from .base import _validate_binary_inputs y_true, y_pred, sample_weight = _validate_binary_inputs(y_true, y_pred, sample_weight) if not all(np.sum(sample_weight[y_true == label]) > 0 for label in (0, 1)): raise ValueError("计算加权KS需要有效权重下同时存在好样本和坏样本") false_positive, true_positive, _ = roc_curve(y_true, y_pred, sample_weight=sample_weight) return float(np.max(np.abs(true_positive - false_positive))) return ks_metric(y_true, y_pred)
[文档] class GiniMetric(BaseMetric): """Gini系数指标. Gini = 2 * AUC - 1 衡量模型区分能力,Gini越大越好。 :param name: 指标名称,默认为"gini" :param score_direction: 同 ``metrics.auc``,默认auto;higher_risk保留原始方向 **参考样例** >>> from hscredit.core.models.losses import GiniMetric >>> gini_metric = GiniMetric() >>> gini_value = gini_metric(y_true, y_pred) """ def __init__(self, name: str = "gini", score_direction: str = "auto"): super().__init__(name, greater_is_better=True) self.score_direction = score_direction def __call__( self, y_true: np.ndarray, y_pred: np.ndarray, sample_weight=None, ) -> float: """计算Gini系数. :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率;统一输入校验请使用 evaluate。 :param sample_weight: 可选非负有限权重,须与标签等长且总和大于 0。 :return: Gini系数,默认范围[0, 1];显式方向时范围[-1, 1],越大越好 >>> value = GiniMetric(score_direction='higher_risk').evaluate( ... [0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ # 确保输入是一维数组 y_true = np.ravel(y_true) y_pred = np.ravel(y_pred) # 计算AUC auc = self._compute_auc(y_true, y_pred, sample_weight=sample_weight) # Gini = 2*AUC - 1 gini = 2 * auc - 1 return float(gini) def _compute_auc( self, y_true: np.ndarray, y_pred: np.ndarray, sample_weight=None, ) -> float: """调用公共AUC;保留单类别回调返回中性值的既有约定。""" if np.unique(y_true).size < 2: return 0.5 return auc_metric(y_true, y_pred, sample_weight=sample_weight, score_direction=self.score_direction)
[文档] class PSIMetric(BaseMetric): """PSI (Population Stability Index) 指标. 衡量样本分布的稳定性,常用于模型监控。 PSI = sum((实际占比 - 期望占比) * ln(实际占比/期望占比)) :param expected: 基准预测分数的一维非空有限数组,默认 None, 为 None 时需要在 __call__ 中提供;不要求与当前样本等长。 :param n_bins: 基准分位分箱的最大数量,整数且至少为 2,默认 10。 :param name: 指标名称,默认为"psi" **参考样例** >>> from hscredit.core.models.losses import PSIMetric >>> >>> # 使用训练集作为基准 >>> psi_metric = PSIMetric(expected=y_train_pred, n_bins=10) >>> >>> # 计算测试集的PSI >>> psi_value = psi_metric(y_test, y_test_pred) >>> print(f"PSI: {psi_value:.4f}") **注意** 复用 ``hscredit.core.metrics.psi`` 的冻结基准分箱,不丢弃落在基准 最小值和最大值之外的样本。低基数或常量基准遵循公共 PSI 的类别口径。 本指标不支持样本权重;它衡量分布漂移,不能单独代替 AUC 等区分度指标。 PSI解释: - PSI < 0.1: 分布稳定 - 0.1 <= PSI < 0.25: 分布有轻微变化 - PSI >= 0.25: 分布变化显著,需要关注 """ def __init__(self, expected: Optional[np.ndarray] = None, n_bins: int = 10, name: str = "psi"): if isinstance(n_bins, (bool, np.bool_)) or not isinstance(n_bins, (int, np.integer)) or n_bins < 2: raise ValueError("PSI 的 n_bins 必须是至少为 2 的整数") super().__init__(name, greater_is_better=False) # PSI越小越好 self.expected = self._validate_distribution(expected, "基准分布") if expected is not None else None self.n_bins = int(n_bins) @staticmethod def _validate_distribution(values, name): """校验一维有限分布,返回独立副本以固定基准。""" try: values = np.asarray(values, dtype=float) except (TypeError, ValueError) as exc: raise ValueError(f"PSI 的{name}必须是一维非空有限数值数组") from exc if values.ndim != 1 or values.size == 0 or not np.isfinite(values).all(): raise ValueError(f"PSI 的{name}必须是一维非空有限数值数组") return values.copy() def __call__(self, y_true: np.ndarray, y_pred: np.ndarray, expected: Optional[np.ndarray] = None) -> float: """计算PSI. :param y_true: 真实标签,此方法中未使用;调用 evaluate 时仍须提供 0/1 标签。 :param y_pred: 当前分布的一维非空有限预测分数。 :param expected: 可选基准分布,覆盖构造时的基准,仅对本次计算生效。 :return: float,PSI 越小表示分布越接近;计算口径同公共 metrics.psi。 :raises ValueError: 未设置基准,或输入为空、多维、非有限数值。 >>> metric = PSIMetric(expected=[0.1, 0.2, 0.3, 0.4], n_bins=2) >>> value = metric([0, 0, 1, 1], [0.1, 0.2, 0.8, 0.9]) """ expected = expected if expected is not None else self.expected if expected is None: raise ValueError("需要提供期望分布(expected)") expected = self._validate_distribution(expected, "基准分布") actual = self._validate_distribution(y_pred, "当前分布") from ...metrics import psi return float(psi(expected, actual, method="quantile", max_n_bins=self.n_bins))