
简介这份资源是面向高校机器学习课程大作业场景的个贷违约预测完整项目源码适合正在完成课程设计、需要参考完整建模流程的本科生与研究生。项目以ROC曲线下面积AUC作为核心评价指标围绕描述性聚类到软聚类的思路展开并实现了多层感知机、决策树概率树以及基于距离-概率转换的自定义模型三种方案便于横向对比不同算法的预测效果。压缩包共59个文件约142.31MB包含csv数据集、py与go源码、md说明文档、png结果图、pdf与docx报告、pth模型权重及pptx汇报幻灯片等覆盖数据、代码、报告与展示全链路。目前已有2871人学习下载。读者可据此获得可直接运行的完整赛题方案、模型训练与评估脚本、实验报告与答辩材料并借助目录中按模型划分的模块结构快速定位代码理解从特征处理到AUC评测的完整实现路径。1. 个贷违约预测大作业一份源码能跑通到什么程度个贷违约预测这个题目几乎每年都会出现在机器学习课程大作业的选题清单里。原因很直接数据是结构化的、标签是二分类的、评价指标是明确的看起来门槛不高但真正动手做的时候很多人会卡在几个具体的地方——类别极度不平衡导致模型全预测成不违约、特征里混着大量脱敏后的匿名变量不知道怎么处理、AUC 跑出来 0.9 但换一份数据就崩。这份机器学习课程大作业个贷违约预测项目源码.zip对应的正是这样一套完整流程从原始表格数据读入、缺失值处理、特征工程、类别不平衡处理到模型训练、阈值调优和结果输出。它适合两类人一类是正在赶大作业、需要一份能跑通且能讲清楚每一步为什么这么做的参考实现另一类是想拿一个真实结构化数据场景练手特征工程和模型评估的入门者。下面我按自己实际跑这类项目的顺序把整条链路拆开讲。2. 数据读入与标签定义先把违约这件事说清楚2.1 个贷违约预测的数据长什么样个贷违约预测的原始数据通常是一张宽表每行代表一个借款人的一笔贷款记录列大致分几类借款人基本信息年龄、职业、收入水平等但很多是脱敏后的编码值、贷款属性金额、期限、利率、还款方式、历史行为过往逾期次数、查询次数、授信额度使用率、以及一个标签列表示是否违约。常见做法是标签列用 0/1 表示1 代表违约。这里第一个容易翻车的地方是不同来源的数据对违约的定义不一样——有的定义是逾期 90 天以上有的定义是逾期 30 天以上有的甚至把提前还款也算作某种负样本。你拿到数据后第一件事不是急着建模而是确认标签列的含义和正负样本比例。import pandas as pd import numpy as np # 读入原始数据注意编码和分隔符要按实际文件调整 df pd.read_csv(loan_data.csv, encodingutf-8) # 查看基本信息 print(df.shape) print(df[default].value_counts()) print(df[default].value_counts(normalizeTrue)) # 确认标签列没有缺失 print(df[default].isnull().sum())这段代码做三件事看数据规模、看标签分布、确认标签列没有缺失。参数上encoding要根据实际文件调整中文环境常见gbk或utf-8value_counts(normalizeTrue)给出的是比例而不是计数方便你判断不平衡程度。如果正样本比例低于 5%后面就必须认真处理不平衡问题不能直接上默认参数的逻辑回归。2.2 缺失值与异常值的处理策略个贷数据里缺失值很常见尤其是收入、职业这类字段。处理方式无非几种删除、均值/中位数填充、众数填充、或者用模型预测填充。我的经验是先看缺失比例——超过 60% 的列直接删掉因为填充带来的噪声可能比信息还大30% 到 60% 之间的列考虑用模型预测填充或者单独标记一个缺失类别30% 以下的列中位数或众数填充通常够用。异常值方面金额和利率这类字段经常有极端值建议用分位数截断而不是直接删除因为极端值本身可能就是违约信号。# 计算每列缺失比例 missing_ratio df.isnull().sum() / len(df) print(missing_ratio[missing_ratio 0].sort_values(ascendingFalse)) # 删除缺失超过60%的列 cols_to_drop missing_ratio[missing_ratio 0.6].index.tolist() df df.drop(columnscols_to_drop) # 数值列用中位数填充类别列用众数填充 num_cols df.select_dtypes(include[np.number]).columns.tolist() cat_cols df.select_dtypes(include[object]).columns.tolist() for col in num_cols: if df[col].isnull().sum() 0: df[col] df[col].fillna(df[col].median()) for col in cat_cols: if df[col].isnull().sum() 0: df[col] df[col].fillna(df[col].mode()[0]) # 对金额类字段做分位数截断 for col in [loan_amount, interest_rate]: if col in df.columns: lower df[col].quantile(0.01) upper df[col].quantile(0.99) df[col] df[col].clip(lower, upper)这里的关键参数是 0.6 这个阈值和 0.01/0.99 的分位数。阈值不是固定的如果你的数据质量好可以放宽到 0.7分位数截断会改变原始分布所以截断后建议重新看一下统计量确认没有把重要信号截掉。类别列的众数填充在类别分布极度偏斜时可能引入偏差如果某个类别列 90% 都是同一个值填充众数等于没填这时候可以考虑单独标记缺失。3. 特征工程把匿名变量变成模型能用的信号3.1 数值特征的分箱与交叉个贷数据里很多数值特征和标签的关系不是线性的比如年龄和违约率往往是 U 型关系——太年轻和太年长的违约率都偏高。这时候直接放原始数值进模型线性模型学不好树模型虽然能自动切分但分箱后往往更稳定。常见做法是对年龄、收入、贷款金额做等频分箱或自定义分箱然后做 WOE 编码或者直接做 one-hot。另外交叉特征在这个场景里很有效比如收入/贷款金额这个比值比单独看收入和贷款金额更能反映还款压力。# 对年龄做自定义分箱 bins [0, 25, 35, 45, 55, 100] labels [young, mid_young, mid, mid_old, old] df[age_bin] pd.cut(df[age], binsbins, labelslabels) # 构造收入贷款比 df[income_loan_ratio] df[income] / (df[loan_amount] 1) # 构造贷款期限与利率的交叉 df[term_rate_interaction] df[term] * df[interest_rate] # 查看分箱后的违约率 print(df.groupby(age_bin)[default].mean())分箱的边界要根据实际数据的分布来定上面给的边界只是示例。income_loan_ratio里加 1 是为了避免除零。交叉特征构造完后要检查一下和标签的相关性相关性太低的可以直接去掉避免维度爆炸。树模型对交叉特征不敏感但线性模型和神经网络会明显受益。3.2 类别特征的编码方式选择类别特征编码是个老生常谈的问题但在个贷场景里有几个具体注意点。高基数类别比如职业代码有几百个取值直接用 one-hot 会导致维度爆炸常见做法是用目标编码target encoding或者频率编码。目标编码要注意用交叉验证的方式计算否则会标签泄露。低基数类别比如性别、婚姻状况用 one-hot 就够了。另外有些类别列虽然是数字编码但实际是名义变量不能当数值用这个坑在脱敏数据里特别常见。from sklearn.model_selection import KFold # 目标编码用K折避免泄露 def target_encode(df, col, target, n_splits5): df[col _te] np.nan kf KFold(n_splitsn_splits, shuffleTrue, random_state42) for train_idx, val_idx in kf.split(df): train_fold df.iloc[train_idx] val_fold df.iloc[val_idx] mean_val train_fold.groupby(col)[target].mean() df.loc[df.index[val_idx], col _te] val_fold[col].map(mean_val) # 用全局均值填充没覆盖到的类别 df[col _te] df[col _te].fillna(df[target].mean()) return df # 对高基数类别做目标编码 for col in [occupation_code, region_code]: if col in df.columns: df target_encode(df, col, default) # 低基数类别做one-hot low_card_cols [gender, marital_status] df pd.get_dummies(df, columnslow_card_cols, drop_firstTrue)目标编码的 K 折数一般取 5 或 10折数越多计算越慢但泄露越少。drop_firstTrue在 one-hot 里去掉一个类别避免共线性对树模型其实无所谓但线性模型需要。目标编码后建议看一下编码后的分布如果某些类别的编码值极端偏离可能是该类别样本太少导致的可以考虑合并稀有类别。4. 不平衡处理与模型训练别让 AUC 骗了你4.1 类别不平衡的三种处理路径个贷违约预测里正样本通常只占几个百分点不平衡处理直接决定模型能不能用。三条路径一是重采样包括过采样SMOTE和欠采样二是改损失函数比如给正样本更高权重三是调阈值模型输出概率后选一个使 F1 或 KS 最大的阈值。我的经验是先试类别权重因为最简单且不改变数据分布如果效果不够再试 SMOTE欠采样在数据量大时可以用但会丢信息。注意 SMOTE 不能直接用在有缺失值的数据上也不能对类别特征做插值。from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from imblearn.over_sampling import SMOTE from sklearn.model_selection import train_test_split # 准备特征和标签 feature_cols [c for c in df.columns if c not in [default, age_bin]] X df[feature_cols].select_dtypes(include[np.number]) y df[default] # 划分训练集和测试集stratify保证分布一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) # 路径一类别权重 lr_weighted LogisticRegression(class_weightbalanced, max_iter1000) lr_weighted.fit(X_train, y_train) # 路径二SMOTE过采样 smote SMOTE(random_state42) X_train_sm, y_train_sm smote.fit_resample(X_train, y_train) rf_smote RandomForestClassifier(n_estimators200, random_state42) rf_smote.fit(X_train_sm, y_train_sm)class_weightbalanced会自动按类别频率反比设置权重。SMOTE 的random_state要固定否则每次结果不一样。注意 SMOTE 只能在训练集上做测试集必须保持原始分布否则评估结果会虚高。stratifyy保证划分后训练集和测试集的正负比例一致这个参数在不平衡场景里必加。4.2 模型评估AUC、KS 和阈值选择个贷违约预测最常用的评估指标是 AUC 和 KS。AUC 衡量排序能力KS 衡量区分度两个都要看。但 AUC 高不代表业务上好用因为业务上需要的是一个明确的阈值来区分通过和拒绝。所以训练完模型后要在验证集上画 KS 曲线找到 KS 最大的点作为阈值参考。另外记得看混淆矩阵和查准率/查全率尤其是在正样本少的时候AUC 0.85 但查全率只有 0.3 的情况很常见。from sklearn.metrics import roc_auc_score, roc_curve, confusion_matrix, classification_report # 预测概率 y_prob rf_smote.predict_proba(X_test)[:, 1] # AUC auc roc_auc_score(y_test, y_prob) print(fAUC: {auc:.4f}) # KS fpr, tpr, thresholds roc_curve(y_test, y_prob) ks max(tpr - fpr) print(fKS: {ks:.4f}) # 找KS最大对应的阈值 ks_idx np.argmax(tpr - fpr) best_threshold thresholds[ks_idx] print(fBest threshold: {best_threshold:.4f}) # 按最优阈值输出分类报告 y_pred (y_prob best_threshold).astype(int) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred))predict_proba返回两列第二列是正类概率。KS 的计算是max(tpr - fpr)对应的阈值就是业务上可以参考的切分点。classification_report里的 recall 对正类很重要如果 recall 太低说明模型漏掉了太多违约用户业务上不可接受。阈值不是固定不变的实际部署时还要根据通过率和坏账率的平衡来调整。5. 避坑与排查那些让大作业翻车的细节5.1 数据泄露AUC 0.99 的幻觉现象模型在验证集上 AUC 接近 1但换一份数据或者交叉验证时结果大幅下降。原因特征里混入了标签相关的信息比如是否催收、逾期天数这类字段这些字段在预测时根本拿不到。解决建模前逐个检查特征凡是和标签有直接因果关系的字段一律删掉用时间切分而不是随机切分来验证确保训练集的时间早于测试集。5.2 目标编码泄露编码后 AUC 虚高现象做了目标编码后 AUC 明显提升但线上效果差。原因目标编码时用了全量数据计算类别均值导致验证集信息泄露到训练集。解决必须用 K 折的方式计算目标编码每一折的编码值只用训练折的数据计算或者用平滑后的目标编码给类别均值加一个先验权重。5.3 缺失值填充引入偏差现象填充后模型在缺失率高的样本上表现特别差。原因中位数填充把所有缺失样本都变成同一个值模型无法区分真的缺失和值就是中位数。解决对缺失率高的列填充的同时加一个缺失标记列或者用模型预测填充但要注意预测模型本身不能用到标签。5.4 阈值选择脱离业务现象模型 AUC 和 KS 都不错但业务方说不能用。原因阈值选的是 KS 最大点但业务上要求通过率不能低于某个值或者坏账率不能高于某个值。解决在验证集上画出通过率-坏账率曲线根据业务约束选阈值而不是只看统计指标。5.5 随机种子不固定导致结果不可复现现象每次跑代码结果都不一样大作业报告里的数字和代码跑出来的对不上。原因SMOTE、随机森林、数据划分等环节没有固定随机种子。解决在所有涉及随机的环节都设置random_state包括train_test_split、SMOTE、RandomForestClassifier等如果用了 GPU还要注意 CUDA 的随机性。6. 从大作业到可复现一个验证脚本和两条经验最后一章说一个具体技巧写一个端到端的验证脚本把数据读入、特征工程、模型训练、评估串起来每次改完代码跑一遍确保结果可复现。这个脚本不需要多复杂但要有固定的随机种子、固定的数据划分、固定的评估指标输出。下面是一个最小示例import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import roc_auc_score def run_pipeline(seed42): df pd.read_csv(loan_data.csv) # 省略特征工程细节假设已经处理完 X df.drop(columns[default]) y df[default] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_stateseed, stratifyy ) model RandomForestClassifier(n_estimators200, random_stateseed) model.fit(X_train, y_train) prob model.predict_proba(X_test)[:, 1] auc roc_auc_score(y_test, prob) return auc if __name__ __main__: for s in [42, 2024, 7]: print(fseed{s}, AUC{run_pipeline(s):.4f})这个脚本跑三个随机种子如果 AUC 波动在 0.01 以内说明模型比较稳定如果波动超过 0.03说明模型对数据划分太敏感需要检查特征或增加数据量。两条经验第一大作业报告里不要只写一个 AUC 数字要写多次运行的均值和标准差这样才显得专业第二特征工程做完后一定要做特征重要性分析把重要性接近零的特征删掉既能提速又能减少过拟合。我自己做这类项目时习惯在每次改完特征后重新跑一遍验证脚本确认 AUC 没有下降才继续。希望帮到你。本文还有配套的精品资源点击获取