☰
信用卡违约预测实战:多模型融合与交叉验证全流程
2026/10/10 7:03:28 网站建设 项目流程

简介:这份资源面向金融风控方向的数据科学从业者与机器学习学习者,聚焦信用卡违约预测这一典型分类任务,提供一套可运行的完整实现方案。压缩包内共1个文件,为单个py脚本,整体约4KB,体量轻便,便于快速阅读与二次修改。脚本预计覆盖数据加载与清洗、特征工程、多模型训练与k折交叉验证,并落地bagging、boosting、stacking等模型融合策略,最后通过AUC-ROC、精确率、召回率、F1分数等指标评估效果,形成从预处理到预测输出的完整链路。已有591人学习下载,说明该方案在同类任务中具备一定参考价值。读者可借此理解逻辑回归、随机森林、XGBoost等算法的选型思路,掌握融合模型降低过拟合、提升稳定性的实践方法,并对照代码梳理金融风控项目的标准流程,适合作为课程作业、竞赛基线或业务原型的技术参考。

1. 拆开 predict.rar:一份能跑通的信用卡违约预测与模型融合实战包

信贷风控里有个反直觉的现象:单模型 AUC 冲到 0.78 就沾沾自喜,上线后逾期召回却一塌糊涂,真正卡住业务脖子的往往不是算法不够新,而是没把多模型的预测结果用好。这次拆的predict.rar里就一个predict.py,配合数据集能完整跑通「数据清洗 → 特征工程 → 多模型训练 → 交叉验证 → 融合输出」这条链路,覆盖逻辑回归、随机森林、XGBoost、LightGBM 到 stacking 元学习。它适合两类人:刚转金融风控、想找一个能照着复现的端到端脚本的工程师;以及手头有信贷数据、想快速验证融合策略是否比单模型更稳的从业者。下面按我实际跑一遍的顺序,把参数、坑和调优边界都摊开讲。

2. 数据预处理与特征工程:predict.py 里最容易被跳过的硬骨头

2.1 先看清数据长什么样再动手

信用卡违约数据集通常是「一张宽表 + 一个二分类标签」,标签列常见命名是default.payment.next.month或is_default,0 代表正常还款,1 代表违约。这类数据有几个固定特征:样本量几万条、特征二三十列、正负样本比例悬殊(违约率常在 20% 上下,极端情况到 5%)。predict.py开头一般用 pandas 读入,我习惯先做一次结构体检再进流程:

import pandas as pd import numpy as np # 读入原始数据,注意编码和分隔符,信贷数据常见 csv 或 xls df = pd.read_csv("credit_default.csv", encoding="utf-8") # 体检:形状、标签分布、缺失、类型 print("shape:", df.shape) print("label ratio:\n", df["is_default"].value_counts(normalize=True)) print("missing:\n", df.isnull().sum()[df.isnull().sum() > 0]) print("dtypes:\n", df.dtypes.value_counts()) # 标签列单独拎出来,避免后续特征工程误伤 y = df["is_default"].astype(int) X = df.drop(columns=["is_default"])

逻辑说明:value_counts(normalize=True)直接给出违约占比,这个数字决定后面要不要做重采样;isnull().sum()只打印有缺失的列,避免刷屏。参数上,encoding要按实际文件改,遇到中文列名乱码就换gbk;如果数据是 Excel,用pd.read_excel并指定sheet_name。

2.2 缺失值、异常值与重复值的三板斧

信贷数据的缺失往往有业务含义——比如「近半年查询次数」为空,可能代表该客户从未被查询过,直接填 0 比填均值更合理。predict.py里常见的处理顺序是:先去重,再处理异常值,最后填缺失。

# 1. 去重:完全重复的行直接删 df = df.drop_duplicates() # 2. 异常值:账龄、年龄这类字段用分位数截断,避免极端值带偏标准化 for col in ["age", "credit_limit", "bill_amount"]: low, high = df[col].quantile([0.01, 0.99]) df[col] = df[col].clip(low, high) # 3. 缺失值:数值列填中位数,类别列填众数或单独一档 num_cols = df.select_dtypes(include=[np.number]).columns cat_cols = df.select_dtypes(include=["object"]).columns df[num_cols] = df[num_cols].fillna(df[num_cols].median()) for c in cat_cols: df[c] = df[c].fillna("Unknown")

逻辑说明:quantile([0.01, 0.99])取 1% 和 99% 分位做截断,比直接删行温和,保留样本量;中位数填充对偏态分布比均值稳。参数上,分位点可以按业务调整,风控里常用 0.005/0.995 更保守。注意:如果缺失比例超过 40%,填任何值都是噪声,我一般直接删列。

2.3 特征工程:分箱、交互与降维的取舍

原始特征直接喂模型不是不行,但风控场景里「年龄 25 和 26 的违约风险几乎一样,35 和 55 却差很多」,所以连续变量分箱(WOE 或等频)往往比原始值更有效。predict.py里如果没做 WOE,至少会做标准化和独热编码。

from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline # 数值列标准化,类别列独热;注意 OneHotEncoder 的 handle_unknown preprocess = ColumnTransformer( transformers=[ ("num", StandardScaler(), num_cols.tolist()), ("cat", OneHotEncoder(handle_unknown="ignore", sparse_output=False), cat_cols.tolist()), ], remainder="drop" ) # 包成 Pipeline,保证交叉验证时预处理只在训练折上 fit,避免数据泄漏 pipe = Pipeline([("prep", preprocess)]) X_processed = pipe.fit_transform(df.drop(columns=["is_default"]))

逻辑说明:ColumnTransformer把数值和类别分开处理,handle_unknown="ignore"保证测试集出现训练时没见过的类别不会报错。关键点是预处理必须放进 Pipeline,否则先对全量数据标准化再交叉验证,验证集信息泄漏进训练,AUC 会虚高——这是新手最常翻车的地方。降维(PCA)在这个数据规模上收益有限,除非特征上百列,否则我一般跳过。

3. 多模型训练与交叉验证:从逻辑回归到 LightGBM 的参数落点

3.1 基模型选型:为什么不是越复杂越好

predict.py里同时出现逻辑回归、随机森林、XGBoost、LightGBM,不是堆砌,而是各有分工。逻辑回归可解释性强、训练快,适合做基线;随机森林对缺失和异常不敏感,方差低;XGBoost 和 LightGBM 在表格数据上通常精度最高,但参数敏感。选型逻辑是:先用逻辑回归跑通全流程拿到基线 AUC,再用树模型冲精度,最后用融合把两者的优势合起来。

from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier from lightgbm import LGBMClassifier models = { "lr": LogisticRegression(max_iter=1000, C=1.0, class_weight="balanced"), "rf": RandomForestClassifier(n_estimators=300, max_depth=8, class_weight="balanced", n_jobs=-1, random_state=42), "xgb": XGBClassifier(n_estimators=400, learning_rate=0.05, max_depth=5, subsample=0.8, colsample_bytree=0.8, eval_metric="auc", random_state=42), "lgb": LGBMClassifier(n_estimators=400, learning_rate=0.05, num_leaves=31, subsample=0.8, colsample_bytree=0.8, random_state=42), }

参数说明:class_weight="balanced"让逻辑回归和随机森林自动按类别频率加权,缓解样本不均衡;XGBoost 的subsample和colsample_bytree控制行、列采样比例,0.8 是抗过拟合的常用起点;LightGBM 的num_leaves比max_depth更影响复杂度,31 是默认值,数据量大可调到 63。random_state固定保证结果可复现,调参时别改。

3.2 K 折交叉验证:别只看平均 AUC

交叉验证不只是算个平均分,更要看每折的方差。如果 5 折 AUC 从 0.72 跳到 0.81,说明模型对数据划分敏感,融合时权重会不稳。

from sklearn.model_selection import StratifiedKFold, cross_val_score skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for name, model in models.items(): scores = cross_val_score(model, X_processed, y, cv=skf, scoring="roc_auc", n_jobs=-1) print(f"{name}: AUC={scores.mean():.4f} (+/- {scores.std():.4f})")

逻辑说明:StratifiedKFold保证每折里违约样本比例一致,比普通 KFold 更适合不均衡数据;scoring="roc_auc"直接输出 AUC。参数上n_splits=5是精度和耗时的平衡点,样本少于 1 万可以上 10 折。看结果时,标准差超过 0.02 就要警惕,可能需要检查是否有泄漏特征或样本量太小。

3.3 模型融合:Bagging、Boosting 与 Stacking 的实操差别

Bagging 和 Boosting 在predict.py里通常体现为随机森林和 XGBoost 本身,真正需要手写的是 Stacking——用基模型的输出当新特征,再训一个元模型。

from sklearn.ensemble import StackingClassifier # 基模型列表,元模型用逻辑回归,防止元层过拟合 estimators = [(name, model) for name, model in models.items()] stack = StackingClassifier( estimators=estimators, final_estimator=LogisticRegression(max_iter=1000), cv=5, passthrough=False, # 是否把原始特征也传给元模型 n_jobs=-1 ) stack.fit(X_processed, y)

逻辑说明:StackingClassifier内部对每个基模型做 5 折交叉预测,生成 out-of-fold 预测作为元模型输入,避免基模型在训练集上的过拟合被元模型学走。passthrough=False表示元模型只看基模型输出,设为 True 会把原始特征拼进去,数据量大时容易过拟合,我一般保持 False。元模型选逻辑回归而非树模型,是因为元层输入维度低(等于基模型数量),线性模型更稳。

4. 避坑与排查:跑 predict.py 时最常撞上的五个问题

4.1 现象:AUC 高得离谱,接近 0.99

原因:预处理在交叉验证之前对全量数据做了 fit,验证集信息泄漏;或者数据里有「未来信息」特征,比如用还款后的状态预测是否违约。解决:把所有预处理塞进 Pipeline,确保fit只在训练折发生;逐列检查特征含义,任何在预测时点拿不到的字段一律删掉。

4.2 现象:LightGBM 报错「Do not support special JSON characters in feature name」

原因:原始列名带中文、空格或特殊符号,LightGBM 对特征名有字符限制。解决:在读入后统一重命名,用df.columns = [f"f{i}" for i in range(df.shape[1])]或正则替换掉非字母数字字符,再进模型。

4.3 现象:Stacking 训练时间暴涨,内存吃满

原因:基模型数量多、每个都做 5 折交叉预测,计算量是单模型的 N×5 倍;n_jobs=-1又让所有核同时抢内存。解决:先单独评估每个基模型的 AUC,把明显拖后腿的(比如 AUC 低于最好模型 0.03 以上)剔出融合列表;n_jobs改成 2 或 4,给内存留余量。

4.4 现象:交叉验证每折 AUC 波动大,融合后反而比单模型差

原因:样本量小或正负比例极端,某些折里违约样本太少,基模型学不稳,元模型学到的是噪声。解决:改用StratifiedKFold并增加折数到 10;或者对训练折做 SMOTE 过采样,但注意过采样只能在训练折内做,不能碰验证折。

4.5 现象:预测输出全是 0 或全是 1

原因:用默认 0.5 阈值切分,而违约样本少、模型输出概率整体偏低。解决:不要用固定阈值,改用验证集上的 F1 或 KS 统计量找最优切点;predict_proba拿概率后,用np.quantile按业务要求的通过率反推阈值。

5. 进阶技巧:用 OOF 预测做加权融合与阈值校准

Stacking 不是唯一出路,实际业务里我更常用「OOF 预测 + 权重搜索」这套轻量方案,因为它可控、可解释,出问题能定位到具体基模型。核心思路是:先拿到每个基模型在训练集上的 out-of-fold 预测概率,再用这些概率去搜一组权重,让加权后的 AUC 最大。

from sklearn.model_selection import cross_val_predict from scipy.optimize import minimize from sklearn.metrics import roc_auc_score # 1. 收集每个基模型的 OOF 预测概率 oof_preds = {} for name, model in models.items(): oof_preds[name] = cross_val_predict( model, X_processed, y, cv=skf, method="predict_proba", n_jobs=-1 )[:, 1] # 2. 定义目标函数:权重非负且和为 1,最大化 AUC def neg_auc(weights): weights = np.abs(weights) / np.abs(weights).sum() # 归一化 blend = sum(w * oof_preds[n] for w, n in zip(weights, oof_preds)) return -roc_auc_score(y, blend) # 3. 从均等权重出发做优化 init = np.ones(len(oof_preds)) / len(oof_preds) res = minimize(neg_auc, init, method="Nelder-Mead") best_w = np.abs(res.x) / np.abs(res.x).sum() print("best weights:", dict(zip(oof_preds.keys(), best_w.round(3))))

逻辑说明:cross_val_predict返回的是每个样本在「没被该折训练过」的模型上的预测,天然避免泄漏;minimize用 Nelder-Mead 是因为目标函数不可导且维度低(基模型数量),比梯度法稳。参数上,初始权重设均等,优化后如果某个模型权重接近 0,说明它对融合没贡献,可以直接剔除再跑一轮。

拿到融合概率后,阈值校准是最后一步。风控业务通常关心「在通过率 70% 的前提下,能抓住多少违约」,所以阈值不是拍脑袋定的:

# 按业务通过率反推阈值:假设只拒绝 30% 的高风险客户 threshold = np.quantile(blend_prob, 0.70) pred_label = (blend_prob >= threshold).astype(int) print("threshold:", round(threshold, 4)) print("recall on default:", recall_score(y, pred_label))

这里np.quantile(blend_prob, 0.70)表示把预测概率从低到高排,取 70% 分位作为切点,高于它的 30% 判为违约。参数 0.70 要按实际审批通过率改,不是固定值。我一般会画一条 KS 曲线,取 KS 最大点作为参考阈值,再结合业务通过率微调。

从那以后我每次跑融合模型,都强制先看 OOF 预测的相关系数矩阵——如果两个基模型相关性超过 0.95,留一个就够,多留只会增加计算量不涨分。这套流程不复杂,但能把「模型融合」从玄学变成可复现的工程步骤。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询