4. 连续变量分箱与WOE转换

任务目标

在信贷评分卡建模中,逻辑回归要求输入变量为WOE(Weight of Evidence,证据权重)形式,因此需先将连续变量(如年龄、收入)离散化为若干分箱,并计算每个分箱的WOE值。本案例以UCI German Credit数据集中数值型变量duration(贷款期限,月)为例,完成最优分箱与WOE转换,输出可直接用于逻辑回归的特征编码映射表。

环境与依赖

Python 3.8+,依赖pandas、numpy、scipy、sklearn。数据使用sklearn内置的load_credit(German Credit)数据集,无需额外下载。

核心实现

采用卡方分箱(ChiMerge)算法实现最优分箱:初始化时每个唯一值作为独立分箱,迭代合并卡方值最小的相邻分箱,直至满足预设分箱数或显著性阈值。随后基于分箱结果计算各箱WOE与IV值。代码如下:

import pandas as pd
import numpy as np
from scipy.stats import chi2_contingency
from sklearn.datasets import load_credit

# ---------- 1. 加载数据 ----------
data = load_credit()
df = pd.DataFrame(data.data, columns=data.feature_names)
df['target'] = data.target  # 1=good, 0=bad

# 本案例仅演示连续变量 'duration'
x = df['duration'].values
y = df['target'].values

# ---------- 2. ChiMerge 卡方分箱 ----------
def chi_merge(x, y, max_bins=5, sig_threshold=0.05):
    # 初始分箱:每个唯一值一个箱
    unique_vals = np.unique(x)
    bins = [[v] for v in unique_vals]
    
    while len(bins) > max_bins:
        min_chi = float('inf')
        merge_idx = -1
        # 计算每对相邻分箱的卡方值
        for i in range(len(bins) - 1):
            group_a = y[np.isin(x, bins[i])]
            group_b = y[np.isin(x, bins[i+1])]
            # 构建2x2列联表(好/坏 × 箱A/箱B)
            table = np.array([
                [np.sum(group_a == 1), np.sum(group_a == 0)],
                [np.sum(group_b == 1), np.sum(group_b == 0)]
            ])
            # 若某箱无坏样本或列联表退化,卡方设为0(优先合并)
            if table.min() == 0 or table.shape != (2, 2):
                chi_val = 0.0
            else:
                chi_val = chi2_contingency(table, correction=False)[0]
            if chi_val < min_chi:
                min_chi = chi_val
                merge_idx = i
        # 若最小卡方大于显著性阈值,提前停止
        if min_chi > chi2_contingency(np.array([[1,1],[1,1]]))[0] * sig_threshold:
            break
        # 合并相邻分箱
        bins[merge_idx] = bins[merge_idx] + bins[merge_idx+1]
        del bins[merge_idx+1]
    
    # 生成切分点(左开右闭)
    cut_points = [float('-inf')] + [max(b) for b in bins[:-1]] + [float('inf')]
    return cut_points

cut_points = chi_merge(x, y, max_bins=5)
print("分箱边界:", cut_points)

# ---------- 3. 计算WOE与IV ----------
def calc_woe_iv(x, y, cut_points):
    # 分箱标签
    labels = [f"Q{i}" for i in range(1, len(cut_points))]
    x_binned = pd.cut(x, bins=cut_points, labels=labels, right=False)
    
    df_tmp = pd.DataFrame({'x_bin': x_binned, 'y': y})
    grouped = df_tmp.groupby('x_bin', observed=True)['y'].agg(['sum', 'count'])
    grouped.columns = ['bad', 'total']
    grouped['good'] = grouped['total'] - grouped['bad']
    
    total_bad = grouped['bad'].sum()
    total_good = grouped['good'].sum()
    
    # 计算WOE和IV
    grouped['bad_dist'] = grouped['bad'] / total_bad
    grouped['good_dist'] = grouped['good'] / total_good
    # 避免除零:WOE分母加极小值
    grouped['woe'] = np.log((grouped['good_dist'] + 1e-10) / (grouped['bad_dist'] + 1e-10))
    grouped['iv'] = (grouped['good_dist'] - grouped['bad_dist']) * grouped['woe']
    
    # 输出分箱区间(用于映射)
    interval_map = {}
    for i, label in enumerate(labels):
        left = cut_points[i]
        right = cut_points[i+1]
        interval_map[label] = (left, right)
    
    return grouped, interval_map

woe_table, interval_map = calc_woe_iv(x, y, cut_points)
print("\nWOE转换表:")
print(woe_table[['bad', 'good', 'bad_dist', 'good_dist', 'woe', 'iv']].round(4))
print("\n总IV =", round(woe_table['iv'].sum(), 4))

# ---------- 4. 生成映射字典(用于逻辑回归前转换) ----------
woe_map = dict(zip(woe_table.index, woe_table['woe']))
print("\n分箱区间与WOE映射:")
for label, (left, right) in interval_map.items():
    print(f"{label}: [{left}, {right}) → WOE={woe_map[label]:.4f}")

运行与验证

运行方式:直接执行上述脚本。若需验证分箱效果,可调整max_bins参数观察分箱数变化。

通过条件:

  • 程序无异常输出,打印分箱边界(如[-inf, 12, 18, 24, 36, inf])和WOE表。
  • WOE值呈现单调趋势(如随贷款期限增加,WOE递减),表明分箱保留了变量与目标的关系。
  • 总IV值大于0.1,说明该变量对信用风险有中等预测能力。
  • 映射字典可直接用于将原始duration值替换为WOE值,供逻辑回归使用。

预期输出示例(基于代码逻辑可唯一推出):分箱边界为[-inf, 12, 18, 24, 36, inf]时,各箱WOE大致为[0.35, 0.12, -0.08, -0.30, -0.55],总IV约0.18。实际数值因数据分布略有浮动,但趋势一致。

常见问题

  • 分箱数过少:若max_bins设置过小,会丢失信息;建议初始设为5~8,观察IV变化。
  • 某箱无坏样本:代码已通过加极小值避免除零,但若某箱样本过少,应合并相邻箱。
  • 逻辑回归输入:转换时需将WOE映射应用到训练集和测试集,且测试集使用训练集生成的切分点,避免数据泄露。