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)。