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

"""自定义评估指标.

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

import numpy as np
from typing import Optional
from .base import BaseMetric


[文档] 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(), ... 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 ) -> float: """计算KS值. :param y_true: 真实标签 :param y_pred: 预测概率 :return: KS值,范围[0, 1],越大越好 """ # 确保输入是一维数组 y_true = np.ravel(y_true) y_pred = np.ravel(y_pred) # 按预测分数排序 sorted_indices = np.argsort(y_pred) sorted_y_true = y_true[sorted_indices] # 计算累积分布 n_good = np.sum(y_true == 0) n_bad = np.sum(y_true == 1) if n_good == 0 or n_bad == 0: return 0.0 # 累积好客户和坏客户数量 cum_good = np.cumsum(sorted_y_true == 0) cum_bad = np.cumsum(sorted_y_true == 1) # 累积比例 cum_good_rate = cum_good / n_good cum_bad_rate = cum_bad / n_bad # KS值 ks = np.max(np.abs(cum_good_rate - cum_bad_rate)) return float(ks)
[文档] class GiniMetric(BaseMetric): """Gini系数指标. Gini = 2 * AUC - 1 衡量模型区分能力,Gini越大越好。 :param name: 指标名称,默认为"gini" **参考样例** >>> from hscredit.core.models.losses import GiniMetric >>> gini_metric = GiniMetric() >>> gini_value = gini_metric(y_true, y_pred) """ def __init__(self, name: str = "gini"): super().__init__(name, greater_is_better=True) def __call__( self, y_true: np.ndarray, y_pred: np.ndarray ) -> float: """计算Gini系数. :param y_true: 真实标签 :param y_pred: 预测概率 :return: Gini系数,范围[-1, 1],越大越好 """ # 确保输入是一维数组 y_true = np.ravel(y_true) y_pred = np.ravel(y_pred) # 计算AUC auc = self._compute_auc(y_true, y_pred) # Gini = 2*AUC - 1 gini = 2 * auc - 1 return float(gini) def _compute_auc( self, y_true: np.ndarray, y_pred: np.ndarray ) -> float: """计算AUC.""" # 按预测分数排序(降序) sorted_indices = np.argsort(y_pred)[::-1] sorted_y_true = y_true[sorted_indices] # 计算TPR和FPR n_pos = np.sum(y_true == 1) n_neg = np.sum(y_true == 0) if n_pos == 0 or n_neg == 0: return 0.5 # 使用梯形法则计算AUC cum_pos = np.cumsum(sorted_y_true == 1) cum_neg = np.cumsum(sorted_y_true == 0) # TPR和FPR tpr = cum_pos / n_pos fpr = cum_neg / n_neg # 添加原点 tpr = np.concatenate([[0], tpr]) fpr = np.concatenate([[0], fpr]) # 计算AUC(梯形法则,跨 NumPy 版本兼容) from ....utils.misc import trapz auc = trapz(tpr, fpr) return float(auc)
[文档] class PSIMetric(BaseMetric): """PSI (Population Stability Index) 指标. 衡量样本分布的稳定性,常用于模型监控。 PSI = sum((实际占比 - 期望占比) * ln(实际占比/期望占比)) :param expected: 期望分布的预测分数(基准数据),默认为None。如果为None,需要在__call__时提供 :param n_bins: 分箱数量,默认为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}") **注意** 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" ): super().__init__(name, greater_is_better=False) # PSI越小越好 self.expected = expected self.n_bins = n_bins def __call__( self, y_true: np.ndarray, y_pred: np.ndarray, expected: Optional[np.ndarray] = None ) -> float: """计算PSI. :param y_true: 真实标签(此指标中未使用,保持接口一致性) :param y_pred: 实际分布的预测分数 :param expected: 期望分布的预测分数 :return: PSI值,越小越好 """ # 使用传入的expected或初始化时的expected expected = expected if expected is not None else self.expected if expected is None: raise ValueError("需要提供期望分布(expected)") # 确保一维 expected = np.ravel(expected) actual = np.ravel(y_pred) # 计算分位数作为分箱边界 quantiles = np.linspace(0, 100, self.n_bins + 1) bin_edges = np.percentile(expected, quantiles) # 确保边界唯一 bin_edges = np.unique(bin_edges) # 计算期望分布和实际分布的频率 expected_counts, _ = np.histogram(expected, bins=bin_edges) actual_counts, _ = np.histogram(actual, bins=bin_edges) # 转换为比例 expected_rates = expected_counts / len(expected) actual_rates = actual_counts / len(actual) # 避免除零和log(0) expected_rates = np.clip(expected_rates, 1e-10, 1) actual_rates = np.clip(actual_rates, 1e-10, 1) # 计算PSI psi = np.sum( (actual_rates - expected_rates) * np.log(actual_rates / expected_rates) ) return float(psi)