hscredit.core.eda.relationship 源代码

"""特征标签关系模块.

提供IV分析、WOE分箱、单调性检验等特征与目标变量关系分析功能.
主要复用 hscredit.core.metrics 的功能.

**引用**

IV(信息价值)/ WOE(证据权重)的定义与分级阈值见
Siddiqi, N. (2006). *Credit Risk Scorecards.* Wiley;单变量 AUC 见
Fawcett, T. (2006). *An introduction to ROC analysis.* Pattern Recognition Letters。
"""

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

from ...utils.parallel import parallel_execute
from .utils import _eda_workload, validate_dataframe, validate_binary_target, iv_rating


_NESTED_BINNING_METHODS = frozenset({"genetic", "or_tools", "cp_sat"})


def _batch_iv_worker(task):
    """计算单个特征 IV,并保留批量接口原有的容错行。"""
    feature_df, feature, target, n_bins, method = task
    try:
        result = iv_analysis(feature_df, feature, target, n_bins, method)
        return (
            {
                "特征名": result["特征名"],
                "IV值": result["IV值"],
                "预测能力": result["预测能力"],
                "分箱数": result["分箱数"],
            },
            result["分箱明细"],
        )
    except Exception:
        return ({"特征名": feature, "IV值": np.nan, "预测能力": "计算失败", "分箱数": 0}, None)


def _feature_importance_worker(task):
    """计算单个特征的 IV/AUC 组合指标。"""
    feature_df, feature, target, metrics = task
    result: Dict[str, Any] = {"特征名": feature}
    if "iv" in metrics:
        try:
            iv_result = iv_analysis(feature_df, feature, target)
            result["IV值"] = iv_result["IV值"]
            result["预测能力"] = iv_result["预测能力"]
        except Exception:
            result["IV值"] = np.nan
    if "auc" in metrics:
        try:
            result["AUC值"] = univariate_auc(feature_df, feature, target)["AUC值"]
        except Exception:
            result["AUC值"] = np.nan
    return result


[文档] def iv_analysis(df: pd.DataFrame, feature: str, target: str, n_bins: int = 10, method: str = 'quantile') -> Dict[str, Union[str, float, pd.DataFrame]]: """单变量IV分析. 复用 hscredit.core.metrics.IV_table :param df: 输入数据 :param feature: 特征名 :param target: 目标变量名 :param n_bins: 分箱数 :param method: 分箱方法,取值同 :class:`~hscredit.core.binning.OptimalBinning` (如 ``'quantile'`` 等频、``'tree'`` 决策树、``'chi'`` 卡方、``'mdlp'`` 信息论、 ``'best_iv'`` 最优IV 等),默认 ``'quantile'`` :return: IV分析结果字典,包含[特征名, IV值, 预测能力, 分箱明细] **参考样例** >>> result = iv_analysis(df, 'age', 'fpd15') >>> print(f"IV值: {result['IV值']}, 预测能力: {result['预测能力']}") >>> print(result['分箱明细']) """ validate_dataframe(df, required_cols=[feature, target]) validate_binary_target(df[target]) # 复用metrics模块 from ..metrics import iv_table # 计算IV表 iv_df = iv_table(df[target], df[feature], method=method, max_n_bins=n_bins) # 计算总IV iv_value = iv_df['分档IV值'].sum() return { '特征名': feature, 'IV值': round(iv_value, 4), '预测能力': iv_rating(iv_value), '分箱数': len(iv_df), '分箱明细': iv_df, }
[文档] def batch_iv_analysis(df: pd.DataFrame, features: List[str], target: str, n_bins: int = 10, method: str = 'quantile', return_details: bool = False, n_jobs=-1, parallel_backend=None, parallel_config=None) -> pd.DataFrame: """批量IV分析. 复用 hscredit.core.metrics.batch_iv :param df: 输入数据 :param features: 特征列表 :param target: 目标变量名 :param n_bins: 分箱数 :param method: 分箱方法,取值同 :class:`~hscredit.core.binning.OptimalBinning` (如 ``'quantile'`` 等频、``'tree'`` 决策树、``'chi'`` 卡方、``'mdlp'`` 信息论、 ``'best_iv'`` 最优IV 等),默认 ``'quantile'`` :param return_details: 是否返回详细分箱结果 :return: IV分析结果DataFrame,列包括[特征名, IV值, 预测能力, 分箱数] **参考样例** >>> iv_result = batch_iv_analysis(df, ['age', 'income', 'score'], 'fpd15') >>> print(iv_result[['特征名', 'IV值', '预测能力']].sort_values('IV值', ascending=False)) """ validate_dataframe(df, required_cols=[target]) validate_binary_target(df[target]) valid_features = [feature for feature in features if feature in df.columns] def iter_tasks(): for feature in valid_features: yield (df.loc[:, [feature, target]], feature, target, n_bins, method) has_parallel_children = method.strip().lower() in _NESTED_BINNING_METHODS analyzed = parallel_execute( _batch_iv_worker, iter_tasks(), n_jobs=n_jobs, parallel_backend=parallel_backend, parallel_config=parallel_config, task_labels=valid_features, default_backend="loky", has_parallel_children=has_parallel_children, workload=_eda_workload( df.loc[:, list(dict.fromkeys(valid_features + [target]))], len(valid_features), operation="批量IV分析", cost_per_item=16.0, capability="process_safe", has_parallel_children=has_parallel_children, ), ) results = [row for row, _ in analyzed] detail_results = { feature: detail for feature, (_, detail) in zip(valid_features, analyzed) if return_details and detail is not None } result_df = pd.DataFrame(results).sort_values('IV值', ascending=False).reset_index(drop=True) if return_details: return result_df, detail_results return result_df
[文档] def woe_analysis(df: pd.DataFrame, feature: str, target: str, n_bins: int = 10, method: str = 'quantile') -> pd.DataFrame: """WOE分箱分析. 复用 hscredit.core.metrics.IV_table 获取WOE值 :param df: 输入数据 :param feature: 特征名 :param target: 目标变量名 :param n_bins: 分箱数 :param method: 分箱方法,取值同 :class:`~hscredit.core.binning.OptimalBinning` (如 ``'quantile'`` 等频、``'tree'`` 决策树、``'chi'`` 卡方、``'mdlp'`` 信息论、 ``'best_iv'`` 最优IV 等),默认 ``'quantile'`` :return: WOE分析DataFrame **参考样例** >>> woe_df = woe_analysis(df, 'age', 'fpd15') >>> print(woe_df[['分箱', '分箱标签', 'WOE值', 'IV值']]) """ validate_dataframe(df, required_cols=[feature, target]) validate_binary_target(df[target]) # 复用metrics模块 from ..metrics import iv_table # 计算IV表(包含WOE值) iv_df = iv_table(df[target], df[feature], method=method, max_n_bins=n_bins) # 选择关键列并重命名 result = iv_df[['分箱标签', '样本总数', '好样本数', '坏样本数', '坏样本率', '分档WOE值', '分档IV值']].copy() result.columns = ['分箱', '样本数', '好样本数', '坏样本数', '逾期率', 'WOE值', 'IV值'] # 添加LIFT值 overall_bad_rate = df[target].mean() result['LIFT值'] = (result['逾期率'] / overall_bad_rate).round(4) return result
[文档] def binning_bad_rate(df: pd.DataFrame, feature: str, target: str, n_bins: int = 10, method: str = 'quantile') -> pd.DataFrame: """分箱逾期率分析. 复用 hscredit.core.metrics.compute_bin_stats :param df: 输入数据 :param feature: 特征名 :param target: 目标变量名 :param n_bins: 分箱数 :param method: 分箱方法,取值同 :class:`~hscredit.core.binning.OptimalBinning` (如 ``'quantile'`` 等频、``'tree'`` 决策树、``'chi'`` 卡方、``'mdlp'`` 信息论、 ``'best_iv'`` 最优IV 等),默认 ``'quantile'`` :return: 分箱逾期率DataFrame **参考样例** >>> bin_df = binning_bad_rate(df, 'score', 'fpd15', n_bins=10) >>> print(bin_df[['分箱', '样本数', '逾期率', 'LIFT值']]) """ validate_dataframe(df, required_cols=[feature, target]) validate_binary_target(df[target]) # 复用metrics模块 from ..metrics import compute_bin_stats from ..binning import OptimalBinning # 数据清洗 valid_mask = df[feature].notna() & df[target].notna() X = df.loc[valid_mask, feature] y = df.loc[valid_mask, target] # 分箱 - 使用DataFrame保持列名一致性 X_df = X.to_frame(name=feature) binner = OptimalBinning(method=method, max_n_bins=n_bins, verbose=False) binner.fit(X_df, y.values) bins = binner.transform(X_df, metric='indices') # 计算分箱统计 bin_labels = None if feature in binner.bin_tables_: bin_table = binner.bin_tables_[feature] if '分箱标签' in bin_table.columns: bin_labels = bin_table['分箱标签'].tolist() stats_df = compute_bin_stats(bins[feature].values, y.values, target_type='binary', bin_labels=bin_labels) return stats_df
[文档] def monotonicity_check(df: pd.DataFrame, feature: str, target: str, n_bins: int = 10) -> Dict[str, Union[str, float]]: """单调性检验. 检查特征分箱后的逾期率是否单调变化 :param df: 输入数据 :param feature: 特征名 :param target: 目标变量名 :param n_bins: 分箱数 :return: 单调性检验结果 **参考样例** >>> result = monotonicity_check(df, 'score', 'fpd15') >>> print(f"单调性: {result['单调性']}, 相关系数: {result['Spearman相关系数']}") """ validate_dataframe(df, required_cols=[feature, target]) validate_binary_target(df[target]) # 获取分箱统计 bin_stats = binning_bad_rate(df, feature, target, n_bins) if len(bin_stats) < 3: return { '特征名': feature, '单调性': '无法判断', '单调方向': '-', 'Spearman相关系数': np.nan, '说明': '分箱数太少', } # 计算单调性 bad_rates = bin_stats['坏样本率'].values x = np.arange(len(bad_rates)) # Spearman相关系数 from scipy import stats spearman_corr, pvalue = stats.spearmanr(x, bad_rates) # 判断单调性 if abs(spearman_corr) < 0.5: monotonicity = '非单调' direction = '-' elif spearman_corr > 0: monotonicity = '单调' direction = '递增' else: monotonicity = '单调' direction = '递减' return { '特征名': feature, '单调性': monotonicity, '单调方向': direction, 'Spearman相关系数': round(spearman_corr, 4), 'P值': round(pvalue, 4), '说明': '单调性良好' if abs(spearman_corr) >= 0.8 else '需检查业务含义', }
[文档] def univariate_auc(df: pd.DataFrame, feature: str, target: str) -> Dict[str, Union[str, float]]: """单变量AUC分析. :param df: 输入数据 :param feature: 特征名 :param target: 目标变量名 :return: AUC分析结果 **参考样例** >>> result = univariate_auc(df, 'score', 'fpd15') >>> print(f"AUC: {result['AUC值']}, 区分能力: {result['区分能力']}") """ validate_dataframe(df, required_cols=[feature, target]) validate_binary_target(df[target]) # 复用metrics模块 from ..metrics import auc # 数据清洗 valid_mask = df[feature].notna() & df[target].notna() X = df.loc[valid_mask, feature] y = df.loc[valid_mask, target] # 计算AUC auc_value = auc(y.values, X.values) # 评级 if auc_value < 0.5: level = '无区分能力(倒置)' elif auc_value < 0.6: level = '弱区分能力' elif auc_value < 0.7: level = '中等区分能力' elif auc_value < 0.8: level = '良好区分能力' elif auc_value < 0.9: level = '强区分能力' else: level = '极强区分能力' return { '特征名': feature, 'AUC值': round(auc_value, 4), '区分能力': level, }
[文档] def feature_importance_ranking(df: pd.DataFrame, features: List[str], target: str, metrics: List[str] = ['iv', 'auc'], n_jobs=-1, parallel_backend=None, parallel_config=None) -> pd.DataFrame: """综合特征重要性排序. 综合IV和AUC等多个指标评估特征重要性 :param df: 输入数据 :param features: 特征列表 :param target: 目标变量名 :param metrics: 评估指标列表 :return: 特征重要性DataFrame **参考样例** >>> ranking = feature_importance_ranking(df, feature_list, 'fpd15') >>> print(ranking[['特征名', 'IV值', 'AUC值', '综合得分', '排名']]) """ validate_dataframe(df, required_cols=[target]) valid_features = [feature for feature in features if feature in df.columns] def iter_tasks(): for feature in valid_features: yield (df.loc[:, [feature, target]], feature, target, tuple(metrics)) results = parallel_execute( _feature_importance_worker, iter_tasks(), n_jobs=n_jobs, parallel_backend=parallel_backend, parallel_config=parallel_config, task_labels=valid_features, default_backend="loky", has_parallel_children=False, workload=_eda_workload( df.loc[:, list(dict.fromkeys(valid_features + [target]))], len(valid_features), operation="特征重要性排名", cost_per_item=16.0 if "iv" in metrics else 4.0, capability="process_safe", ), ) result_df = pd.DataFrame(results) # 计算综合得分(标准化后的平均值) score_cols = [] if 'IV值' in result_df.columns: result_df['IV得分'] = (result_df['IV值'] / result_df['IV值'].max()).fillna(0) score_cols.append('IV得分') if 'AUC值' in result_df.columns: # AUC需要处理倒置情况(AUC<0.5) auc_norm = result_df['AUC值'].apply(lambda x: max(x, 1-x)) result_df['AUC得分'] = (auc_norm / auc_norm.max()).fillna(0) score_cols.append('AUC得分') if score_cols: result_df['综合得分'] = result_df[score_cols].mean(axis=1).round(4) result_df = result_df.sort_values('综合得分', ascending=False) result_df['排名'] = range(1, len(result_df) + 1) return result_df.reset_index(drop=True)