"""分类模型评估指标.
提供二分类模型评估的核心指标计算功能。
**参数**
所有函数支持numpy.ndarray和pandas.Series两种输入格式,自动进行类型转换。
**参考样例**
>>> from hscredit.core.metrics import ks, auc, gini, accuracy
>>> import numpy as np
>>> np.random.seed(42)
>>> y_true = np.random.randint(0, 2, 1000) # 模拟真实标签(0=好,1=坏)
>>> y_prob = np.random.uniform(0, 1, 1000) # 模拟模型预测概率
>>> print(f"KS={ks(y_true, y_prob):.4f}, AUC={auc(y_true, y_prob):.4f}, Gini={gini(y_true, y_prob):.4f}")
**引用**
- 评分卡评估指标(KS / AUC / Gini)的业界用法见
Siddiqi, N. (2006). *Credit Risk Scorecards: Developing and Implementing
Intelligent Credit Scoring.* Wiley.
- accuracy / precision / recall / f1 / roc_curve / confusion_matrix 等直接封装自
scikit-learn,定义与参数含义同
https://scikit-learn.org/stable/modules/model_evaluation.html
"""
import numpy as np
import pandas as pd
from typing import Union, Tuple, Optional, Dict, Any
from sklearn.metrics import (
roc_auc_score, roc_curve as sklearn_roc_curve,
confusion_matrix as sk_confusion_matrix,
classification_report as sk_classification_report,
accuracy_score, precision_score, recall_score, f1_score
)
from ._base import _validate_same_length, _validate_binary_target
from ._binning import compute_bin_stats
from scipy.stats import ks_2samp as _ks_2samp
[文档]
def ks(y_true: Union[np.ndarray, pd.Series],
y_prob: Union[np.ndarray, pd.Series]) -> float:
"""计算Kolmogorov-Smirnov统计量.
KS值衡量模型区分正负样本的能力,值越大区分效果越好。
KS = max(TPR - FPR),其中TPR为正样本累积率,FPR为负样本累积率。
**参数**
:param y_true: 真实标签 (0/1),0为负样本,1为正样本
:param y_prob: 预测为正样本的概率值
:return: KS统计量,取值范围[0, 1],越接近1区分能力越强
:raises ValueError: 标签非二值或y_true/y_prob长度不一致时
:raises ValueError: y_true中全为正样本或全为负样本时
**参考样例**
>>> from hscredit.core.metrics import ks
>>> y_true = [0, 0, 1, 1, 1, 0, 1, 0] # 真实标签序列
>>> y_prob = [0.1, 0.3, 0.7, 0.6, 0.8, 0.2, 0.9, 0.4] # 预测概率(高分对应坏样本)
>>> ks(y_true, y_prob)
0.75
**引用**
Kolmogorov-Smirnov 统计量原为两分布的最大累积差异检验
(Kolmogorov, 1933; Smirnov, 1948),在信用评分中作为模型区分度的
标准指标,定义见 Siddiqi, N. (2006). *Credit Risk Scorecards.* Wiley。
经验阈值:KS<0.2 区分弱、0.2~0.4 可用、0.4~0.6 强、>0.75 需排查标签泄漏。
"""
y_true = np.asarray(y_true)
y_prob = np.asarray(y_prob)
_validate_same_length(y_true, y_prob, ("y_true", "y_prob"))
_validate_binary_target(y_true)
# 按预测概率降序排序
desc_score_indices = np.argsort(y_prob)[::-1]
y_true_sorted = y_true[desc_score_indices]
n_total = len(y_true)
n_pos = np.sum(y_true)
n_neg = n_total - n_pos
if n_pos == 0 or n_neg == 0:
return 0.0
cum_pos = np.cumsum(y_true_sorted) / n_pos
cum_neg = np.cumsum(1 - y_true_sorted) / n_neg
return np.max(np.abs(cum_pos - cum_neg))
[文档]
def ks_2samps(sample1: Union[np.ndarray, pd.Series],
sample2: Union[np.ndarray, pd.Series]) -> float:
"""计算两组独立样本之间的 Kolmogorov-Smirnov 统计量(两样本KS).
与 ks(y_true, y_prob) 不同,本函数比较两个独立样本的分布,
适用于好坏样本分布对比分析。
:param sample1: 第一组样本(通常是好样本的分数)
:param sample2: 第二组样本(通常是坏样本的分数)
:return: KS统计量,取值范围[0, 1]
**参考样例**
>>> from hscredit.core.metrics import ks_2samps
>>> import numpy as np
>>> np.random.seed(42)
>>> good_scores = np.random.normal(0.5, 0.1, 500)
>>> bad_scores = np.random.normal(0.7, 0.1, 500)
>>> ks_2samps(good_scores, bad_scores)
0.69
**引用**
封装自 :func:`scipy.stats.ks_2samp`(两样本 Kolmogorov-Smirnov 检验),
取其返回的统计量分量。详见
https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.ks_2samp.html
"""
s1 = np.asarray(sample1).ravel()
s2 = np.asarray(sample2).ravel()
if len(s1) == 0 or len(s2) == 0:
return 0.0
return _ks_2samp(s1, s2)[0]
[文档]
def auc(y_true: Union[np.ndarray, pd.Series],
y_prob: Union[np.ndarray, pd.Series]) -> float:
"""计算ROC曲线下的面积(AUC).
AUC值衡量模型在不同分类阈值下区分正负样本的综合能力,
值在0.5-1.0之间,越接近1.0模型效果越好。
**参数**
:param y_true: 真实标签 (0/1),0为负样本,1为正样本
:param y_prob: 预测为正样本的概率值
:return: AUC值,取值范围[0.5, 1.0]
:raises ValueError: y_true/y_prob长度不一致时
**参考样例**
>>> from hscredit.core.metrics import auc
>>> y_true = [0, 0, 1, 1, 1, 0, 1, 0]
>>> y_prob = [0.1, 0.3, 0.7, 0.6, 0.8, 0.2, 0.9, 0.4]
>>> auc(y_true, y_prob)
0.875
**引用**
封装自 :func:`sklearn.metrics.roc_auc_score`。AUC 的概率解释(随机抽取一正一负
样本,正样本得分更高的概率)见 Fawcett, T. (2006). *An introduction to ROC
analysis.* Pattern Recognition Letters, 27(8), 861-874。
"""
return roc_auc_score(y_true, y_prob)
[文档]
def gini(y_true: Union[np.ndarray, pd.Series],
y_prob: Union[np.ndarray, pd.Series]) -> float:
"""计算基尼系数 (Gini Coefficient).
基尼系数是AUC的线性变换:基尼系数 = 2 * AUC - 1。
范围从-1到1,越接近1表示模型区分能力越强。
**参数**
:param y_true: 真实标签 (0/1),0为负样本,1为正样本
:param y_prob: 预测为正样本的概率值
:return: 基尼系数,取值范围[-1, 1]
:raises ValueError: y_true/y_prob长度不一致时
**参考样例**
>>> from hscredit.core.metrics import gini
>>> y_true = [0, 0, 1, 1, 1, 0, 1, 0]
>>> y_prob = [0.1, 0.3, 0.7, 0.6, 0.8, 0.2, 0.9, 0.4]
>>> gini(y_true, y_prob)
0.75
**引用**
此处的基尼系数(亦称 Accuracy Ratio / Somers' D)由 AUC 线性变换得到,
Gini = 2·AUC − 1,是信用风险领域衡量区分度的常用指标,见
Siddiqi, N. (2006). *Credit Risk Scorecards.* Wiley。
"""
return 2 * auc(y_true, y_prob) - 1
[文档]
def accuracy(y_true: Union[np.ndarray, pd.Series],
y_pred: Union[np.ndarray, pd.Series]) -> float:
"""计算准确率 (Accuracy).
准确率 = 预测正确的样本数 / 总样本数。
**参数**
:param y_true: 真实标签(任意类型)
:param y_pred: 预测标签(与y_true类型一致)
:return: 准确率,取值范围[0, 1]
:raises ValueError: y_true/y_pred长度不一致时
**参考样例**
>>> from hscredit.core.metrics import accuracy
>>> y_true = [0, 1, 1, 0, 1]
>>> y_pred = [0, 1, 0, 0, 1]
>>> accuracy(y_true, y_pred)
0.8
"""
return accuracy_score(y_true, y_pred)
[文档]
def precision(y_true: Union[np.ndarray, pd.Series],
y_pred: Union[np.ndarray, pd.Series],
average: str = 'binary') -> float:
"""计算精确率 (Precision).
精确率 = TP / (TP + FP),即预测为正的样本中真正为正的比例。
**参数**
:param y_true: 真实标签
:param y_pred: 预测标签
:param average: 平均方式,'binary'(二分类默认只看正类),'micro'(全局TP/FP/FN),
'macro'(各类分别计算后取平均),'weighted'(加权平均),默认为'binary'
:return: 精确率,取值范围[0, 1]
**参考样例**
>>> from hscredit.core.metrics import precision
>>> y_true = [0, 1, 1, 0, 1]
>>> y_pred = [0, 1, 0, 0, 1]
>>> precision(y_true, y_pred) # 二分类默认
1.0
>>> precision(y_true, y_pred, average='macro')
0.9
"""
return precision_score(y_true, y_pred, average=average, zero_division=0)
[文档]
def recall(y_true: Union[np.ndarray, pd.Series],
y_pred: Union[np.ndarray, pd.Series],
average: str = 'binary') -> float:
"""计算召回率 (Recall / Sensitivity).
召回率 = TP / (TP + FN),即所有正样本中被正确预测的比例。
**参数**
:param y_true: 真实标签
:param y_pred: 预测标签
:param average: 平均方式,'binary'(二分类默认只看正类),'micro','macro','weighted',
默认为'binary'
:return: 召回率,取值范围[0, 1]
**参考样例**
>>> from hscredit.core.metrics import recall
>>> y_true = [0, 1, 1, 0, 1]
>>> y_pred = [0, 1, 0, 0, 1]
>>> recall(y_true, y_pred)
0.666...
"""
return recall_score(y_true, y_pred, average=average, zero_division=0)
[文档]
def f1(y_true: Union[np.ndarray, pd.Series],
y_pred: Union[np.ndarray, pd.Series],
average: str = 'binary') -> float:
"""计算F1分数 (F1 Score).
F1 = 2 * (精确率 * 召回率) / (精确率 + 召回率),是精确率和召回率的调和平均。
**参数**
:param y_true: 真实标签
:param y_pred: 预测标签
:param average: 平均方式,'binary'(二分类默认只看正类),'micro','macro','weighted',
默认为'binary'
:return: F1分数,取值范围[0, 1]
**参考样例**
>>> from hscredit.core.metrics import f1
>>> y_true = [0, 1, 1, 0, 1]
>>> y_pred = [0, 1, 0, 0, 1]
>>> f1(y_true, y_pred)
0.8
"""
return f1_score(y_true, y_pred, average=average, zero_division=0)
[文档]
def ks_bucket(y_true: Union[np.ndarray, pd.Series],
y_prob: Union[np.ndarray, pd.Series],
method: str = 'quantile',
max_n_bins: int = 10,
min_bin_size: float = 0.01,
**kwargs) -> pd.DataFrame:
"""计算分桶KS统计表.
将样本按预测概率分为多个桶,计算每个桶的KS统计信息,
包括各桶的样本数、坏账率、累积好坏样本率及KS贡献。
**参数**
:param y_true: 真实标签 (0/1),0为负样本(好样本),1为正样本(坏样本)
:param y_prob: 预测为正样本的概率值
:param method: 分箱方法,取值与 OptimalBinning.VALID_METHODS 一致(共17种),
默认为'quantile'。常用值:
- quantile: 等频分箱
- uniform: 等宽分箱
- tree: 决策树最优分箱
- chi: 卡方分箱
- mdlp: MDLP信息论分箱
- best_iv/best_ks: 最优IV/KS分箱
完整列表见 :class:`~hscredit.core.binning.OptimalBinning`
:param max_n_bins: 最大分桶数量,默认为10
:param min_bin_size: 每桶最小样本占比,默认为0.01
:param kwargs: 其他传递给OptimalBinning的参数
:return: 包含桶统计信息的DataFrame,列包括:
- 桶编号: 分箱序号
- 最小概率: 该桶内概率最小值
- 最大概率: 该桶内概率最大值
- 样本数: 该桶内样本数量
- 坏样本率: 该桶内坏样本占比
- 累积坏样本率: 累积坏样本占总坏样本比例
- 累积好样本率: 累积好样本占总好样本比例
- KS贡献: 该桶对KS值的贡献
:raises ValueError: 数据全部为缺失值时
**参考样例**
>>> from hscredit.core.metrics import ks_bucket
>>> import numpy as np
>>> np.random.seed(42)
>>> y_true = np.random.randint(0, 2, 1000)
>>> y_prob = np.random.uniform(0, 1, 1000)
>>> result = ks_bucket(y_true, y_prob, max_n_bins=5)
>>> print(result[['桶编号', '样本数', '坏样本率']])
"""
y_true = np.asarray(y_true)
y_prob = np.asarray(y_prob)
_validate_same_length(y_true, y_prob, ("y_true", "y_prob"))
# 移除缺失值
valid_mask = ~(pd.isna(y_prob) | pd.isna(y_true))
y_prob_clean = y_prob[valid_mask]
y_true_clean = y_true[valid_mask]
if len(y_prob_clean) == 0:
raise ValueError("没有有效数据(全部为缺失值)")
# 使用OptimalBinning进行分箱
from ..binning import OptimalBinning
df = pd.DataFrame({'prob': y_prob_clean, 'target': y_true_clean})
binner = OptimalBinning(
method=method,
max_n_bins=max_n_bins,
min_bin_size=min_bin_size,
verbose=False,
**kwargs
)
binner.fit(df[['prob']], df['target'])
bins = binner.transform(df[['prob']], metric='indices').values.flatten()
stats_df = compute_bin_stats(bins, y_true_clean, round_digits=False)
results = []
for _, row in stats_df.iterrows():
bin_idx = int(row['分箱'])
mask = bins == bin_idx
prob_min = y_prob_clean[mask].min() if mask.sum() > 0 else np.nan
prob_max = y_prob_clean[mask].max() if mask.sum() > 0 else np.nan
cum_total = row['累积坏样本数'] + row['累积好样本数']
results.append({
'桶编号': bin_idx,
'最小概率': prob_min,
'最大概率': prob_max,
'样本数': int(row['样本总数']),
'坏样本率': row['坏样本率'],
'累积坏样本率': row['累积坏样本数'] / cum_total if cum_total > 0 else 0,
'累积好样本率': row['累积好样本数'] / cum_total if cum_total > 0 else 0,
'KS贡献': row['分档KS值'],
})
return pd.DataFrame(results)
[文档]
def roc_curve(y_true: Union[np.ndarray, pd.Series],
y_prob: Union[np.ndarray, pd.Series]) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""计算ROC曲线数据.
返回ROC曲线绘制所需的FPR、TPR和阈值数据。
**参数**
:param y_true: 真实标签 (0/1)
:param y_prob: 预测为正样本的概率值
:return: 三元组 (fpr, tpr, thresholds)
- fpr: 假阳性率(False Positive Rate)数组
- tpr: 真阳性率(True Positive Rate)数组
- thresholds: 对应的概率阈值数组
**参考样例**
>>> from hscredit.core.metrics import roc_curve
>>> y_true = [0, 0, 1, 1, 1, 0, 1, 0]
>>> y_prob = [0.1, 0.3, 0.7, 0.6, 0.8, 0.2, 0.9, 0.4]
>>> fpr, tpr, thresholds = roc_curve(y_true, y_prob)
"""
return sklearn_roc_curve(y_true, y_prob)
[文档]
def confusion_matrix(y_true: Union[np.ndarray, pd.Series],
y_pred: Union[np.ndarray, pd.Series],
labels: Optional[list] = None) -> np.ndarray:
"""计算混淆矩阵.
混淆矩阵展示分类器的预测结果与真实标签的对应关系。
**参数**
:param y_true: 真实标签
:param y_pred: 预测标签
:param labels: 类别标签列表,指定矩阵的行序和列序,默认为None(自动推断)
:return: 混淆矩阵(2D ndarray)
- 行表示真实类别,列表示预测类别
- 二分类时:[[TN, FP], [FN, TP]]
**参考样例**
>>> from hscredit.core.metrics import confusion_matrix
>>> y_true = [0, 1, 1, 0, 1]
>>> y_pred = [0, 1, 0, 0, 1]
>>> confusion_matrix(y_true, y_pred)
array([[2, 0],
[1, 2]])
"""
return sk_confusion_matrix(y_true, y_pred, labels=labels)
[文档]
def classification_report(y_true: Union[np.ndarray, pd.Series],
y_pred: Union[np.ndarray, pd.Series],
target_names: Optional[list] = None,
output_dict: bool = False) -> Union[str, dict]:
"""生成分类报告.
输出各类的精确率、召回率、F1分数和支持样本数。
**参数**
:param y_true: 真实标签
:param y_pred: 预测标签
:param target_names: 目标类别名称列表,用于报告中替代类别标签显示
:param output_dict: 如果为True,返回字典格式;否则返回格式化字符串,默认为False
:return: 当output_dict=False时返回格式化字符串,当output_dict=True时返回字典
**参考样例**
>>> from hscredit.core.metrics import classification_report
>>> y_true = [0, 1, 1, 0, 1]
>>> y_pred = [0, 1, 0, 0, 1]
>>> print(classification_report(y_true, y_pred))
precision recall f1-score support
<BLANKLINE>
0 0.67 1.00 0.80 2
1 1.00 0.67 0.80 3
<BLANKLINE>
accuracy 0.80 5
macro avg 0.83 0.83 0.80 5
weighted avg 0.87 0.80 0.80 5
>>> classification_report(y_true, y_pred, output_dict=True)
{'0': {'precision': 0.67, 'recall': 1.0, 'f1-score': 0.8, 'support': 2}, ...}
"""
return sk_classification_report(
y_true, y_pred,
target_names=target_names,
output_dict=output_dict
)