从银行营销数据到认购概率:Python机器学习建模实战
发布时间:2026/9/28 15:52:43来源:尧图网络
简介基于机器学习的银行客户认购产品预测项目是一套面向计算机专业毕业设计及项目实战学习的完整可运行源码包。项目围绕银行营销场景下的客户定期存款认购行为利用数据集完成清洗、可视化、特征构造与模型调优输出二分类预测结果可直接支撑毕业设计、课程设计或期末大作业。压缩包共66个文件、约9.39MB包括5个Python脚本类别编码、树变换、自动学习等、3个Jupyter Notebook建模分析文档、5个CSV训练/测试数据、2个Pkl模型文件预处理流水线与最优模型以及JSON日志、XLSX字段说明、四十三张PNG特征分布图等目录按code、dataset、figures、models、results清晰组织。目前已有481人学习下载尤其适合需要完整项目范例、快速复现预测流程、借鉴数据挖掘与机器学习实践思路的高校学生。通过该资源可获取从字段理解、数据探索到模型部署的全链路方案包含可视化统计图表、自动调参脚本及可直接调用的模型文件便于在此基础上扩展和继续研究。1. 这个项目在解决什么问题从“营销名单”到“认购概率”银行客户经理手头最不缺的就是名单最缺的是“该先打哪个电话”。一个季度几万条客户数据全靠人工经验去筛要么漏掉真正会买的客户要么把电销时间浪费在完全没意向的人身上。这个压缩包把整条链路做成了可复现的 Python 机器学习项目读入客户的历史数据训练一个二分类模型给每个客户输出一个“认购产品概率”最后按概率排序生成营销名单。源码、数据集、模型文件都齐了拿到手能直接复现训练过程也能把自己的数据灌进去换一套模型出来。适合正在做课程设计的学生、刚转行做数据分析想接触银行业务建模的新人以及需要把名单排序落地到营销系统里的从业者。下面按一个完整项目该有的顺序来拆先看数据和特征再训练和调参然后讲评估和业务口径最后把几个最容易翻车的点一次性说清楚。2. 先吃透数据集银行营销数据的字段、分布与特征工程2.1 数据集长什么样常见公开银行营销数据集的核心字段这类项目的数据集最常遇到的是 UCI 上的 Bank Marketing 数据集目录里一般会有一个 CSV 文件加一个字段说明文档。先不要急着训练第一步是把字段说明逐行读完搞清楚哪些是客户属性、哪些是营销动作的记录。常见字段大致分三类字段类别典型字段说明客户画像age、job、marital、education、default、balance、housing、loan年龄、职业、婚姻、学历、是否违约、余额、房贷、个人贷款营销记录contact、month、day_of_week、duration、campaign、pdays、previous、poutcome联系方式、联系月份、星期几、通话时长、本次营销次数、距上次联系天数、上次联系次数、上次结果标签y是否认购定期存款yes/no读取数据这一步代码很简单但有两个地方要额外看一是列类型是否被正确识别比如 balance 这类数值字段被读成字符串会导致后面标准化直接报错二是缺失值很多版本的公开数据集在 pdays 和 previous 里用 -1 和 0 表示“此前从未联系过”这是业务含义不是缺失不能随便填。import pandas as pd df pd.read_csv(bank_marketing.csv, sep;) print(df.shape) print(df.dtypes) print(df.isnull().sum())这个数据集里比较隐蔽的点是分隔符。有些版本用分号;有些用逗号先读前几行确认再决定 sep 参数。pdays 里大量 -1 表示“之前没联系过”做特征工程时当作一个独立状态处理而不是直接作为数值参与计算。如果有真正的缺失值None 或 NaN再决定填充策略不建议在没看分布前就统一填 0 或均值。2.2 特征工程的最小闭环分箱、哑变量与数值标准化特征工程在这个项目里不需要做得花哨但有几个动作不做模型效果会差一大截。数值特征里age、balance、campaign 的分布都严重偏斜尤其是 balance很多人余额是几百块少数人余额上百万。直接把原始值喂给模型不仅让逻辑回归的系数变得不稳定梯度提升树也会花更多分裂次数在极端值上。常见的做法是年龄分箱比如 18~30、31~40、41~50、51~60、60campaign本次营销联系次数按 1、2、3、4 分箱balance 和 previous 做对数变换后再标准化。分类特征用 one-hot 编码job 有十几种取值学历和联系方式也都有多个类别。import numpy as np # 年龄分箱 df[age_bin] pd.cut(df[age], bins[18, 30, 40, 50, 60, 99], labels[18-30, 31-40, 41-50, 51-60, 60]) # campaign 截断后分箱超过 5 次的归为一档 df[campaign_bin] pd.cut(df[campaign].clip(upper5), bins[0, 1, 2, 3, 5], labels[1, 2, 3, 4-5]) # balance 做 log1p 变换缓解右偏分布-1 和 0 不会报错 df[balance_log] np.log1p(df[balance].clip(lower0)) # 类别列统一转为字符串类别类型再用 get_dummies categorical_cols [job, marital, education, default, housing, loan, contact, month, day_of_week, poutcome, age_bin, campaign_bin] df[categorical_cols] df[categorical_cols].astype(category) X_cat pd.get_dummies(df[categorical_cols], prefixcategorical_cols, drop_firstFalse) # 数值列标准化 from sklearn.preprocessing import StandardScaler numeric_cols [balance_log] scaler StandardScaler() X_num pd.DataFrame(scaler.fit_transform(df[numeric_cols]), columnsnumeric_cols)代码里分箱用clip(upper5)是把极端值先压到 5 再分箱避免单独成一类后样本量太少。get_dummies加上drop_firstFalse是为了保留所有类别这样后面做特征重要性时能看清每个类别各自的方向。StandardScaler只喂了一个 balance_log实际使用时要记得把其他数值特征比如 duration 如果要用一起放进去。2.3 两个必须先剔除的特征duration 与 pdays 的时间穿越陷阱这一步是这个数据集里最容易让人误以为自己模型很牛的地方。duration 是“本次营销通话的时长”pdays 是“距离上次联系的间隔天数”。这两个字段有个共同特点它们描述的是营销动作发生后客户给到的反馈而不是营销动作发生前就已经知道的客户属性。如果目标是“在发起营销之前预测客户会不会买”那么打电话之前 duration 和 pdays 是不存在的。把这两个字段放进特征模型等于提前偷看了答案。我在第一次跑这类项目时AUC 直接干到 0.95还以为是调参调得好后来把 duration 删掉AUC 回到 0.80 左右这才意识到前一个模型学的是“通话时长越长越越可能买”对业务毫无用处。# 剔除事后特征只保留营销前就能拿到的客户信息与历史营销记录 exclude_cols [duration, pdays, y] X df.drop(columnsexclude_cols) y df[y].map({yes: 1, no: 0})这个剔除动作很多人会漏因为数据完整性和模型效果都很好看不出来问题。判断一个特征能不能用标准只有一个在预测时刻它是否已经确定。下次在上线新的营销名单前先拿这个标准过一遍全部特征能省下很多返工的时间。3. 训练与调参用逻辑回归打底、XGBoost 做主力3.1 分层划分与交叉验证为什么不能直接 train_test_split这个数据集的标签分布是极度不平衡的认购率一般在 10% 上下。如果不做分层采样随机划分时有一定概率让测试集里的正样本比例更低模型评估出来的指标一次一个样根本没法判断调参是否有效。from sklearn.model_selection import StratifiedKFold, train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) print(f训练集正样本占比: {y_train.mean():.4f}, 测试集正样本占比: {y_test.mean():.4f})stratifyy是关键它保证划分前后正样本占比保持一致。StratifiedKFold在交叉验证时同样会保留每折的类别比例这样每一折评估出的指标才有可比性。random_state42固定下来保证每次重跑结果一致后面排查问题时才不会因为“这次跑出来的不一样”而误判是代码改坏了。3.2 两个必调的 XGBoost 参数scale_pos_weight 与 max_depth逻辑回归在这个项目里适合作为 baseline优势是训练快、可解释系数直接能看出哪个特征对认购概率是正贡献。但逻辑回归对特征交互和阈值处理偏弱所以主力模型我一般直接用 XGBoost。它自带树模型的正则化能处理大规模稀疏 one-hot 特征而且训练速度快迭代调参的周期短。XGBoost 在这个项目里最值得动手调的两个参数是scale_pos_weight和max_depth。scale_pos_weight用来缓解类别不平衡它控制了正样本权重相对负样本权重的放大倍数通常设成负样本数除以正样本数max_depth控制树的深度设太大容易在正样本很少的情况下迅速过拟合设太小又学不到足够深的分裂规则。import xgboost as xgb neg_count (y_train 0).sum() pos_count (y_train 1).sum() scale_pos_weight_value neg_count / pos_count model xgb.XGBClassifier( n_estimators300, learning_rate0.05, max_depth4, subsample0.8, colsample_bytree0.8, scale_pos_weightscale_pos_weight_value, eval_metricauc, random_state42, n_jobs-1, ) model.fit( X_train, y_train, eval_set[(X_test, y_test)], verboseFalse, )这里scale_pos_weight neg_count / pos_count是按训练集实际比例动态计算而不是拍脑袋写固定值换一个数据集也能自动适配。learning_rate0.05配合n_estimators300先用小学习率让模型慢慢学树数量靠 early stopping 控制。max_depth4在大多数表格型数据上是一个不容易翻车的起点数据量小就降到 3数据量很大特征很多再试 6。subsample和colsample_bytree都给到 0.8相当于每次建树只抽八成的行和列对抑制过拟合很有帮助。3.3 early stopping 怎么设用测试集判断代价很高上面代码里eval_set直接传了测试集日常快速实验可以这样用但严格来说这是“用测试集指导训练”试多了会信息泄漏。更好的做法是从训练集再切一部分做验证集或者干脆用交叉验证里的某一折来做 early stopping。from sklearn.model_selection import cross_val_score # 先用 3 折交叉验证快速验证参数方向 scores cross_val_score( model, X_train, y_train, cv3, scoringroc_auc, n_jobs-1, ) print(fAUC: {scores.mean():.4f} ± {scores.std():.4f})交叉验证输出的是“这个参数组合在数据上是否稳定”的信号。AUC 平均值高、标准差小说明参数的泛化性好。标准差大于 0.02优先怀疑特征里还有噪声或样本量不足此时先别急着加树的数量而应该回去看特征。4. 模型评估要看业务账准确率会骗人AUC 之外还要看召回率和提升度4.1 混淆矩阵与三类指标91% 的准确率为什么可能完全没用银行营销场景里正样本认购客户占比通常在 10% 上下。一个把所有客户都判为“不会买”的模型准确率也能达到 90% 左右但这个模型对业务零价值。只看准确率是这类项目最常见的错误。from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score y_pred model.predict(X_test) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names[no, yes])) print(fAUC: {roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]):.4f})classification_report里的 recall 是最该看的在模型预测为“会买”的客户里真正买了的人占比是多少在所有真实买了的客户里预测正确覆盖了多少。前一个指标是精确率后一个是召回率。业务上外呼资源有限更多时候要精确率优先因为打给错误客户浪费的是人力但如果目标是“尽可能不错过任何潜在客户”召回率优先级会更高。AUC 给出的是“随机挑一个正样本和一个负样本模型把正样本排在负样本前面的概率”它不依赖阈值用来判断特征和模型本身好不好更稳。4.2 阈值扫描把 proba 变成一张能执行的外呼名单XGBoost 默认用 0.5 作为判定阈值但在正样本只有 10% 的数据里0.5 通常太高导致所有客户都被判成 no。实际项目中模型输出的predict_proba列才是真正要用的东西阈值则根据手头能打多少电话来倒推。import pandas as pd prob model.predict_proba(X_test)[:, 1] results pd.DataFrame({ y_true: y_test.values, prob: prob, }) results results.sort_values(prob, ascendingFalse).reset_index(dropTrue) top_k 200 top_recall results[y_true].head(top_k).mean() overall_recall y_test.mean() lift top_recall / overall_recall print(f全量认购率: {overall_recall:.4f}) print(fTop{top_k} 认购率: {top_recall:.4f}) print(f提升度 Lift: {lift:.2f})这个“Lift”才是给业务方看的核心数字同样打 200 个电话按模型排序打能触达的认购客户是随机打的多少倍。模型训练得再复杂最终的价值就体现在这个倍数上。Lift 大于 1 说明模型有筛选能力大于 2 说明有实用价值如果接近 1说明前期的数据或特征工程有方向性问题。5. 项目里最容易翻车的四个点从数据泄漏到模型文件打不开5.1 特征穿越时间AUC 虚高 0.1 的元凶现象训练完模型 AUC 高达 0.93~0.95测试集表现异常地好。原因把 duration、pdays 这类营销结果产生后的字段放进了训练特征模型实际是在用“结果预测结果”。解决每次训练前统一用“在预测时刻该字段是否已知”这个标准筛选特征列。删除持续时间和距上次联系天数后模型 AUC 会回落到 0.80 左右这才是真实水平。源码里如果跑出的指标比业界同类项目高出一大截先怀疑数据泄露再怀疑自己调参很强。5.2 类别不平衡被无视准确率虚高、正样本一个都捞不到现象测试集准确率 89%一看召回率只有不到 5%所有真正认购的客户几乎都没预测出来。原因模型在负样本占绝对多数时把全部样本判成 no 就能把损失降到最小。解决在逻辑回归里设置class_weightbalanced在 XGBoost 里设置scale_pos_weightneg/pos然后结合混淆矩阵检查正样本召回率是否明显抬升。注意这两个参数要配合阈值调整一起用否则正样本召回率上来了精确率可能又会掉得很厉害。5.3 哑变量列顺序不一致预测时特征数量对不上现象用保存好的模型文件对新数据做预测直接报Feature shape mismatch或者结果全是同一个值。原因训练时用pd.get_dummies生成的列集合和预测数据经过同样处理后生成的列集合容易因某个类别缺失或顺序不同而对不上。解决用sklearn.preprocessing.OneHotEncoder替代 get_dummies在训练集上fit在预测集上transform这样列名和列顺序都由编码器统一管理。或者更简单一点训练后把特征列名列表joblib.dump一起保存预测时用X_forecast X_forecast[column_names]强制对齐再丢进模型。5.4 模型文件加载失败不同环境之间版本冲突现象在自己机器上训练好并保存的模型文件换一台机器或换一个 Python 环境加载时报ModuleNotFoundError或UnpicklingError。原因XGBoost 和 scikit-learn 的版本在迭代中会调整模型的序列化格式高版本训练出的模型文件低版本库往往不认。解决保存模型时同时保存模型文件和模型参数在项目目录里放一个 requirements.txt记录 Python、scikit-learn、XGBoost 的具体版本。如果只是临时看结果用 xgboost 原生接口训练并保存为 json 格式比直接 pickle 更稳至少加载失败的概率低一些。源码包里如果带了预训练模型优先确认它是在哪个版本环境下导出的。6. 把模型落成一个固定 Pipeline换数据不再反复跳坑单独把模型文件保存下来只能管一时下一次换一批客户数据、换一个月份的产品活动又要重新训练、重新评估、重新踩一遍前面的坑。我一般会直接把整个预处理、训练、预测串成一个Pipeline保存下来之后新数据进来只需要跑一个transform再加一个predict_proba。from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler # 统一保存预处理器和模型避免预测阶段重复写特征工程代码 pipeline Pipeline(steps[ (scaler, StandardScaler()), (model, model), ]) # 训练 pipeline.fit(X_train, y_train) # 保存 import joblib joblib.dump(pipeline, bank_product_model.pkl) # 预测新客户名单 def score_new_customers(new_df, pipeline_pathbank_product_model.pkl): loaded joblib.load(pipeline_path) prob loaded.predict_proba(new_df)[:, 1] return prob这里Pipeline的价值在于把预处理和模型绑成一个整体预测新数据时scaler 用的是训练集上拟合的均值方差而不是重新对预测数据集拟合这一步经常有人做错。新数据进predict_proba之前还需要用同一个 OneHotEncoder 做变换所以规范的落地方式是把 OneHotEncoder 也一起塞进 Pipeline这样从原始数据到概率分只有一次调用。收个尾这类银行营销预测项目模型热身容易真正拉差距的地方永远是数据清洗、特征剔除、评估口径。我在最早跑通类似项目的时候看到 AUC 高就兴奋得不行直到发现是 duration 字段在作弊才明白一个能落地的模型得先经得住业务上“为什么要信它”的质询。把 feature engineering 和模型评估的每一步都留痕、都可回溯后来的路会顺很多。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网