
简介面向机器学习课程大作业场景这份个贷违约预测项目源码提供了完整的建模与评测实现特别适合高校学生、竞赛入门者参考。项目围绕经典二分类任务展开以ROC曲线下面积AUC作为核心评价指标并引入描述性聚类中的软聚类思路进行数据探索。核心代码包含多层感知机、决策树概率树以及自定义的“距离-概率转换”三种模型方便对比不同方法的预测效果。压缩包共59个文件约占142.31MB其中csv数据文件16个、py/Go源码16个另有项目报告docx/pdf、演示幻灯片pptx、说明文档md/txt和模型权重文件pth等目录结构清晰。目前已有2870人学习下载资源附带的成绩截图与实验记录能直观展示模型表现是一份可动手复现、二次扩展的课程项目资料。1. 从“个贷违约预测”到能跑通全流程的机器学习源码见过太多个贷违约预测作业了数据量几千条违约率不到 5%有人用逻辑回归拿 0.62 的 AUC有人用同样数据把 LightGBM 调到 0.82差距不在模型深度而在三个常识性动作——时间字段拆没拆、标准化在切分前还是切分后、阈值是不是默认 0.5。机器学习课程大作业个贷违约预测项目源码.zip 这个标题基本概括了多数机器学习期末项目最标准的交付物形态一个二分类器、一套可复现代码、外加一个能对未知数据做预测的入口。这篇按完整项目源码该有的顺序梳理一遍赶机器学习课程大作业的能照着跑想看看二分类项目怎么组织的也能拿来当参照。2. 数据清洗与特征工程课程作业里分差最大的环节个贷违约预测的数据集通常来自放贷机构的脱敏记录常见字段包括年龄、收入、负债率、征信查询次数、贷款金额、利率、历史逾期次数、放款日期等。拿到手直接丢进模型的作业全部都会在第一步翻车。原因很简单这份数据的噪声远大于普通公开数据集缺失率高的列动辄占到一半以上类别字段和数值字段混在一起时间字段如果不拆解就变成一列字符串模型只能把它当类别编码处理。所有分差几乎都从这里开始。2.1 拿到数据先做的三个动作字段盘点、缺失率排序、时间字段拆解我一般先把数据读进来逐列统计缺失率并确认每个字段在放款决策时点的真实含义再决定保留、填充还是删除。这里的关键不是“缺失率超过某个阈值就删”而是先搞清楚缺失模式。如果是纯随机缺失填充不会带来明显偏差如果缺失本身和违约率相关高缺失列直接删除反而比填充更安全。import pandas as pd raw pd.read_csv(loan_data.csv, parse_dates[issue_date]) # 逐列缺失率排序后决定处理策略 missing_rate raw.isnull().mean().sort_values(ascendingFalse) print(missing_rate[missing_rate 0.3]) num_cols raw.select_dtypes(include[float64, int64]).columns cat_cols raw.select_dtypes(include[object]).columns # 数值列用中位数填充类别列补成 unknown raw[num_cols] raw[num_cols].fillna(raw[num_cols].median()) raw[cat_cols] raw[cat_cols].fillna(unknown) # 从放款日期拆出业务可用特征 raw[issue_year] raw[issue_date].dt.year raw[issue_month] raw[issue_date].dt.month raw.drop(columns[issue_date], inplaceTrue)parse_dates让 pandas 在读入阶段直接完成时间类型转换后面对.dt.year的调用才不会报错。中位数填充比均值稳尤其在收入这类右偏长尾变量上均值会被少数高收入样本拉高模型拿到的填充值会系统性偏大。类别列填充unknown是刻意为之缺失本身应该作为独立类别保留而不是用众数去盖掉。缺失率特征类型常见处理适用理由5%数值型中位数填充对异常值不敏感保留数据整体分布5% ~ 30%数值型按目标分组取分位数填充保留该特征与违约率的条件关联30%任意类型删除列或衍生缺失指示位缺失模式的信息价值高于列本身取值任意比例类别型填充为unknown缺失本身是一种取值树模型天然支持提示字段盘点阶段务必人工核对一遍特征说明。如果数据里出现“放款后至今逾期次数”这类和预测目标同源的字段直接删除否则后续所有评估指标都会虚高。2.2 标准化必须发生在数据切分之后顺序反了就是数据泄露机器学习模型训练里最隐蔽也最常见的错误是对全量数据做了StandardScaler().fit_transform()再做训练测试切分。表面看代码没报错、结果还挺高实际测试集已经见过训练集的均值和方差交叉验证的每一折都被污染评估分数不具备任何泛化意义。个贷这种样本量不大、特征量纲差异大的场景标准化又是必然动作顺序问题会被放大。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X raw.drop(columns[default]) y raw[default] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) scaler StandardScaler() X_train_s scaler.fit_transform(X_train) # 只在训练集上拟合 X_test_s scaler.transform(X_test) # 测试集只用 transformfit_transform和transform拆开写不是形式主义。fit会计算训练集的均值和标准差transform只做数值平移缩放。测试集一旦参与fit它的分布信息就反向进入了训练过程后期报告里的 AUC 就不可信了。逻辑回归和 SVM 这类基于梯度下降的模型必须做这一步树模型不做也不影响训练但如果后续要用同一个数据源跑多套模型统一走这个流程更省事。2.3 类别特征编码树模型直接声明线性模型才真正需要 one-hot课程作业里最常见的编码误区是拿到所有字符串列就无脑pd.get_dummies。对 LightGBM 和 XGBoost 这类树模型类别特征完全可以保留原始字符串直接传入模型内部会按类别分裂。提前 one-hot 不仅让特征矩阵膨胀几十列还让树模型学不到高基数类别内部的层级关系纯属帮倒忙。逻辑回归没有这个能力必须显式编码。import lightgbm as lgb categorical_features [purpose, grade, employment_status] # 树模型保留字符串声明为类别特征 lgb_train lgb.Dataset(X_train, labely_train, categorical_featurecategorical_features)线性模型侧的做法是按特征基数分档处理类别数小于 10 的用OneHotEncoder类别几十个以上的用 target encoding 或直接并入“其他”类。个贷数据里像purpose这种十来个取值的列用 one-hot 问题不大遇到 ZIP 码这类高基数列就必须先做频次合并。这条规则在机器学习应用流程里属于最容易被忽略但又最能压分的点。3. 模型训练与参数取舍逻辑回归兜底LightGBM 拉上限个贷违约预测本质是一个二分类问题课程作业的评分通常看两点报告里能不能把机器学习算法讲清楚最后的预测指标有没有说服力。这两个诉求正好对应两套模型。逻辑回归负责可解释LightGBM 负责分数上限。只交一个模型的同学大概率会在答辩环节被追问“为什么选这个模型以及另一个会怎样”。3.1 逻辑回归为什么是课程作业里的兜底模型逻辑回归的权重向量天然带有“特征方向”语义系数为正表示该特征值越大违约概率越高负号则相反。这在风控报告里可以直接翻译成业务结论树模型的特征重要性做不到这一点。而且逻辑回归对特征共线性敏感这反而成了倒逼学生做特征筛选的理由。梯度下降收敛需要标准化正则项对量纲不一致的特征惩罚也不同所以前面标准化那一步对逻辑回归不是可选项。from sklearn.linear_model import LogisticRegression lr LogisticRegression( C1.0, class_weightbalanced, max_iter1000, random_state42, ) lr.fit(X_train_s, y_train)C1.0是默认正则强度样本量在几千条级别时不用太激进class_weightbalanced让少数类样本的损失按频率反比放大这一步直接对标违约样本占比不足 10% 的类别不均衡问题max_iter1000是必须给的liblinear外的默认求解器在特征未标准化或迭代不足时容易报收敛警告。逻辑回归的 AUC 通常在 0.65 到 0.75 之间这个分数作为报告里的“基线模型”正当合理没人会质疑。3.2 LightGBM 训练的最小完整代码与参数边界LightGBM 在当前机器学习期末项目里基本是默认主力。训练快、能直接吃类别特征和缺失值、AUC 上限比逻辑回归高 5 到 15 个百分点。但要小心一件事它太好用了学生很容易把num_boost_round拉到几千轮不设早停然后在测试集上拿到一个过拟合的漂亮分数。import numpy as np import lightgbm as lgb from sklearn.model_selection import StratifiedKFold from sklearn.metrics import roc_auc_score params { objective: binary, metric: auc, learning_rate: 0.05, num_leaves: 31, max_depth: 7, min_child_samples: 50, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, verbose: -1, } skf StratifiedKFold(n_splits10, shuffleTrue, random_state42) oof np.zeros(len(X_train)) for fold, (tr_idx, va_idx) in enumerate(skf.split(X_train, y_train)): dtr lgb.Dataset(X_train.iloc[tr_idx], labely_train.iloc[tr_idx], categorical_featurecategorical_features) dva lgb.Dataset(X_train.iloc[va_idx], labely_train.iloc[va_idx], categorical_featurecategorical_features) model lgb.train(params, dtr, num_boost_round2000, valid_sets[dva], callbacks[lgb.early_stopping(100, verboseFalse)]) oof[va_idx] model.predict(X_train.iloc[va_idx], num_iterationmodel.best_iteration) print(f10-fold OOF AUC: {roc_auc_score(y_train, oof):.4f})num_leaves31对应树深 4 到 5 的复杂度在几千条样本上已经够用继续加大只提升训练集分数验证集 AUC 反而波动变大min_child_samples50是风控小样本场景的关键参数强制叶子节点最少含 50 个样本防止树在个位数的异常样本上反复分裂learning_rate0.05配合num_boost_round2000和早停让模型自己决定在哪一轮停而不是拍脑袋定迭代次数。10 折分层交叉验证在这里不只是评估手段oof概率还会在下一章用于阈值选择。3.3 随机种子与可复现性的边界固定random_state42是课程作业的标配但要清楚它的能力边界它只保证同一环境、同一依赖版本下的结果可复现。换一台机器、升级一次 LightGBM 或 sklearn 版本相同种子得到的结果不会完全一致。写报告时把numpy、pandas、scikit-learn、lightgbm的版本号留在 README 里比在代码里反复强调种子更重要。交叉验证本身已经包含随机性所以 10 折的shuffle种子和模型内部种子要分开设置否则折与折之间的随机性会被不必要地绑定。4. 样本不均衡与决策阈值个贷场景的评估套路个贷违约预测最典型的场景特征是违约率极低常见数据里正样本占 3% 到 8%。这种分布下无脑全部预测为“不违约”也能拿到超过 90% 的 accuracy看起来分数漂亮但这份机器学习项目源码就失去了意义。先认清评估目标再谈调参是这一章要解决的核心问题。4.1 违约率 5% 时accuracy 不能作为机器学习分类器的评判指标accuracy 在类别均衡时直观好用在违约率只有 5% 的数据上就是垃圾指标。假设模型把所有人预测为不违约accuracy 是 95%这比任何认真训练的模型都高但它一个违约用户都抓不住。贷款风控里漏掉一个违约用户造成的损失远大于误伤一个正常用户的利息收入所以业务语义天然偏向“少漏”。课程作业报告如果只写 accuracy答辩时基本必被追问。正确做法是同时给出 AUC、KS 和 PR 曲线。AUC 衡量排序能力Kolmogorov-Smirnov 统计量衡量违约与非违约样本的累积分布最大差异PR 曲线则在正样本稀缺时比 ROC 更敏感因为它的分母用的是预测为正的样本数不会被大量真负样本稀释。我在报告里一般写三个数AUC、KS、以及阈值调到最优后的 recall这样既有整体排序指标又有业务可操作的决策点。4.2 类别不均衡处理手段对比加权重、过采样还是移动阈值处理手段原理课程作业里的实际坑class_weightbalanced按类别频率反比放大少数类损失概率分布发生偏移阈值不能继续用 0.5SMOTE 过采样在特征空间合成少数类样本对树模型收益有限答辩时难解释“合成样本的真实性”阈值移动保持模型不变只调整决策分界点最稳妥但阈值必须在验证集上重新选取随机欠采样丢弃多数类样本样本量小于 2 万时慎用方差显著变大我一般只在实际操作中用两类手段逻辑回归加class_weightbalancedLightGBM 不做过采样而是直接把scale_pos_weight或者不设权重、靠阈值移动来修正。SMOTE 在树模型上几乎不会带来提升因为它合成的样本并不是真实分布中的点树模型又天然擅长处理分布边缘合成样本只会让边界更混乱。放在报告里作为对比实验是可以的直接作为主方案则容易在答辩时被问住。4.3 用交叉验证的 OOF 概率自动寻找最佳决策阈值阈值调整的对象不是测试集而是训练过程中产出的 OOF 概率。把每一折的验证集概率收集起来等于拿到了一个“模型从未直接见过”的预测分布在这个分布上选阈值才接近真实泛化表现。from sklearn.metrics import precision_recall_curve precision, recall, thresholds precision_recall_curve(y_train, oof) f1_scores 2 * precision * recall / (precision recall) best_idx np.argmax(f1_scores[:-1]) # 最后一个阈值位置无意义 best_threshold thresholds[best_idx] print(fbest threshold: {best_threshold:.4f}, F1: {f1_scores[best_idx]:.4f}) print(f违约率: {y_train.mean():.2%})当违约率只有 3% 时F1 最大化对应的阈值通常在 0.2 到 0.4 之间很少会待在 0.5。f1_scores[:-1]去掉最后一个无意义位置是因为precision_recall_curve会在末尾补一个阈值 1对应召回率为 0。选好阈值之后把它和模型一起保存预测阶段直接使用这比在测试集上反复试阈值再挑好看的分数要严谨得多。5. 让 zip 压缩包里的 predict.py 交付后能直接跑起来课程作业源码包的交付标准只有一个换一台没有训练环境的机器按 README 装完依赖跑predict.py能出结果。很多项目源码.zip 解压后代码只能在作者自己电脑上运行问题几乎都出在预处理逻辑与模型文件分离上。标准化器的均值方差、中位数填充值、特征列顺序这些训练阶段产生的“状态”全部需要随模型一起持久化。5.1 预处理状态和模型一起存盘而不是让测试脚本重新计算第一版写预测脚本的人通常会犯一个错误在predict.py里直接StandardScaler().fit_transform(单条新数据)。单条数据拟合出来的均值和标准差就是它自己缩放后全部变成 0预测结果完全失真。正确的做法是在训练脚本结束时把所有预处理产物统一保存。import joblib model_dir ./model/ joblib.dump(scaler, model_dir scaler.joblib) joblib.dump(model, model_dir lgb_model.joblib) joblib.dump(X_train.columns.tolist(), model_dir feature_list.joblib) joblib.dump(best_threshold, model_dir threshold.joblib)joblib对 numpy 数组的序列化效率高于 pickle是 sklearn 官方推荐做法。feature_list一定不能省略预测时新数据列顺序稍有变化reindex会自动对齐没有这个列表就只能靠肉眼核对列名。跨机器加载失败时先检查 sklearn 和 joblib 版本次贷危机式的兼容问题在机器学习项目里最常见原因就是版本不一致。5.2 predict.py 的最小完整结构预测入口的逻辑必须和训练流程严格保持一致先填充缺失值再做列对齐再标准化最后进模型。import joblib import pandas as pd model_dir ./model/ model joblib.load(model_dir lgb_model.joblib) scaler joblib.load(model_dir scaler.joblib) feature_list joblib.load(model_dir feature_list.joblib) threshold joblib.load(model_dir threshold.joblib) def predict(raw_df: pd.DataFrame) - pd.DataFrame: df raw_df.copy() df df.fillna({income: df[income].median()}) df df.reindex(columnsfeature_list, fill_value0) prob model.predict_proba(scaler.transform(df))[:, 1] return pd.DataFrame({pred_prob: prob, pred_label: (prob threshold).astype(int)})reindex做了三件事去掉训练集不存在的列、补齐训练集有但输入数据缺失的列、保证列顺序与训练时一致。fill_value0只适合极少数无关列缺失的情况真正重要的特征如果缺失这里应该直接抛异常而不是静默补 0。加了assert df.isnull().sum().sum() 0在标准化之前能在模型前拦截到未处理干净的输入。5.3 用特征重要性反向排查数据泄露zip 里交付的模型如果分数高得离谱比如 AUC 超过 0.95先不要高兴大概率是数据泄露。排查手段很便宜打印 LightGBM 的 gain 特征重要性看排名靠前的特征是否在放款决策时点真实可见。import pandas as pd importance pd.Series(model.feature_importance(gain), indexfeature_list) print(importance.sort_values(ascendingFalse).head(20))gain表示该特征在所有分裂点带来的累计增益比split更能反映信息贡献量。课程作业数据泄露的高频特征是用户 ID、放款批次号、以及任何包含“后”“至今”字样的时间衍生字段。把重要性 top-1 的特征抓出来和特征说明逐条比对凡是看起来在贷款发生时还未知的字段一律回到数据清洗阶段重新处理。本文还有配套的精品资源点击获取