hscredit.report.mining.metrics 源代码

"""规则评估指标模块.

提供丰富的规则评估指标,所有指标计算统一收口到hscredit.core.metrics。
"""

import copy
import logging
import numpy as np
import pandas as pd
from typing import Union, List, Dict, Optional, Any
from sklearn.metrics import (
    accuracy_score, precision_score, recall_score, f1_score,
    confusion_matrix
)

# 从统一metrics模块导入指标
from ...core.metrics import (
    ks, gini,
    iv,
    lift_table as metrics_lift_table,
)

logger = logging.getLogger(__name__)

from ...utils.parallel import ParallelizableMixin
from .base import _mining_workload


def _rule_metrics_worker(task):
    """评估一个独立规则并附加稳定的输入序号。"""
    calculator, ordinal, rule, X_train, y_train, X_test, y_test, kwargs = task
    result = calculator.evaluate_rule(
        copy.deepcopy(rule),
        X_train,
        y_train,
        X_test,
        y_test,
        **kwargs,
    )
    result["规则编号"] = ordinal
    result["规则"] = str(rule)
    return result


[文档] class RuleMetrics(ParallelizableMixin): """规则评估指标计算器. 提供全面的规则评估指标,支持训练集和测试集的对比分析。 所有指标计算统一收口到hscredit.core.metrics。 **参考样例** >>> from hscredit.core.rules.mining import RuleMetrics >>> metrics = RuleMetrics() >>> result = metrics.evaluate_rule(rule, X_train, y_train, X_test, y_test) # 单规则评估:返回KS/AUC/IV等指标 >>> results = metrics.evaluate_rules(rules, X_train, y_train, X_test, y_test) # 批量规则评估:返回规则列表各自的指标 """ def __init__( self, target_positive: int = 1, n_jobs: Optional[Union[int, float]] = -1, parallel_backend: Optional[str] = None, parallel_config: Optional[Dict[str, Any]] = None, ): """ :param target_positive: 正类标签,默认1 """ self.target_positive = target_positive self.n_jobs = n_jobs self.parallel_backend = parallel_backend self.parallel_config = parallel_config
[文档] def evaluate_rule( self, rule, X_train: pd.DataFrame, y_train: pd.Series, X_test: Optional[pd.DataFrame] = None, y_test: Optional[pd.Series] = None, amount_train: Optional[pd.Series] = None, amount_test: Optional[pd.Series] = None ) -> Dict[str, Any]: """评估单个规则. :param rule: 规则对象(Rule或MinedRule) :param X_train: 训练集特征 :param y_train: 训练集标签 :param X_test: 测试集特征(可选) :param y_test: 测试集标签(可选) :param amount_train: 训练集金额(可选) :param amount_test: 测试集金额(可选) :return: 评估指标字典 """ result = {} # 训练集评估 train_metrics = self._calculate_metrics( rule, X_train, y_train, amount_train ) for k, v in train_metrics.items(): result[f'训练_{k}'] = v # 测试集评估 if X_test is not None and y_test is not None: test_metrics = self._calculate_metrics( rule, X_test, y_test, amount_test ) for k, v in test_metrics.items(): result[f'测试_{k}'] = v # 稳定性指标 - 使用统一的PSI计算 result['badrate_diff'] = ( train_metrics.get('命中LIFT值', 0) - test_metrics.get('命中LIFT值', 0) ) return result
def _calculate_metrics( self, rule, X: pd.DataFrame, y: pd.Series, amount: Optional[pd.Series] = None ) -> Dict[str, float]: """计算基础指标. 规则效果类指标统一通过 Rule.report 计算得到; 分类指标(accuracy/precision/recall/f1)仍基于命中掩码计算。 """ # 应用规则掩码(用于分类指标) if hasattr(rule, 'evaluate'): mask = rule.evaluate(X) elif hasattr(rule, 'predict'): mask = rule.predict(X) else: raise ValueError("规则对象必须有evaluate或predict方法") if isinstance(mask, pd.Series): mask = mask.values # Rule.report 计算规则效果 data = X.copy() data['__target__'] = y.values amount_col = None if amount is not None: amount_col = '__amount__' data[amount_col] = amount.values if isinstance(amount, pd.Series) else amount report_df = rule.report( datasets=data, target='__target__', amount=amount_col ) hit = report_df[report_df['分箱'] == '命中'].iloc[0].to_dict() if '分箱' in report_df.columns and not report_df[report_df['分箱'] == '命中'].empty else {} # 分类指标 y_pred = np.zeros_like(y) y_pred[mask] = 1 accuracy = accuracy_score(y, y_pred) precision = precision_score(y, y_pred, zero_division=0) recall = recall_score(y, y_pred, zero_division=0) f1 = f1_score(y, y_pred, zero_division=0) # 混淆矩阵 tn, fp, fn, tp = confusion_matrix(y, y_pred).ravel() total_bad = y.sum() total = len(y) hit_bad = float(hit.get('坏样本数', np.sum(y[mask]) if np.any(mask) else 0)) hit_count = float(hit.get('样本总数', np.sum(mask))) overall_badrate = total_bad / total if total > 0 else 0 badrate_after = (total_bad - hit_bad) / (total - hit_count) if (total - hit_count) > 0 else 0 badrate_reduction = (overall_badrate - badrate_after) / overall_badrate if overall_badrate > 0 else 0 metrics = { '命中样本数': hit_count, '命中样本占比': float(hit.get('样本占比', hit_count / total if total > 0 else 0)), '命中坏样本数': hit_bad, '命中好样本数': float(hit.get('好样本数', hit_count - hit_bad)), '命中坏样本率': float(hit.get('坏样本率', hit_bad / hit_count if hit_count > 0 else 0)), '命中LIFT值': float(hit.get('LIFT值', 0)), '准确率': accuracy, '精确率': precision, '召回率': recall, 'F1值': f1, '真正例': int(tp), '假正例': int(fp), '真负例': int(tn), '假负例': int(fn), '拦截后坏样本率': badrate_after, '坏账改善': badrate_reduction } # 金额口径指标(当传入amount时,Rule.report已按金额口径计算) if amount_col is not None: total_amount = float(data[amount_col].sum()) selected_amount = float(hit.get('样本总数', 0)) bad_amount = float(hit.get('坏样本数', 0)) metrics.update({ '总金额': total_amount, '命中金额': selected_amount, '命中金额占比': selected_amount / total_amount if total_amount > 0 else 0, '命中损失率': bad_amount / selected_amount if selected_amount > 0 else 0, '命中金额LIFT值': float(hit.get('LIFT值', 0)) }) return metrics
[文档] def evaluate_rules( self, rules: List, X_train: pd.DataFrame, y_train: pd.Series, X_test: Optional[pd.DataFrame] = None, y_test: Optional[pd.Series] = None, **kwargs ) -> pd.DataFrame: """批量评估规则. :param rules: 规则列表 :param X_train: 训练集特征 :param y_train: 训练集标签 :param X_test: 测试集特征 :param y_test: 测试集标签 :return: 评估结果DataFrame """ tasks = [ ( self, ordinal, rule, X_train, y_train, X_test, y_test, dict(kwargs), ) for ordinal, rule in enumerate(rules) ] results = self._parallel_execute( _rule_metrics_worker, tasks, task_labels=[f"规则 {ordinal}" for ordinal in range(len(rules))], default_backend="threading", has_parallel_children=False, workload=_mining_workload( X_train, len(tasks), operation="规则指标批量评估", cost_per_item=10.0, ), ) return pd.DataFrame(results)
[文档] def calculate_ks(self, y_true: np.ndarray, y_score: np.ndarray) -> float: """计算KS统计量. 使用统一的KS计算。 :param y_true: 真实标签 :param y_score: 预测分数 :return: KS值 """ return ks(y_true, y_score)
[文档] def calculate_iv( self, feature: pd.Series, target: pd.Series, n_bins: int = 10 ) -> float: """计算IV值(信息价值). 使用统一的IV计算。 :param feature: 特征值 :param target: 目标变量 :param n_bins: 分箱数 :return: IV值 """ return iv(target, feature, max_n_bins=n_bins)
[文档] def calculate_gini(self, y_true: np.ndarray, y_score: np.ndarray) -> float: """计算Gini系数. 使用统一的Gini计算。 :param y_true: 真实标签 :param y_score: 预测分数 :return: Gini系数 """ return gini(y_true, y_score)
[文档] def calculate_rule_metrics( rule, X: pd.DataFrame, y: pd.Series, X_test: Optional[pd.DataFrame] = None, y_test: Optional[pd.Series] = None ) -> Dict[str, Any]: """便捷函数:计算规则评估指标. :param rule: 规则对象 :param X: 训练集特征 :param y: 训练集标签 :param X_test: 测试集特征 :param y_test: 测试集标签 :return: 评估指标字典 """ metrics = RuleMetrics() return metrics.evaluate_rule(rule, X, y, X_test, y_test)
def calculate_lift_chart( y_true: pd.Series, y_score: pd.Series, n_buckets: int = 10 ) -> pd.DataFrame: """计算Lift图表数据. 使用统一的lift_table计算。 :param y_true: 真实标签 :param y_score: 预测分数 :param n_buckets: 分桶数 :return: Lift数据DataFrame """ return metrics_lift_table(y_true, y_score, n_bins=n_buckets)