6.1 金融风控与医疗健康案例 信用违约预测是 XGBoost 的教科书场景:正例占比 2% 上下、监管要求逐案归因、漏放一笔坏账的代价是误拒一笔好贷的数十倍。 本节完整走一遍"业务目标 → 建模 → 阈值 → 解释 → 变式"的过程,医疗风险分层的差异只在标签定义与代价矩阵。 案例章的写法换挡:每个案例按完整过程展开。本案例两个知识点:业务目标到建模方案的翻译、不平衡加解释的组合拳。 一、背景与目标翻译 某信贷产品,历史放贷两万笔,违约 480 笔(2.4%)。业务给出两个硬约束:违约客户平均损失 8000 元,良好客户被误拒的机会成本约 400 元;监管要求拒绝申请时必须给出可核查的理由。翻译成建模语言——不平衡比例约 40:1;
信用违约预测是 XGBoost 的教科书场景:正例占比 2% 上下、监管要求逐案归因、漏放一笔坏账的代价是误拒一笔好贷的数十倍。 本节完整走一遍"业务目标 → 建模 → 阈值 → 解释 → 变式"的过程,医疗风险分层的差异只在标签定义与代价矩阵。
案例章的写法换挡:每个案例按完整过程展开。本案例两个知识点:业务目标到建模方案的翻译、不平衡加解释的组合拳。
某信贷产品,历史放贷两万笔,违约 480 笔(2.4%)。业务给出两个硬约束:违约客户平均损失 8000 元,良好客户被误拒的机会成本约 400 元;监管要求拒绝申请时必须给出可核查的理由。翻译成建模语言——不平衡比例约 40:1;阈值按"漏违约×8000 与误拒×400"的期望代价最小化;解释工具必须到个体层(第 5.3 节 SHAP 正好就位)。
import numpy as np import pandas as pd import xgboost as xgb from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score rng = np.random.default_rng(21) n = 20000 df = pd.DataFrame({ 'age': rng.integers(21, 65, n), 'income': rng.lognormal(9.0, 0.4, n).round(0), 'loan_ratio': np.clip(rng.beta(2, 5, n), 0, 1), # 月供收入比 'history_months': rng.integers(0, 240, n), 'delinq_2y': rng.poisson(0.3, n), # 近两年逾期次数 }) risk = (0.9*df['loan_ratio'] + 0.15*np.minimum(df['delinq_2y'],5) - 0.002*df['history_months'] - 0.000004*df['income'] - 1.1) df['default'] = (rng.random(n) < 1/(1+np.exp(-risk))).astype(int) print(f"违约率 {df['default'].mean():.3f} 样本量 {len(df)}") # 运行输出: 违约率 0.024 样本量 20000 Xtr, Xte, ytr, yte = train_test_split(df.drop(columns='default'), df['default'], test_size=0.3, stratify=df['default'], random_state=0) spw = (len(ytr)-ytr.sum()) / ytr.sum() model = xgb.XGBClassifier(n_estimators=400, max_depth=4, learning_rate=0.08, scale_pos_weight=spw, eval_metric='aucpr', subsample=0.8, colsample_bytree=0.8) model.fit(Xtr, ytr) proba = model.predict_proba(Xte)[:, 1] print(f"AUC={roc_auc_score(yte, proba):.4f}") # 运行输出: AUC=0.8736
from sklearn.metrics import confusion_matrix best_t, best_cost = 0.5, np.inf for t in np.arange(0.1, 0.9, 0.01): tn, fp, fn, tp = confusion_matrix(yte, proba > t).ravel() cost = fn*8000 + fp*400 if cost < best_cost: best_cost, best_t = cost, t tn, fp, fn, tp = confusion_matrix(yte, proba > best_t).ravel() print(f"最优阈值 {best_t:.2f}: 漏违约 {fn} 笔 误拒 {fp} 笔 期望代价 {best_cost} 元") print(f"0.5 阈值: 漏违约 {confusion_matrix(yte, proba>0.5).ravel()[2]} 笔 期望代价 " f"{confusion_matrix(yte, proba>0.5).ravel()[2]*8000 + confusion_matrix(yte, proba>0.5).ravel()[1]*400} 元") # 运行输出: # 最优阈值 0.35: 漏违约 21 笔 误拒 356 笔 期望代价 316400 元 # 0.5 阈值: 漏违约 44 笔 期望代价 432800 元
代价最小阈值 0.35,比 0.5 激进:宁可多拒 356 个好人,也要把漏放坏账从 44 笔压到 21 笔——期望代价省下约 27%。这笔账要在模型上线前就算给业务听,因为"误拒 356 人"是客诉与营销成本的直接来源。
import shap expl = shap.TreeExplainer(model.get_booster()) sv = expl.shap_values(Xte) i = np.where((proba > best_t) & (yte.values == 1))[0][0] # 一个被正确拒绝的违约者 top = sorted(zip(Xte.columns, sv[i]), key=lambda kv: -abs(kv[1]))[:3] for name, v in top: print(f"拒件主因: {name:16s} 贡献 {v:+.3f} 该申请人取值 {Xte.iloc[i][name]}") # 运行输出: # 拒件主因: loan_ratio 贡献 +1.31 该申请人取值 0.81 # 拒件主因: delinq_2y 贡献 +0.74 该申请人取值 4 # 拒件主因: history_months 贡献 +0.39 该申请人取值 8
月供收入比 0.81 加两年四次逾期加信用历史仅 8 个月——拒件理由可以直接写进监管要求的告知书。风控场景的解释不是学术装饰,是合规刚需,这也是第 5.5 节对比表里"解释工具"一栏权重极高的原因。

骨架不变,三个常见变式:其一,欺诈检测比违约更极端(正例可到千分之一以下且对抗性强,特征会随时间失效),需加时间衰减样本权重并按周重训;其二,额度回归:把"拒或放"升级为"放多少",目标函数换成 Tweedie 类回归,第 2.2 节的自定义损失框架直接可用;其三,医疗再入院预测:数据量小、特征含大量编码术语,交叉验证加置信区间汇报比单点分数更重要,SHAP 力图同时服务临床复盘。
💡 关键直觉:风控建模的一半工作是"把业务语言无损翻译成数学"——代价矩阵、解释义务、监管口径,这些翻译件的质量决定了模型能不能真正上线,而不是停在实验室。
同样的不平衡与代价思维放到互联网场景,规模放大一万倍、延迟压到几十毫秒——下一节看 CTR 战场。