简介:这份PDF文献面向金融风控从业者、数据科学学习者与高校研究人员,聚焦商业银行信用风险评估这一核心课题,系统梳理了机器学习方法在违约预测中的应用路径。资源为单份PDF文档,压缩包约1.5MB,内容完整、便于检索与引用,适合作为课程作业、论文写作或风控建模的参考文献。文中以西南财经大学“新网银行杯”竞赛脱敏数据为实证基础,围绕高维稀疏、无标签样本与好坏样本不平衡等难点,依次展开缺失值分析与填充、特征选取、模型训练与评估等环节,并对比Logistic回归、决策树、Adaboost、GradientBoosting与LGB等模型的AUC表现,最终得出LGB指标最优的结论。读者可借此掌握从数据清洗到模型比对的完整建模思路,理解AUC评估与半监督方法的应用要点,为信用风险预测实践提供可复用的方法参考。目前已有538人学习。
1. 信用风险预测模型:从 157 维脱敏数据到 0.7887 AUC 的完整复现路径
银行信贷场景里,坏样本占比往往低得让人头疼。这份来自新网银行杯竞赛的脱敏数据集,15000 条带标签训练样本中高风险客户只有 691 条,正负比达到 21:1,157 维特征全部匿名且缺失值统一用 -99 填充。更棘手的是,数据里还混着 10000 条无标签样本和 10000 条测试集,客群变量 cust_group 被明令禁止入模。就是在这样的条件下,原作者用 Logistic 回归、决策树、Adaboost、GradientBoosting 和 LGB 五类模型跑出了一套完整方案,最终 LGB 拿到加权 AUC 0.7887。这篇笔记拆的就是这份 PDF 里的技术路线——数据怎么洗、特征怎么选、模型怎么调、坑在哪。如果你正在做信贷风控建模、半监督学习落地,或者只是想找一个真实脱敏数据练手,这份材料值得跟着走一遍。
2. 数据解析与缺失值处理:-99 不是数值,是信号
2.1 三个文件的分工与标签分布
竞赛给的数据分三个文件,角色完全不同。train_xy.csv 是带标签训练集,15000 条,含 y 字段;train_x.csv 是无标签训练集,10000 条,除 y 外字段与 train_xy 一致;test_all.csv 是测试集,10000 条,同样无 y。特征维度 157,其中 x1 到 x95 是数值型,x96 到 x157 是类别型。标签 y 中 0 代表低风险,14309 条;1 代表高风险,691 条。cust_group 有三个取值,分布也不均衡,group0 有 14544 条低风险、456 条高风险,group1 有 2487 条低风险、129 条高风险,group2 有 3489 条低风险、106 条高风险。
这个分布意味着两件事:第一,不能直接用准确率做评估指标,否则全预测 0 就能拿 95% 以上;第二,分层抽样必须按 y 和 cust_group 联合分层,否则某折验证集里可能一个坏样本都没有。常见做法是用StratifiedKFold对 y 分层,但如果要兼顾客群分布,可以构造一个组合标签y * 10 + cust_group再做分层。
2.2 缺失值统计与 -99 的语义
原始数据里缺失值统一用 -99 表示,但 -99 本身是一个合法数值,直接送进模型会被当成真实值参与计算。所以第一步必须把 -99 替换成np.nan,再做缺失率统计。原文图 2 显示有大量特征缺失率高达 100%,这些列对模型没有任何信息量,应该直接删除。剩下有缺失的列,按缺失率分档处理。
import pandas as pd import numpy as np # 读取带标签训练集 train_xy = pd.read_csv('train_xy.csv') train_x = pd.read_csv('train_x.csv') test_all = pd.read_csv('test_all.csv') # 将 -99 替换为 NaN,注意只对特征列操作,不动 id 和 y feature_cols = [c for c in train_xy.columns if c.startswith('x_')] train_xy[feature_cols] = train_xy[feature_cols].replace(-99, np.nan) train_x[feature_cols] = train_x[feature_cols].replace(-99, np.nan) test_all[feature_cols] = test_all[feature_cols].replace(-99, np.nan) # 统计缺失率 missing_rate = train_xy[feature_cols].isnull().mean().sort_values(ascending=False) # 删除缺失率 100% 的列 drop_cols = missing_rate[missing_rate == 1.0].index.tolist() train_xy.drop(columns=drop_cols, inplace=True) train_x.drop(columns=drop_cols, inplace=True) test_all.drop(columns=drop_cols, inplace=True)这段代码的关键点在于:替换 -99 必须在划分特征列之后做,避免把 id 或 y 里的 -99 误伤;删除 100% 缺失列要在三个数据集上同步操作,保证特征维度一致。参数上,missing_rate == 1.0是硬阈值,实际比赛中也可以放宽到 0.95,但原文选择了最保守的做法。
2.3 数值型均值填充与类别型哑变量处理
原文对数值型特征用均值填充,对类别型特征引入哑元变量。这里有一个容易翻车的地方:均值填充必须在训练集上计算均值,然后应用到验证集和测试集,不能各自算各自的均值。否则验证集的信息会泄露到填充过程里,导致线下 AUC 虚高。
from sklearn.preprocessing import StandardScaler # 区分数值型和类别型 num_cols = [c for c in feature_cols if c.startswith('x_') and int(c.split('_')[1]) <= 95] cat_cols = [c for c in feature_cols if c.startswith('x_') and int(c.split('_')[1]) > 95] # 数值型:训练集均值填充,验证/测试集用训练集均值 num_means = train_xy[num_cols].mean() train_xy[num_cols] = train_xy[num_cols].fillna(num_means) train_x[num_cols] = train_x[num_cols].fillna(num_means) test_all[num_cols] = test_all[num_cols].fillna(num_means) # 类别型:引入哑元变量,缺失单独作为一类 for df in [train_xy, train_x, test_all]: for col in cat_cols: df[col] = df[col].fillna(-1).astype(int).astype(str) # 合并后做 one-hot,保证三个数据集类别对齐 combined = pd.concat([train_xy[cat_cols], train_x[cat_cols], test_all[cat_cols]], axis=0) combined_dummies = pd.get_dummies(combined, columns=cat_cols) train_xy_dummies = combined_dummies.iloc[:len(train_xy), :] train_x_dummies = combined_dummies.iloc[len(train_xy):len(train_xy)+len(train_x), :] test_dummies = combined_dummies.iloc[len(train_xy)+len(train_x):, :] # 归一化 scaler = StandardScaler() train_xy[num_cols] = scaler.fit_transform(train_xy[num_cols]) train_x[num_cols] = scaler.transform(train_x[num_cols]) test_all[num_cols] = scaler.transform(test_all[num_cols])逻辑说明:均值填充和归一化都遵循「训练集 fit,其他 transform」的原则。类别型特征先填充 -1 再转字符串,是为了让缺失值成为一个独立类别,而不是被忽略。one-hot 编码在合并数据上做,是为了保证三个数据集的哑变量列完全一致。参数上,fillna(-1)里的 -1 是任意选的非原始类别值,只要不与已有类别冲突即可。
注意:原文提到对填充完的数据进行归一化,但归一化只对数值型特征做,类别型 one-hot 后的 0/1 列不需要再标准化。
3. 特征选择与模型对比:为什么 157 维不降维反而更好
3.1 随机森林特征重要性排序与 TOP25 选取
原文用随机森林对 157 个特征做重要性排序,取 TOP25 作为候选特征。但后面又做了一个反直觉的决定:不降维,让模型自动选择特征。这个决策的逻辑是,157 维本身不算高维,而且特征是匿名的,降维后很难解释保留了哪些信息;树模型自带特征选择能力,强行降维反而可能丢掉弱信号。
from sklearn.ensemble import RandomForestClassifier # 只用带标签训练集做特征重要性 X_train = train_xy.drop(columns=['id', 'y', 'cust_group'], errors='ignore') y_train = train_xy['y'] rf = RandomForestClassifier(n_estimators=200, max_depth=8, random_state=42, n_jobs=-1) rf.fit(X_train, y_train) # 取 TOP25 importances = pd.Series(rf.feature_importances_, index=X_train.columns) top25 = importances.sort_values(ascending=False).head(25).index.tolist()参数上,n_estimators=200是经验值,再大边际收益递减;max_depth=8是为了防止单棵树过拟合,因为正负样本极度不平衡。random_state=42保证可复现。TOP25 的选取没有固定标准,原文取 25 是基于后续模型对比的实验结果,实际可以试 20、30、50 几个档位。
3.2 五类模型的 AUC 对比与降维/不降维差异
原文对比了 Logistic 回归、决策树、Adaboost、GradientBoosting 四类模型在降维和不降维两种条件下的表现。降维方式应该是用了某种特征选择或 PCA,但原文没有明确写具体方法,从结果表看,不降维的加权 AUC 普遍高于降维。以 GradientBoosting 为例,降维后加权 AUC 0.73528,不降维后 0.762。Logistic 回归降维后 0.7202,不降维后 0.741。这个差距说明匿名特征里存在非线性交互,线性降维会破坏这些结构。
| 模型 | 降维加权 AUC | 不降维加权 AUC |
|---|---|---|
| Logistic | 0.7202 | 0.741 |
| AdaBoost | 0.6896 | 0.708 |
| Decision Tree | 0.73327 | 0.762 |
| GradientBoosting | 0.73528 | 0.762 |
评估指标是加权 AUC:AUC = 0.3×AUC1 + 0.3×AUC2 + 0.4×AUC3,其中 AUC1、AUC2、AUC3 分别对应三个客群的 AUC。这个加权方式意味着 group3 的权重最高,达到 0.4,所以模型在 group3 上的表现对最终得分影响最大。从原文结果看,group3 的 AUC 普遍高于 group1 和 group2,说明 group3 的样本区分度更好。
3.3 过拟合现象与 5 折分层交叉验证
原文图 5 显示 Logistic 回归和 GradientBoosting 的 ROC 曲线都存在过拟合。过拟合的原因有两个:一是数据量小,15000 条带标签样本里只有 691 个正样本,模型很容易记住少数类;二是模型本身复杂度高,比如 GradientBoosting 在不限制深度时会把训练集拟合得很完美。
解决方法是 5 折分层交叉验证。分层的意思是每一折里正负样本比例与全集一致,避免某一折验证集里正样本太少导致 AUC 波动大。
from sklearn.model_selection import StratifiedKFold from sklearn.metrics import roc_auc_score import lightgbm as lgb skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) auc_scores = [] for train_idx, val_idx in skf.split(X_train, y_train): X_tr, X_val = X_train.iloc[train_idx], X_train.iloc[val_idx] y_tr, y_val = y_train.iloc[train_idx], y_train.iloc[val_idx] model = lgb.LGBMClassifier( n_estimators=500, learning_rate=0.05, num_leaves=31, subsample=0.8, colsample_bytree=0.8, random_state=42 ) model.fit(X_tr, y_tr) y_pred = model.predict_proba(X_val)[:, 1] auc_scores.append(roc_auc_score(y_val, y_pred)) print(f'5 折 AUC 均值: {np.mean(auc_scores):.4f}, 标准差: {np.std(auc_scores):.4f}')参数说明:n_estimators=500配合learning_rate=0.05是 LGB 的常用组合,学习率低需要更多树来补偿;num_leaves=31控制单棵树复杂度,太大容易过拟合;subsample=0.8和colsample_bytree=0.8是行采样和列采样,进一步降低过拟合风险。5 折的均值反映模型泛化能力,标准差反映稳定性,如果标准差超过 0.02,说明数据划分对结果影响大,需要检查分层是否到位。
4. LGB 模型调优与半监督学习:无标签数据怎么用
4.1 LGB 参数调优的实操路径
原文最终选择 LGB 作为最终模型,得到 AUC1=0.74482、AUC2=0.76577、AUC3=0.84287、加权 AUC=0.7887。这个结果明显高于其他四个模型。LGB 的优势在于直方图算法对稀疏数据的处理效率高,而且自带缺失值处理机制,对匿名特征友好。
调参路径建议从默认参数开始,先调num_leaves和learning_rate,再调subsample和colsample_bytree,最后调min_child_samples和reg_alpha/reg_lambda。常见做法是用GridSearchCV或Optuna做自动搜索,但要注意正负样本不平衡时,scale_pos_weight需要设置为负正样本比,即 14309/691≈20.7。
# 设置正负样本权重 scale_pos_weight = (y_train == 0).sum() / (y_train == 1).sum() model = lgb.LGBMClassifier( n_estimators=800, learning_rate=0.03, num_leaves=63, max_depth=7, subsample=0.8, colsample_bytree=0.7, min_child_samples=50, reg_alpha=0.1, reg_lambda=0.1, scale_pos_weight=scale_pos_weight, random_state=42 )scale_pos_weight的作用是让损失函数对少数类更敏感,但不宜设置过大,否则模型会过度预测正类,导致 AUC 反而下降。实际调参时可以先设 1,再逐步增加到 20 左右,观察验证集 AUC 变化。
4.2 半监督学习:无标签样本的两种用法
原文提到可以使用监督与半监督方法综合预测,但没有展开具体实现。常见做法有两种:一是自训练(Self-training),先用带标签数据训练一个初始模型,对无标签数据预测伪标签,把置信度高的样本加入训练集重新训练;二是协同训练(Co-training),用两个不同视角的特征集分别训练模型,互相给无标签数据打标签。
# 自训练伪标签示例 model = lgb.LGBMClassifier(n_estimators=500, learning_rate=0.05, random_state=42) model.fit(X_train, y_train) # 对无标签数据预测 X_unlabeled = train_x.drop(columns=['id', 'cust_group'], errors='ignore') pseudo_proba = model.predict_proba(X_unlabeled)[:, 1] # 选取置信度高于 0.9 或低于 0.1 的样本 high_conf_idx = np.where((pseudo_proba > 0.9) | (pseudo_proba < 0.1))[0] X_pseudo = X_unlabeled.iloc[high_conf_idx] y_pseudo = (pseudo_proba[high_conf_idx] > 0.5).astype(int) # 合并重新训练 X_augmented = pd.concat([X_train, X_pseudo], axis=0) y_augmented = pd.concat([y_train, pd.Series(y_pseudo)], axis=0) model_final = lgb.LGBMClassifier(n_estimators=800, learning_rate=0.03, random_state=42) model_final.fit(X_augmented, y_augmented)伪标签的阈值选择是关键,太高则加入的样本太少,太低则引入噪声。常见做法是先用 0.9/0.1 的严格阈值,观察验证集 AUC 是否提升,如果提升不明显再放宽到 0.8/0.2。注意伪标签样本的权重可以调低,比如设sample_weight=0.5,避免噪声主导训练。
4.3 多客群 AUC 的加权计算与验证
原文的评估指标是 AUC = 0.3×AUC1 + 0.3×AUC2 + 0.4×AUC3,其中 AUC1、AUC2、AUC3 分别对应 cust_group 取 0、1、2 的样本。计算时需要按客群拆分验证集,分别算 AUC 再加权。
def weighted_auc(y_true, y_pred, groups): aucs = {} for g in groups.unique(): mask = groups == g if mask.sum() > 0 and len(np.unique(y_true[mask])) > 1: aucs[g] = roc_auc_score(y_true[mask], y_pred[mask]) else: aucs[g] = 0.5 # 样本不足时给随机值 weighted = 0.3 * aucs.get(0, 0.5) + 0.3 * aucs.get(1, 0.5) + 0.4 * aucs.get(2, 0.5) return weighted, aucs # 在验证集上计算 val_groups = train_xy.iloc[val_idx]['cust_group'] weighted, aucs = weighted_auc(y_val, y_pred, val_groups) print(f'加权 AUC: {weighted:.4f}, 各客群 AUC: {aucs}')这个函数里有一个边界处理:如果某个客群在验证集里只有一个类别,AUC 无法计算,给 0.5 是保守做法。实际比赛中,如果某客群样本太少,可以考虑在分层时保证每个客群都有足够正负样本。
5. 避坑与排查:脱敏数据建模的五个血泪教训
5.1 缺失值 -99 未替换导致模型学到假信号
现象:模型训练集 AUC 很高,但验证集 AUC 只有 0.5 左右,完全随机。原因:-99 被当成真实数值参与均值填充和归一化,模型把 -99 当作一个强特征,但这个特征在验证集和测试集里分布不同。解决:在数据加载后第一件事就是把 -99 替换为np.nan,再做任何统计和填充。检查方法是看特征最小值,如果大量特征最小值是 -99,说明替换没做干净。
5.2 归一化在划分数据集之后做导致信息泄露
现象:线下交叉验证 AUC 0.85,提交测试集后 AUC 只有 0.72。原因:先划分训练集和验证集,再各自做归一化,验证集的均值和方差信息泄露到了训练过程。解决:归一化必须在训练集上fit,然后transform验证集和测试集。同理,均值填充、WOE 编码、目标编码都要遵循这个原则。
5.3 分层抽样只按 y 分层忽略客群分布
现象:5 折交叉验证的 AUC 标准差很大,某几折 AUC 明显偏低。原因:只按 y 分层,某些折里 group2 的样本极少,而 group2 的 AUC 权重是 0.4,导致加权 AUC 波动大。解决:构造组合标签y * 10 + cust_group再做分层,保证每折里各客群的正负样本比例与全集一致。如果某客群样本太少,可以考虑在评估时对该客群做上采样或调整权重。
5.4 LGB 的 scale_pos_weight 设置过大导致 AUC 下降
现象:设置scale_pos_weight=20后,训练集 AUC 提升到 0.95,但验证集 AUC 从 0.78 降到 0.74。原因:过大的正样本权重让模型过度预测正类,牺牲了排序能力,而 AUC 衡量的是排序质量,不是分类准确率。解决:scale_pos_weight从 1 开始逐步增加,每次增加 5,观察验证集 AUC 变化,找到峰值后停止。或者改用is_unbalance=True让 LGB 自动处理。
5.5 伪标签阈值过低引入噪声样本
现象:加入无标签数据自训练后,验证集 AUC 不升反降。原因:伪标签阈值设了 0.6/0.4,大量低置信度样本被加入训练集,噪声淹没了真实信号。解决:伪标签阈值至少设 0.9/0.1,并且给伪标签样本设sample_weight=0.3~0.5。更稳妥的做法是先用带标签数据训练一个强模型,只对无标签数据中模型预测置信度极高的样本打伪标签,且伪标签样本数量不超过带标签样本的 30%。
6. 从 0.7887 再往上走:三个可落地的进阶技巧
第一个技巧是特征交互。157 维匿名特征虽然不能解释含义,但可以用 LGB 的interaction_constraints参数强制某些特征组合交互,或者手动构造数值型特征的比值、差值。常见做法是对 TOP25 特征两两做除法,生成 C(25,2)=300 个新特征,再用随机森林筛一遍。注意除法要处理分母为 0 的情况,加一个极小值1e-6。
第二个技巧是模型融合。LGB 单模型 AUC 0.7887,如果把 LGB、XGBoost、CatBoost 三个模型的预测概率做加权平均,权重用验证集 AUC 归一化,通常能提升 0.005~0.01。更稳的做法是 Stacking:用 5 折交叉验证生成三个模型的 OOF 预测,再用一个 Logistic 回归做元学习器。元学习器的输入是三个模型的预测概率,输出是最终概率。
from sklearn.linear_model import LogisticRegression # 假设 lgb_oof, xgb_oof, cat_oof 是三个模型的 OOF 预测 stack_X = np.column_stack([lgb_oof, xgb_oof, cat_oof]) meta_model = LogisticRegression() meta_model.fit(stack_X, y_train) final_pred = meta_model.predict_proba(stack_X)[:, 1]第三个技巧是阈值移动。AUC 不依赖阈值,但实际业务中需要把概率转成 0/1 决策。正负比 21:1 时,默认阈值 0.5 会导致大量正类被漏掉。常见做法是用验证集画 KS 曲线,取 KS 最大处的阈值,或者按业务要求设定召回率下限,反推阈值。我一般会同时看 KS 和 F1,选一个平衡点。
从那以后我每次拿到脱敏数据,第一件事就是检查缺失值编码,第二件事就是确认归一化和填充的 fit/transform 边界,第三件事就是跑一遍分层交叉验证看标准差。这三步走完,线下 AUC 和线上 AUC 的差距通常能控制在 0.02 以内。希望帮到你。
本文还有配套的精品资源,点击获取