
简介这是一份基于机器学习的银行客户认购产品预测完整项目适合计算机专业毕业设计、课程设计与期末大作业。项目围绕银行营销场景展开包含数据预处理、特征工程、模型训练与评估等完整流程提供多个版本的可运行 Jupyter Notebook 与 Python 脚本可直接复现或二次开发。压缩包共 65 个文件约 9.58MB核心内容涵盖 csv 数据集、字段说明 xlsx、43 张可视化分析图、pkl 模型文件以及 json 配置与日志并附最优模型与预处理流水线其中包含 train/test/submission 数据、三版建模 notebook、自动化学习脚本与特征编码工具结构清晰便于对照学习。目前已有 572 人学习下载。项目经导师指导并严格调试可直接作为毕设源码使用也能帮助学习者快速掌握银行营销场景下的分类预测建模思路与工程实践细节。1. 银行客户认购预测用Python和机器学习跑通一个完整的毕业设计项目银行电话营销有个很现实的痛点客户经理一天打几百通电话真正愿意认购定期存款的可能不到十分之一。这个标题指向的项目正是用Python和机器学习对银行客户的历史数据和电话营销记录建模在拨号之前先预测客户的认购概率把精力集中到高意向客户上。它同时包含了项目源码、数据集和模型文件是一个训练、评估、保存、复用链路完整的毕业设计包不是那种只有空壳代码的演示项目。适合拿它做毕业设计、数据挖掘课程设计或者想系统走一遍机器学习建模流程的入门者。你不需要自己造数据也不需要从零搭框架跟着下面这条路径能把预测模型从数据集一路跑到模型文件落盘还能说清楚每个环节为什么这么做。2. 理解预测任务银行营销数据集的特征含义与评估指标怎么定2.1 bank数据集里每一列在描述什么这类项目最常见的数据集是银行营销数据集典型文件是bank.csv或bank-full.csv以分号作为分隔符。每条记录代表一次电话营销活动中某个客户的特征和最终结果目标列是客户是否认购了定期存款yes/no。先把这个表里的特征按业务含义分成几组后面做特征工程时心里才有底。客户属性包括age年龄、job职业类型、marital婚姻状况、education教育水平、default是否有信用违约、balance年平均账户余额、housing是否有住房贷款、loan是否有个人贷款。这批字段描述的是客户本身的经济状况和风险画像建模时的直觉是一个有稳定职业、有存款、没有违约记录的客户认购定期存款的概率会更高。营销活动属性包括contact联系方式手机或座机、day和month上次联系的日期和月份、duration上次联系持续时长以秒为单位、campaign本次活动中联系该客户的次数、pdays上次活动后到本次联系过去的天数如果是首次联系则为-1、previous本次之前联系该客户的次数、poutcome上次营销活动的结果。这里有个关键点pdays取-1代表客户从未被联系过这个约定在清洗时不能按普通数值处理否则会污染数值分布。另一类常见的数据集字段是“emp.var.rate”就业变动率和“euribor3m”三月期欧元银行间拆借利率这类宏观指标它们描述了联系当月的经济环境。宏观特征和个体特征放在一起建模模型能捕捉到“经济上行期客户更愿意投资”这类规律。特征列看着很多但真正建模时不是全塞进去就完事。数值特征和类别特征要区别处理类别特征里的“unknown”要单独决定是保留还是合并宏观特征和个体特征之间可能还有多重共线性这些都会在第3章逐一展开。2.2 二分类还是概率排序业务目标决定建模方式标题里说的是“预测客户是否认购产品”表面上看是个标准的二分类问题输出0或1客户买还是不买。但实际落地时银行真正需要的不只是这个二进制答案而是每个客户的认购概率排序。理由很简单如果模型只是输出1和0你得到的是一份“会买”和“不会买”的名单但客户经理每天能触达的人数有限排在前面的应该是预测概率最高的那批客户。同一个模型逻辑回归输出predict_proba、XGBoost输出pred_proba拿到的是0到1之间的连续值按概率倒序排列就能排出销售线索的优先级。所以建模时我会同时保留预测类别和预测概率两个输出评估指标也围绕排序能力来选。这也直接影响训练目标的设计。如果你的模型用交叉熵损失函数学的其实是概率分布而不是简单的最小化误分类个数。后面不管是调class_weight还是调阈值都是在“概率排序”这个基础上做文章。理解这一步就不会在拿到模型后只盯着准确率看。2.3 为什么准确率会骗人AUC、召回率与阈值的取舍银行客户认购定期存款的比例通常在10%到15%之间这属于典型的类别不平衡场景。如果模型把所有人都预测为“不买”准确率也能有85%以上看起来效果不错实际上一单都捞不到。这就是为什么准确率在这个任务上不能作为唯一指标。常用指标及其适用场景可以这样看指标计算方式在银行认购预测中的意义准确率正确预测数 / 总样本数类别不平衡时容易虚高参考价值有限精确率预测为“买”且实际也买的人数 / 预测为“买”的总人数衡量“筛出来的人群有多准”误报成本高时看它召回率预测为“买”且实际也买的人数 / 实际购买总人数衡量“真正想买的人被捞出了多少”漏报成本高时看它AUCROC曲线下面积衡量模型把正样本排在负样本前面的能力类别不平衡下依旧稳健KS值正样本与负样本累计分布的最大差值衡量模型区分度风控领域常用在银行这个场景里我会优先看AUC和召回率。AUC告诉你模型会不会排顺序召回率告诉你名单里有没有漏掉大客户。精确率和准确率作为参考但要记住它们受阈值影响很大调低阈值精确率会下降、召回率会上升这是一个跷跷板。模型训练完成后用ROC曲线找约登指数Youdens Index对应的最优阈值而不是死等默认的0.5这是后文第5章会展开的一个关键点。评估指标的选择从数据探索阶段就要想好因为它直接决定你在调参时用哪个score函数。3. 数据清洗与特征工程建模前30分钟决定模型上限3.1 先用pandas摸清数据家底分布、缺失值和unknown拿到项目里的数据集第一步不是建模而是把数据的分布、缺失情况、目标变量比例看清楚。以bank.csv为例读取时要注意分隔符是分号而不是逗号。下面的代码展示了这套项目里最基础的数据探查流程import pandas as pd import numpy as np # 读取银行营销数据集分隔符是分号 df pd.read_csv(bank.csv, sep;) print(数据集形状:, df.shape) print(目标变量分布:) print(df[y].value_counts(normalizeTrue)) # 检查缺失值银行数据中缺失常用 unknown 标记不能用 isnull 直接查 missing_cols df.isin([unknown]).sum() print(unknown 缺失分布:) print(missing_cols[missing_cols 0]) # 数值特征的描述统计 print(df.describe().T)这段代码里有两个地方容易翻车一是sep;用默认的逗号解析会把整行读成一个字段二是缺失值检查用的是isin([unknown])而不是isnull()因为银行原始数据中职业、教育、联系方式等字段的缺失值是以unknown字符串形式存在的。如果直接当正常类别处理模型会学出一套“unknown客户”的规律如果直接删掉又可能丢失样本。describe输出里重点关注balance和duration这类数值特征的均值、分位数和最大值。如果最大值和75%分位数的差距特别大说明存在长尾分布下游模型可能需要做log变换或裁剪。目标变量分布用normalizeTrue看比例如果“买”的比例低于15%后续建模就要考虑类别不平衡处理。这个阶段不急着做任何清洗但要记录下异常点形成对数据的整体手感。3.2 类别特征编码为什么LabelEncoder不适合job和maritaljob、marital、education、contact这些都是类别特征机器学习模型没法直接吃字符串必须转成数值。最常见的做法是LabelEncoder和OneHotEncoder但选错编码方式会埋下隐患。from sklearn.preprocessing import LabelEncoder # 错误示范对 job 做 LabelEncoder会引入业务上不存在的顺序关系 le LabelEncoder() df[job_le] le.fit_transform(df[job]) print(LabelEncoder 给 job 编码结果:, df[job_le].unique()) # 输出可能类似 [0 1 2 3 4 5 ...]但 admin. 和 blue-collar 之间没有大小关系 # 正确做法用 one-hot 编码保留类别之间的独立性 df_encoded pd.get_dummies(df, columns[job, marital, education], drop_firstTrue) print(one-hot 编码后的形状:, df_encoded.shape)LabelEncoder的问题在于它会给类别赋予0、1、2、3这样的整数顺序模型会默认这些数字之间有大小关系比如“blue-collar”蓝领被编码成1、“entrepreneur”企业家被编码成2模型就会认为企业家这个类别在数值上大于蓝领这个逻辑在业务上站不住。get_dummies或OneHotEncoder把每个类别拆成独立的0/1列虽然列数变多但不会引入虚假的顺序。drop_firstTrue的目的是避免“虚拟变量陷阱”n个类别只需要n-1个0/1列就能表示全部信息多出来的那一列和其他列存在完全共线性逻辑回归这类线性模型会对这种共线性非常敏感。如果后续要使用树模型one-hot还是LabelEncoder影响没那么大但我建议养成统一用one-hot的习惯这样在逻辑回归到XGBoost之间切换时不用回头改数据。编码器在训练集上fit后一定要保存测试集和预测阶段要复用同一个编码器这个坑在第5章会详细说。3.3 duration特征相关性最高但是一把双刃剑在所有特征里duration上次联系持续时长和认购结果的相关性通常是最高的通话时间长说明客户愿意听下去购买概率自然高。但这个特征在真实落地时有严重的数据泄漏风险预测发生在拨号之前而duration是拨号之后才有的信息。建模时你不可能提前知道这通电话会打多久。# 查看数值特征与目标变量的相关性 numeric_cols df.select_dtypes(include[np.number]).columns corr df[numeric_cols].corr()[y].sort_values(ascendingFalse) print(corr)在这个项目的数据探索阶段你大概率会看到duration的相关系数显著高于其他特征。针对这种情况有一个常见做法建模前直接去掉duration用其他特征来预测或者做一个保守版本把duration单独挑出来只在“电话已结束”的复盘场景里使用。如果用完整特征训练并在预测时也没有duration这个模型在评估集上看起来AUC很高上线后却会因为特征缺失直接报错或性能骤降。我一般会做一个无duration版本和一个含duration版本做对比。毕设答辩时能把“为什么放弃一个高相关特征”讲清楚比把模型AUC堆高几分更有说服力。这个处理思路同样适用于pdays-1表示从未联系过这个标记本身是一个有效信息但它和previous、poutcome之间的组合含义需要在特征工程阶段一并思考不要简单把-1当作缺失值填掉。4. 模型训练与调优从逻辑回归到XGBoost的落地路径4.1 用分层采样划分训练集和测试集划分数据集时如果直接随机切类别不平衡的数据集在训练集和测试集上的正样本比例可能差很多。比如全量数据里“买”占11%随机切完后训练集可能变成12%测试集变成8%评估结果就失真了。解决方式是stratify参数按目标变量的比例分层抽样。from sklearn.model_selection import train_test_split # 特征和标签分离y 转为 0/1方便后续计算 AUC X df_encoded.drop(y, axis1) y (df[y] yes).astype(int) # stratifyy 保证训练集和测试集的正样本比例一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) print(训练集正样本比例:, y_train.mean()) print(测试集正样本比例:, y_test.mean())stratify参数是这里的关键。设置stratifyy之后训练集和测试集里的正样本比例会尽量接近原始数据集两个比例应该差不多。random_state固定为42保证每次跑出来的划分一致方便复现别人的实验结果也能让你在调参时确认指标的变化来自参数而不是数据划分的随机抖动。test_size取0.2或0.3都可以如果数据集本身只有几千条建议用0.2给训练集多留一些样本。另外还有一个更严格的划分思路如果数据带月份字段可以在划分时按月份切分用前面的月份做训练、最后一个月做验证。这种时间外验证更接近上线后的真实场景在第6章会给出代码示例。4.2 第一个基准模型逻辑回归与class_weight逻辑回归是这类表格数据最稳妥的基准模型训练快、可解释性强而且给后续树模型提供一个对比的基线。如果逻辑回归就能拿到不错的AUC那项目里更复杂的模型是否有必要需要掂量一下。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.metrics import roc_auc_score, classification_report # 逻辑回归对特征尺度敏感先做标准化 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # class_weightbalanced 缓解类别不平衡 model LogisticRegression(max_iter1000, C1.0, class_weightbalanced) model.fit(X_train_scaled, y_train) # 用 predict_proba 取正样本概率不要用 predict 出来的 0/1 去算 AUC y_prob model.predict_proba(X_test_scaled)[:, 1] print(AUC:, roc_auc_score(y_test, y_prob)) print(classification_report(y_test, (y_prob 0.5).astype(int)))fit_transform和transform要分开用对训练集做fit_transform学习均值和方差对测试集只做transform否则会把测试集的统计信息泄漏到模型里。class_weightbalanced让模型在训练时给少数类更高权重。如果不用这个参数逻辑回归会把所有样本都推向“不买”AUC看起来还行但recall会非常难看。max_iter1000是防止默认的100次迭代在特征多时不收敛如果训练时报“ConvergenceWarning”优先加大max_iter而不是调低C。C是正则化强度的倒数C越小正则化越强默认1.0可以先跑通后面用网格搜索去调。逻辑回归训练完要记录AUC和classification_report这个AUC就是后面所有模型对比的基准。这里拿到的y_prob不要丢掉后面调阈值时还要用。4.3 升级到XGBoost树模型在表格数据上的优势逻辑回归的瓶颈在于它只能捕捉特征的线性关系而银行客户是否认购往往存在复杂的交叉影响比如“年轻且高余额的客户”和“中年且有住房贷款的客户”可能有完全不同的购买模式。XGBoost这类梯度提升树模型能自动学习特征之间的非线性交互在表格数据上通常比逻辑回归强一截。from xgboost import XGBClassifier from sklearn.metrics import roc_auc_score xgb_model XGBClassifier( n_estimators200, max_depth4, learning_rate0.1, subsample0.8, colsample_bytree0.8, scale_pos_weight8, # 正负样本不平衡时使用约等于 负样本数/正样本数 random_state42, eval_metricauc ) xgb_model.fit(X_train, y_train) y_prob_xgb xgb_model.predict_proba(X_test)[:, 1] print(XGBoost AUC:, roc_auc_score(y_test, y_prob_xgb))scale_pos_weight是XGBoost处理类别不平衡最直接的手段值设为负样本数除以正样本数。如果正样本占11%负样本占89%那这个值大约就是8。它起到的作用和逻辑回归里的class_weightbalanced类似都是给少数类更高的惩罚权重。eval_metricauc让模型在训练时每轮迭代都会输出AUC指标方便观察模型是否过拟合。XGBoost对特征尺度不敏感所以这里不需要再做标准化直接用原始特征就能训练。max_depth4比默认的6要浅控制模型复杂度subsample和colsample_bytree都是0.8相当于每棵树只用80%的样本和80%的特征增加随机性。树模型在这种表格数据上表现好的原因之一是它能自动处理one-hot展开后的稀疏特征不需要提前做特征筛选。如果环境里没装XGBoost用sklearn的GradientBoostingClassifier也能达到接近的效果只是训练速度慢一些。对比逻辑回归和XGBoost的AUC如果XGBoost只高出0.01那在答辩时可以强调逻辑回归的可解释性优势如果高出0.05以上那XGBoost就是主角。4.4 网格搜索调参参数怎么设才不浪费时间参数调优最容易犯的错误是一上来就铺开一个巨大的参数网格比如把max_depth从1到10、learning_rate从0.01到0.5全部放进去一个网格搜索跑几个小时还不见得收敛。正确做法是先粗调后细调每次只动两三个参数。from sklearn.model_selection import GridSearchCV # 先固定 n_estimators 和 scale_pos_weight只调树的深度和学习率 param_grid { max_depth: [3, 4, 5], learning_rate: [0.05, 0.1], subsample: [0.7, 0.8, 0.9] } grid GridSearchCV( XGBClassifier(n_estimators100, scale_pos_weight8, random_state42), param_grid, scoringroc_auc, # 网格搜索也按 AUC 打分不用准确率 cv5, n_jobs-1 ) grid.fit(X_train, y_train) print(最优参数:, grid.best_params_) print(最优AUC:, grid.best_score_)scoringroc_auc决定了网格搜索的每个参数组合用什么指标评判在类别不平衡场景下必须用它而不是默认的准确率。cv5表示5折交叉验证每一组参数都要训练5次网格越大耗时越长。n_jobs-1表示用满CPU所有核心如果电脑配置一般可以改成n_jobs4。一个有效的搜索顺序是先固定n_estimators为100调max_depth和learning_rate找到合适的深度后再调subsample和colsample_bytree最后再回头适当增加n_estimators。XGBoost里n_estimators和learning_rate是联动关系learning_rate越小需要的棵树越多如果learning_rate从0.1降到0.05n_estimators最好翻倍。网格搜索跑完要把grid.best_estimator_重新在完整训练集上fit一遍吗在GridSearchCV里fit完成后best_estimator_已经用全量训练集重新训练过了直接用就行。这里还有一个常见的细节网格搜索里的X_test不能参与交叉验证只能作为最终评估使用否则有信息泄漏。5. 模型文件保存与项目复现四个典型排查记录5.1 模型文件怎么存pickle、joblib和依赖版本项目标题里带了“模型文件”说明训练完的模型需要保存下来而不是每次预测都重新训练。常见的保存方式是用joblib或pickle序列化下面这套操作会把模型、标准化器、特征列表一并落盘import joblib # 保存模型和标准化器grid.best_estimator_ 是网格搜索选出的最优模型 joblib.dump(grid.best_estimator_, bank_model.joblib) joblib.dump(scaler, scaler.joblib) # 保存特征列名预测阶段的输入必须按这个顺序排列 feature_names list(X_train.columns) joblib.dump(feature_names, feature_names.joblib) # 加载模型做预测 model joblib.load(bank_model.joblib) loaded_scaler joblib.load(scaler.joblib) loaded_features joblib.load(feature_names.joblib)pickle和joblib都能保存模型但joblib对包含numpy数组的对象比如sklearn模型内部参数效率更高。这里有一个很关键的细节只保存模型和scaler还不够特征列名的顺序必须也存下来。比如源数据经过get_dummies后列顺序是固定的但预测阶段换了一台机器重新加载数据列顺序可能因为pandas版本不同而改变模型接收到的特征顺序和训练时不一致预测结果就会出错。模型的版本管理也是一个容易被忽略的点。加载模型报错的常见原因是sklearn或xgboost版本不一致。跨Python版本、跨sklearn版本加载模型轻则警告重则直接抛异常。建议在项目目录下维护一个requirements.txt把numpy、pandas、scikit-learn、xgboost的版本号固定下来作为模型文件的配套交付物。这也是毕业设计源码包里除了代码和模型文件之外最应该附上的文件。5.2 排查一预测结果全是“0”模型根本没学到正样本现象训练完模型后评估测试集发现predict输出的全是0AUC却可能有0.7以上召回率是0。原因这是类别不平衡和小阈值共同作用的结果。数据集里正样本只占11%模型通过class_weight或scale_pos_weight学到了正样本的规律但预测概率普遍偏低默认阈值0.5落在所有预测概率之上导致所有样本都被判为负类。AUC衡量的是排序能力即使概率都很低只要正样本的概率高于负样本AUC依然不会太差但0.5这个阈值完全不适用。解决用ROC曲线重新选定阈值通常是找约登指数敏感度 特异度 - 1的最大值点。from sklearn.metrics import roc_curve fpr, tpr, thresholds roc_curve(y_test, y_prob) # 约登指数最大的位置对应最优阈值 optimal_idx (tpr - fpr).argmax() optimal_threshold thresholds[optimal_idx] print(最优阈值:, optimal_threshold)拿到最优阈值之后预测时用(y_prob optimal_threshold).astype(int)替代(y_prob 0.5).astype(int)这通常是模型效果从“看起来没用”到“真的能用”的分水岭。这个阈值要在训练集的交叉验证里确定不要在测试集上直接选否则会过拟合到测试集。测试集只在最后评估一次。5.3 排查二预测时报特征数量不一致OneHotEncoder没保存现象训练阶段正常但加载模型对单条新数据进行预测时报错信息类似“X has 12 features, but LogisticRegression is expecting 18 features”。原因这是典型的编码器使用不一致问题。训练时用get_dummies对job、marital等列做了one-hot编码得到了18列预测阶段如果直接对一份新的原始数据重新执行get_dummies某个在训练集中出现过的职业类别在新数据里没出现或者训练集中没出现过的职业类别在新数据里出现了展开后的列数和顺序怎么可能对得上。解决训练时保存特征列名预测时按列名对齐数据。更稳妥的做法是训练阶段把OneHotEncoder对象也保存下来预测时直接复用。# 训练时保存 get_dummies 用的列名 feature_names list(X_train.columns) joblib.dump(feature_names, feature_names.joblib) # 预测阶段按保存的列名对齐数据 X_new pd.get_dummies(raw_new_data, columns[job, marital, education]) # 如果缺少某些列补0如果多了某些列删掉 X_new X_new.reindex(columnsfeature_names, fill_value0)reindex在这里的fill_value0很关键新数据里某个职业类别没有出现对应列用0填充新数据里出现了一个训练时没见过的职业类别get_dummies会多生成一列reindex会自动把它丢掉。这样能保证预测阶段的输入列和训练阶段完全一致。5.4 排查三AUC很高但业务落地效果差阈值陷进现象模型AUC做到0.85看上去很不错但在实际客户名单筛选时圈定的高概率客户并没有想象中那么高的购买率。原因AUC衡量的是相对排序不是绝对概率校准。它告诉你模型能把正样本以多大概率排在负样本前面但模型输出的概率绝对值可能整体偏高或偏低。比如模型对所有人都输出0.3到0.6之间的概率排序是准的但0.5以上的“高概率”名单可能根本不存在。另外训练数据和真实业务的先验概率差异也会让概率失真比如训练集里的正样本是被人工筛选过的和自然流量不同。解决如果要使用概率绝对值需要做概率校准。sklearn提供CalibratedClassifierCV可以通过Platt缩放或Isotonic回归把模型输出概率校准到接近真实概率。from sklearn.calibration import CalibratedClassifierCV # 在训练集上对模型做概率校准使用交叉验证避免过拟合 calibrated_model CalibratedClassifierCV( model, methodsigmoid, cv3 ) calibrated_model.fit(X_train, y_train) calibrated_prob calibrated_model.predict_proba(X_test)[:, 1]放业务里我一般把概率校准后的值当作真实购买率来看复试完再看看不同概率档位0.1、0.2、0.3的实际购买率验证能不能对齐。sigmoid方法适合样本量不大、概率分布单调的情况isotonic方法拟合能力更强但容易过拟合样本量太小时不推荐。校准后的概率如果还是系统性偏高或偏低大概率是训练集采样方式和实际业务不一致这时候要回头检查训练数据的来源。5.5 排查四换个环境加载模型失败版本管理混乱现象在A机器上训练并保存的模型文件拷贝到B机器后加载时报错。错误五花八门有报“module not found”的有报“incompatible version”的还有的是序列化数据解析失败的。原因模型文件本质是序列化的Python对象记录了生成它的类结构和所在模块信息。sklearn不同版本之间类名和参数可能有变动xgboost更是频繁更新跨大版本加载很常见。Python版本差异次之numpy版本影响也偶有发生。解决训练环境里用pip freeze或手动维护requirements.txt把主要依赖版本固定模型文件统一在使用同样版本依赖的环境里加载。pip freeze requirements.txt加载模型时也可以增加一个友好的异常捕获排查问题更快import traceback try: model joblib.load(bank_model.joblib) except Exception as e: print(模型加载失败请检查依赖版本是否一致) traceback.print_exc()这里的最佳实践是在模型文件旁边放一份README或model_card.txt写清楚训练环境的Python版本、sklearn版本、xgboost版本和关键特征列表。毕设答辩或交接时这些信息比模型文件本身更重要。如果对方环境版本确实不一致最快的办法是按requirements.txt创建虚拟环境而不是在现有环境里强行兼容。6. 从“能跑”到“能答辩”模型解释性、部署与效果验证6.1 用SHAP解释模型答辩时最加分的一步模型跑完、AUC达标只是一个开始。答辩时老师最常问的问题就是“你的模型凭什么做出这个判断”。SHAP是目前解释树模型最主流的方式能准确计算出每个特征对每个样本的贡献值import shap # 用树模型创建解释器X_test 可以抽样一部分以加快计算 explainer shap.TreeExplainer(xgb_model) shap_values explainer.shap_values(X_test[:500]) shap.summary_plot(shap_values, X_test[:500])summary_plot输出每个特征对模型预测贡献度的排序你能直接看到哪些特征把预测推向“认购”、哪些推向“不认购”。和前面duration特征的分析对照起来会让整个项目的逻辑链更完整。6.2 用Flask把模型包成Web接口做完训练和评估把模型封装成一个接口会让项目显得更完整。最简单的做法是用Flask搭一个预测服务from flask import Flask, request, jsonify import joblib import pandas as pd app Flask(__name__) model joblib.load(bank_model.joblib) scaler joblib.load(scaler.joblib) feature_names joblib.load(feature_names.joblib) app.route(/predict, methods[POST]) def predict(): data request.get_json() df pd.DataFrame([data])[feature_names] # 按特征列名对齐 prob model.predict_proba(scaler.transform(df))[0][1] return jsonify({probability: float(prob)}) if __name__ __main__: app.run(host0.0.0.0, port5000)这一步能体现出对“模型文件”价值的理解模型文件不只是训练完的存档而是可以被业务系统消费的资产。6.3 用时间外数据验证模型效果上一招防过拟合评估模型最容易被高估的方式是用随机划分的测试集因为随机划分出来的测试集和训练集来自同一时间段模型在分布上占了便宜。如果数据集带时间字段更严格的做法是按时间切分用前几个月的样本训练、最后一个月的样本验证。拿这种时间外验证结果对比随机划分的AUC差值越大说明模型的泛化能力越不稳定。我个人的习惯是模型文件保存前把随机划分的AUC、时间外验证的AUC、最优阈值和校准后的概率分布这四样记录进一个txt文件跟模型文件放一起。这样无论是自己复盘还是答辩展示都能讲清楚模型的真实边界在哪里。项目做到这一步技术含量和说服力都不缺了。希望这份笔记能帮你在跑通这个项目时少走几步弯路。本文还有配套的精品资源点击获取