Skip to content

WOE 编码 + IV 筛选——将原始特征转换为单调风险信号的信用评分标准化工序

核心理念

WOE(证据权重)编码将任意特征(连续或离散)转换为与目标变量具有单调关系的标准化风险信号。IV(信息价值)量化每个特征对违约预测的贡献,用于特征筛选。加上 Logistic 回归 + 评分刻度转换 = 完整的信用评分卡流水线。

操作步骤

步骤 1:WOE 编码——将特征映射到对数几率空间

WOE 的核心公式: $\(\text{WOE}_i = \ln\left(\frac{\text{Good}_i / \text{Total Good}}{\text{Bad}_i / \text{Total Bad}}\right) = \ln\left(\frac{\text{该组好客户占比}}{\text{该组坏客户占比}}\right)\)$

WOE 的三种状态: - WOE > 0 → 该组「好客户」比例高于平均(低风险组) - WOE < 0 → 该组「坏客户」比例高于平均(高风险组) - WOE = 0 → 该组风险与平均水平相同

import pandas as pd
import numpy as np

def calculate_woe(df, feature, target, n_bins=10):
    """计算单个特征的 WOE 编码"""
    df_temp = df[[feature, target]].copy()

    # 等频分箱
    df_temp['bin'] = pd.qcut(df_temp[feature], q=n_bins, duplicates='drop')

    # 分箱统计
    bin_stats = df_temp.groupby('bin').agg(
        good=(target, lambda x: (x == 0).sum()),
        bad=(target, lambda x: (x == 1).sum()),
        total=(target, 'count')
    ).reset_index()

    total_good = bin_stats['good'].sum()
    total_bad = bin_stats['bad'].sum()

    # WOE 计算
    bin_stats['pct_good'] = bin_stats['good'] / total_good
    bin_stats['pct_bad'] = bin_stats['bad'] / total_bad
    bin_stats['woe'] = np.log(
        (bin_stats['pct_good'] + 1e-10) / (bin_stats['pct_bad'] + 1e-10)
    )

    # 事件率
    bin_stats['event_rate'] = bin_stats['bad'] / bin_stats['total']

    return bin_stats

WOE 的核心价值: 1. 处理缺失值:缺失值自动成为独立分箱(有专属 WOE 值) 2. 处理异常值:极端值被分到边缘箱中,不影响模型 3. 处理非线性:单调性由 WOE 值保证,不需要对原始特征做非线性变换 4. 标准化不同尺度:所有特征映射到同一对数几率空间

步骤 2:IV 筛选——量化特征预测能力

\[\text{IV} = \sum_i (\text{pct\_good}_i - \text{pct\_bad}_i) \times \text{WOE}_i\]

IV 评估标准(行业标准):

IV 范围 预测能力 使用建议
< 0.02 直接丢弃
0.02 - 0.1 作为辅助特征
0.1 - 0.3 中等 核心特征
0.3 - 0.5 强特征,需警惕过拟合
> 0.5 过强 检查是否特征泄露
def calculate_iv(bin_stats):
    """计算单个特征的 IV 值"""
    total_good = bin_stats['good'].sum()
    total_bad = bin_stats['bad'].sum()

    bin_stats = bin_stats.copy()
    bin_stats['pct_good'] = bin_stats['good'] / total_good
    bin_stats['pct_bad'] = bin_stats['bad'] / total_bad
    bin_stats['woe'] = np.log(
        (bin_stats['pct_good'] + 1e-10) / (bin_stats['pct_bad'] + 1e-10)
    )

    bin_stats['iv_component'] = (
        (bin_stats['pct_good'] - bin_stats['pct_bad']) * bin_stats['woe']
    )
    iv = bin_stats['iv_component'].sum()

    return iv

# 批量计算所有特征的 IV
def batch_iv_analysis(df, features, target, n_bins=10):
    """批量计算所有特征的 IV 并排序"""
    results = []
    for feat in features:
        bin_stats = calculate_woe(df, feat, target, n_bins)
        iv = calculate_iv(bin_stats)
        results.append({'feature': feat, 'iv': iv})

    iv_df = pd.DataFrame(results).sort_values('iv', ascending=False)

    def get_power(iv):
        if iv < 0.02: return '无'
        elif iv < 0.1: return '弱'
        elif iv < 0.3: return '中等'
        elif iv < 0.5: return '强'
        else: return '过强'

    iv_df['predictive_power'] = iv_df['iv'].apply(get_power)
    return iv_df

步骤 3:WOE 编码应用——将原始特征转换为 WOE 值

def apply_woe_encoding(df, feature, woe_stats):
    """
    将数据框中的原始特征替换为 WOE 编码值
    """
    df_encoded = df.copy()

    # 为每行找到对应分箱的 WOE 值
    bin_map = woe_stats.set_index('bin')['woe'].to_dict()

    # 等频分箱(使用训练集的分界点)
    df_encoded[f'{feature}_bin'] = pd.qcut(
        df_encoded[feature], 
        q=len(woe_stats), 
        duplicates='drop'
    )

    df_encoded[f'{feature}_woe'] = df_encoded[f'{feature}_bin'].map(bin_map)

    # 未知箱 → WOE = 0(即该组与平均水平相同)
    df_encoded[f'{feature}_woe'] = df_encoded[f'{feature}_woe'].fillna(0)

    return df_encoded[f'{feature}_woe']

步骤 4:评分刻度转换——从概率到信用评分

业界标准评分转换:

\[\text{Score} = \text{Offset} - \text{Slope} \times \ln(\text{Odds})\]

其中 \(\text{Odds} = p / (1-p)\) 是违约/正常的几率。

PDO(Points to Double the Odds):几率翻倍时分数减少的分数。业界标准 PDO = 50。

# 评分刻度参数
base_score = 600  # 基础分(该分数对应的 odds)
pdo = 50          # 几率翻倍时分数变化
base_odds = 1/60  # 基准几率(600 分时好:坏 = 60:1)

# 计算 Slope 和 Offset
slope = pdo / np.log(2)
offset = base_score + slope * np.log(base_odds)

def probability_to_score(prob, slope=slope, offset=offset):
    """将违约概率转换为信用评分"""
    odds = prob / (1 - prob + 1e-10)
    score = offset - slope * np.log(odds + 1e-10)
    return np.clip(score, 300, 900)  # 截断到合理范围

评分刻度的含义: - 分数越高 = 违约风险越低 = 好客户 - PDO = 50 意味着:如果 A 的分数比 B 高 50 分,A 的违约几率是 B 的一半

步骤 5:决策阈值设定

# 三区决策规则
approve_threshold = 650  # 高于此分直接批准
reject_threshold = 550   # 低于此分直接拒绝
# 550-650 之间 → 人工复核

def make_decision(score, approve=650, reject=550):
    if score >= approve:
        return '批准'
    elif score < reject:
        return '拒绝'
    else:
        return '人工复核'

# 阈值设定的业务逻辑:
# 提高批准阈值 → 降低坏账率但减少业务量
# 降低批准阈值 → 增加业务量但提高坏账率
# 最优阈值 = 边际收益 = 边际成本 的点

💡 关键洞察

WOE 为什么替代 One-Hot 编码

在信用评分中,WOE 编码在所有方面优于 One-Hot: - 维度:每个特征 → 1 列 WOE 值(One-Hot 需要 n_bins 列) - 可解释性:WOE 值直接反映风险方向(正 = 低风险,负 = 高风险) - 单调性:Logistic 回归在 WOE 编码上学习单调模式 - 处理缺失:缺失自动为一个分箱

IV > 0.5 的警告

当 IV > 0.5 时,不是说明特征「特别好」——而是说明可能存在特征泄露(如用「是否已逾期」预测「是否违约」——这是事后变量)。过强的 IV 是数据质量问题的信号。

评分卡的部署监控——PSI

评分卡部署后需要持续监控总体稳定性指数(PSI)

\[\text{PSI} = \sum_i (\text{actual\%}_i - \text{expected\%}_i) \times \ln\left(\frac{\text{actual\%}_i}{\text{expected\%}_i}\right)\]

PSI > 0.25 = 评分分布发生显著变化 → 模型需要重建。

限制与边界

  • 单调假设:WOE 要求特征与目标的关系是单调的——如果真实的 U 型关系存在(如中等收入群体违约率最高),WOE 会丢失这部分信息
  • Logistic 回归的前提:评分卡基于 Logit 模型,假设特征和对数几率之间是线性关系——这在 WOE 编码下通常成立
  • 等频分箱的敏感性:分箱数量影响 IV 值——n_bins 少 → IV 被低估,n_bins 多 → IV 被高估
  • 不适用于非线性交互:如果需要捕捉特征间的复杂交互,需要使用 GBDT 或神经网络——但那是黑箱模型,不符合信用评分的监管可解释性要求

与概念笔记的关联

  • → 概念笔记「度量选择是元决策选什么指标决定组织优化方向选错比没有更危险」——选择 IV 作为特征筛选指标本身就是度量选择:你在用「预测能力」而非「业务意义」做筛选。IV 是信息论度量(Kullback-Leibler 散度的变形),选择它而不是业务相关性度量(如「客户收入对违约的直觉理解」)决定了评分卡最终的特征组合
  • → 概念笔记「概率分布选择不是数学偏好」——等频分箱将连续特征离散化,本质上是选择了「分段常数」作为分布的近似形式。更关键的是,WOE 编码要求特征与目标之间是单调的——这本身就是对数据生成过程的强假设
  • → 概念笔记「机器学习失败金字塔算法选择是塔尖」——PSI 监控和 IV > 0.5 的泄漏警告是 ML 失败金字塔中「评估指标」和「数据质量」在信用评分场景的具体化。Pipeline 中的每一步(数据清洗→WOE分箱→IV筛选→Logistic回归→评分刻度)都有特定的失败模式
  • → 概念笔记「因果推断方法阵营分歧不在数学每种方法估计不同的因果量」——评分卡估计的是相关(区分好/坏客户),不是因果(降低利率对违约率的因果效应)。评分卡用于审批决策的前提假设是「被拒绝的申请人如果被批准,其行为与批准的相似申请人一样」——这个假设需要离线因果验证而不能被评分卡本身的 KS 值保证