news 2026/9/28 15:52:39

从银行营销数据到认购概率:Python机器学习建模实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从银行营销数据到认购概率:Python机器学习建模实战

简介:基于机器学习的银行客户认购产品预测项目,是一套面向计算机专业毕业设计及项目实战学习的完整可运行源码包。项目围绕银行营销场景下的客户定期存款认购行为,利用数据集完成清洗、可视化、特征构造与模型调优,输出二分类预测结果,可直接支撑毕业设计、课程设计或期末大作业。压缩包共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、60+;campaign(本次营销联系次数)按 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(upper=5), bins=[0, 1, 2, 3, 5], labels=["1", "2", "3", "4-5"]) # balance 做 log1p 变换,缓解右偏分布,-1 和 0 不会报错 df["balance_log"] = np.log1p(df["balance"].clip(lower=0)) # 类别列统一转为字符串类别类型,再用 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], prefix=categorical_cols, drop_first=False) # 数值列标准化 from sklearn.preprocessing import StandardScaler numeric_cols = ["balance_log"] scaler = StandardScaler() X_num = pd.DataFrame(scaler.fit_transform(df[numeric_cols]), columns=numeric_cols)

代码里分箱用clip(upper=5)是把极端值先压到 5 再分箱,避免单独成一类后样本量太少。get_dummies加上drop_first=False是为了保留所有类别,这样后面做特征重要性时能看清每个类别各自的方向。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(columns=exclude_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_size=0.2, random_state=42, stratify=y ) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) print(f"训练集正样本占比: {y_train.mean():.4f}, 测试集正样本占比: {y_test.mean():.4f}")

stratify=y是关键,它保证划分前后正样本占比保持一致。StratifiedKFold在交叉验证时同样会保留每折的类别比例,这样每一折评估出的指标才有可比性。random_state=42固定下来,保证每次重跑结果一致,后面排查问题时才不会因为“这次跑出来的不一样”而误判是代码改坏了。

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_estimators=300, learning_rate=0.05, max_depth=4, subsample=0.8, colsample_bytree=0.8, scale_pos_weight=scale_pos_weight_value, eval_metric="auc", random_state=42, n_jobs=-1, ) model.fit( X_train, y_train, eval_set=[(X_test, y_test)], verbose=False, )

这里scale_pos_weight= neg_count / pos_count是按训练集实际比例动态计算,而不是拍脑袋写固定值,换一个数据集也能自动适配。learning_rate=0.05配合n_estimators=300,先用小学习率让模型慢慢学,树数量靠 early stopping 控制。max_depth=4在大多数表格型数据上是一个不容易翻车的起点,数据量小就降到 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, cv=3, scoring="roc_auc", n_jobs=-1, ) print(f"AUC: {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(f"AUC: {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", ascending=False).reset_index(drop=True) 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(f"Top{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_weight="balanced",在 XGBoost 里设置scale_pos_weight=neg/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_path="bank_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 和模型评估的每一步都留痕、都可回溯,后来的路会顺很多。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 15:52:39

CM311-5刷机实战:先认GK6323芯片再刷安卓9固件

1. 为什么CM311-5刷机要先看芯片代号而不是只看型号1.1 同型号不同芯:CM311-5的硬件变体先说个我自己的教训。手里这台魔百和CM311-5是帮亲戚搞的,他之前在网上看教程,照着别人的CM311-5刷机教程走了大半,结果刷到一半发现进度条永…

作者头像 李华
网站建设 2026/9/28 15:52:07

Python虚假新闻检测:BERT向量融合LightGBM与CatBoost的混合模型实战

简介:基于Python搭建的多模态虚假新闻检测项目,融合文本与图像特征对新闻真实性进行自动识别,面向计算机、人工智能、通信工程、自动化等专业的高校学生和开发者。资源可在毕设答辩、课程设计、项目初期演示中直接使用,也适合作为…

作者头像 李华
网站建设 2026/9/28 15:51:37

多模态Agent统一架构:理解、生成、行动如何闭环

过去两年我一直在做多模态 Agent 相关的项目,也看过不少团队在这个方向上的挣扎。现在的 Agent 给你感觉什么都能聊两句,但真让它去完成一项需要"看懂 — 想清楚 — 做出来"的完整任务,立刻露馅。这种割裂感不是某一个模型的问题&a…

作者头像 李华
网站建设 2026/9/28 15:51:34

深色皮肤皮肤病YOLO数据集实战:234张图像训练与避坑指南

简介:面向yolo系列算法目标检测任务,聚焦深色皮肤相关皮肤病医学图像数据集,涵盖白糠疹、炎症后色素沉着过度、特发性黑色素沉着症、汇流和网状、白癜风等类别,共703个文件,压缩包约9.16MB。数据集包含234张jpg原图、2…

作者头像 李华
网站建设 2026/9/28 15:50:45

YOLOv8s通道剪枝实战:从稀疏化训练到边缘部署提速

做模型落地的人,十有八九都会碰到同一个坎:YOLOv8s在服务器上精度正正好,一搬到边缘盒子,帧率就拉胯,显存也紧张。这时候最常用的招数之一就是给模型做通道剪枝。这篇文章不空谈原理,直接给你一套我在实际项…

作者头像 李华
网站建设 2026/9/28 15:50:44

微信开源WeKnow-RAG:动态证据链知识库问答系统实战

最近刷 GitHub 的时候,微信开源的知识库项目在趋势榜上挂了好几天,评论区不少人在问“这玩意儿到底能不能用来搭自己的知识库”。我正好几个月前因为在公司里做内部文档问答系统,把 RAG 相关的主流方案几乎都摸了一遍,所以这个项目…

作者头像 李华