Python机器学习实战:用Scikit-learn从0构建信用风险评分模型(含WOE编码+AUC/KS/PSI评估)

0 阅读5分钟

Python机器学习实战:用Scikit-learn从0构建信用风险评分模型

在银行、消费金融、互联网信贷等领域,信用风险评分模型(Credit Scoring Model)是核心业务模型之一。它决定了一个用户是否能拿到贷款、能拿多少、利率是多少。

本文用Python + Scikit-learn,从零搭建一个完整的信用评分模型,包括:数据预处理、特征工程、模型训练(逻辑回归+随机森林+XGBoost对比)、模型评估(AUC/KS/PSI)、以及最终的WOE编码标准评分卡生成。

代码全部可运行,适合有Python基础 + 想入门风控建模的同学。


一、数据准备与探索性分析

我们使用经典的 German Credit Dataset(德国信用数据集),这是机器学习领域最常用的信用风险公开数据集,包含1000条样本、20个特征和1个二分类标签(1=好客户,2=坏客户)。


import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')

# 加载数据(使用UCI German Credit Dataset)
from sklearn.datasets import fetch_openml

# 方法1:直接从sklearn加载(需要网络)
# data = fetch_openml('credit-g', version=1, as_frame=True)
# df = data.frame

# 方法2:手动创建模拟数据(离线可运行)
np.random.seed(42)
n = 1000

df = pd.DataFrame({
    'age': np.random.randint(18, 75, n),
    'job': np.random.choice([0, 1, 2, 3], n),  # 0=无技能失业, 1=无技能受雇, 2=技能工人, 3=高技能
    'housing': np.random.choice(['own', 'free', 'rent'], n),
    'saving_accts': np.random.choice(['little', 'moderate', 'quite rich', 'rich', 'NA'], n),
    'checking_acct': np.random.choice(['little', 'moderate', 'rich', 'NA'], n),
    'credit_amount': np.random.exponential(3000, n).astype(int) + 500,
    'duration': np.random.randint(4, 72, n),  # 月
    'purpose': np.random.choice(['car', 'furniture', 'radio/TV', 'education', 'business', 'repairs'], n),
    'risk': np.random.choice([0, 1], n, p=[0.3, 0.7])  # 0=坏客户, 1=好客户
})

print("数据集形状:", df.shape)
print("\n目标变量分布:")
print(df['risk'].value_counts(normalize=True).round(4))
print("\n数值特征统计:")
print(df[['age', 'credit_amount', 'duration']].describe())

输出结果:


数据集形状: (1000, 9)

目标变量分布:
1    0.7
0    0.3

数值特征统计:
              age  credit_amount    duration
count  1000.000000    1000.000000  1000.000000
mean     46.507000    3530.278000    38.108000
std      16.283975    3044.897891    18.869327
min      18.000000     500.000000     4.000000
25%      32.000000    1261.000000    21.000000
50%      47.000000    2581.000000    38.000000
75%      61.000000    5007.000000    54.000000
max      74.000000   29943.000000    71.000000

二、特征工程:WOE编码(Weight of Evidence)

WOE编码是信用评分领域最常用的特征转换方法。它的核心思路是:把每个特征的每个值,转换成"这个值的坏账率相对于全局坏账率的比率"的对数形式。

WOE公式:WOE = ln(坏客户占比 / 好客户占比)


class WOEEncoder:
    """
    WOE(Weight of Evidence)编码器
    适用于二分类信用评分模型
    """
    
    def __init__(self, bins=5, min_samples=50):
        self.bins = bins
        self.min_samples = min_samples
        self.woe_dict = {}
        self.iv_dict = {}
    
    def _calc_woe_iv(self, x, y, categorical=False):
        """计算单个特征的WOE和IV值"""
        df = pd.DataFrame({'x': x, 'y': y})
        
        if categorical:
            grouped = df.groupby('x')['y'].agg(['sum', 'count'])
            grouped.columns = ['good', 'total']
        else:
            # 数值型:分箱处理
            df['x_bin'] = pd.qcut(df['x'], q=self.bins, duplicates='drop')
            grouped = df.groupby('x_bin')['y'].agg(['sum', 'count'])
            grouped.columns = ['good', 'total']
        
        grouped['bad'] = grouped['total'] - grouped['good']
        grouped['good_rate'] = grouped['good'] / grouped['good'].sum()
        grouped['bad_rate'] = grouped['bad'] / grouped['bad'].sum()
        
        # 避免除以0
        grouped['good_rate'] = grouped['good_rate'].replace(0, 0.0001)
        grouped['bad_rate'] = grouped['bad_rate'].replace(0, 0.0001)
        
        grouped['woe'] = np.log(grouped['good_rate'] / grouped['bad_rate'])
        grouped['iv'] = (grouped['good_rate'] - grouped['bad_rate']) * grouped['woe']
        
        return grouped['woe'].to_dict(), grouped['iv'].sum()
    
    def fit(self, X, y, categorical_cols=None):
        """拟合WOE编码器"""
        categorical_cols = categorical_cols or []
        
        for col in X.columns:
            is_cat = col in categorical_cols
            woe_map, iv = self._calc_woe_iv(X[col], y, categorical=is_cat)
            self.woe_dict[col] = woe_map
            self.iv_dict[col] = iv
        
        return self
    
    def get_iv_report(self):
        """获取各特征IV值报告"""
        iv_df = pd.DataFrame({
            'feature': list(self.iv_dict.keys()),
            'iv': list(self.iv_dict.values())
        }).sort_values('iv', ascending=False)
        
        # IV值判断标准
        def iv_judgment(iv):
            if iv 0.2 有价值,>0.3 良好,>0.4"""
    fpr, tpr, thresholds = roc_curve(y_true, y_prob)
    ks = max(tpr - fpr)
    return ks

def calc_psi(expected, actual, bins=10):
    """
    计算PSI(Population Stability Index,群体稳定性指数)
    PSI  0.25: 显著变化,需重建模型
    """
    expected_pct, _ = np.histogram(expected, bins=bins, density=True)
    actual_pct, _ = np.histogram(actual, bins=bins, density=True)
    
    # 避免0值
    expected_pct = np.where(expected_pct == 0, 0.0001, expected_pct)
    actual_pct = np.where(actual_pct == 0, 0.0001, actual_pct)
    
    # 归一化
    expected_pct = expected_pct / expected_pct.sum()
    actual_pct = actual_pct / actual_pct.sum()
    
    psi = np.sum((actual_pct - expected_pct) * np.log(actual_pct / expected_pct))
    return psi

# 选择最优模型评估(以XGBoost为例)
best_pred = xgb_pred_proba

ks_value = calc_ks(y_test, best_pred)

# PSI:用训练集分数 vs 测试集分数模拟
train_pred = xgb_model.predict_proba(X_train)[:, 1]
psi_value = calc_psi(train_pred, best_pred)

print("="*55)
print("XGBoost 模型评估报告")
print("="*55)
print(f"AUC:{xgb_auc:.4f}   (判断标准:>0.7 有效,>0.8 优秀)")
print(f"KS :{ks_value:.4f}   (判断标准:>0.2 有价值,>0.3 良好)")
print(f"PSI:{psi_value:.4f}   (判断标准:= 650:
            risk_level = "低风险"
            suggestion = "建议通过,可提供标准额度"
        elif score >= 580:
            risk_level = "中风险"
            suggestion = "建议小额通过,加强贷后监控"
        elif score >= 480:
            risk_level = "高风险"
            suggestion = "建议拒绝或需补充征信材料"
        else:
            risk_level = "极高风险"
            suggestion = "建议直接拒绝"
        
        return {
            'score': int(score),
            'probability': round(float(prob), 4),
            'risk_level': risk_level,
            'suggestion': suggestion
        }

# 初始化评分模型
scoring_model = CreditScoringModel(
    model=xgb_model,
    feature_cols=X_processed.columns.tolist(),
    categorical_cols=categorical_cols
)
scoring_model.fit_encoders(X)

# 测试预测
sample_applicants = [
    {
        'age': 35, 'job': 2, 'housing': 'own', 
        'saving_accts': 'moderate', 'checking_acct': 'little',
        'credit_amount': 5000, 'duration': 24, 'purpose': 'car'
    },
    {
        'age': 22, 'job': 0, 'housing': 'rent',
        'saving_accts': 'little', 'checking_acct': 'NA',
        'credit_amount': 15000, 'duration': 60, 'purpose': 'education'
    }
]

print("="*60)
print("实时评分结果:")
print("="*60)
for i, applicant in enumerate(sample_applicants):
    result = scoring_model.predict_score(applicant)
    print(f"\n申请人 {i+1}:")
    print(f"  年龄:{applicant['age']}岁 | 额度:{applicant['credit_amount']}元 | 期限:{applicant['duration']}月")
    print(f"  评分:{result['score']}分")
    print(f"  好客户概率:{result['probability']:.2%}")
    print(f"  风险等级:{result['risk_level']}")
    print(f"  建议:{result['suggestion']}")

输出示例:


============================================================
实时评分结果:
============================================================

申请人 1:
  年龄:35岁 | 额度:5000元 | 期限:24月
  评分:642分
  好客户概率:62.50%
  风险等级:中风险
  建议:建议小额通过,加强贷后监控

申请人 2:
  年龄:22岁 | 额度:15000元 | 期限:60月
  评分:521分
  好客户概率:38.20%
  风险等级:高风险
  建议:建议拒绝或需补充征信材料

七、总结与延伸

本文覆盖了信用评分模型的完整流程:

① 数据探索:理解目标变量分布、特征类型

② WOE编码:信用模型特有的特征转换,核心是IV值筛选

③ 多模型对比:逻辑回归(可解释)vs 随机森林 vs XGBoost

④ 风控评估指标:AUC + KS + PSI,三个缺一不可

⑤ 评分卡转换:把概率转成300-850的标准分制

⑥ 实时预测封装:工程化部署的基础

进阶方向:

  • 引入征信局数据(芝麻分、百行征信)

  • 时序特征工程(近30天/90天行为特征)

  • 模型监控与在线学习

  • SHAP值解释模型输出

有问题欢迎评论区交流,代码已测试可运行(需安装:pandas、sklearn、xgboost、numpy)。