ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

银行客户认购预测实战:从特征工程到模型交付全流程解析

银行客户认购预测实战:从特征工程到模型交付全流程解析 简介这是一份面向计算机相关专业毕业设计及课程设计的机器学习项目资源完整实现了银行客户认购定期存款产品的预测流程。项目以银行营销数据集为基础涵盖数据探索可视化、特征工程、模型训练与结果提交等环节适合需要从零搭建分类模型的本科学生或练习者。资源共66个文件包含43张数据分布与正样本特征图、5个Python脚本、5个CSV数据文件、3个Jupyter Notebook以及训练好的pkl模型和JSON日志等zip压缩包仅9.39MB结构清晰便于按模块学习其中可视化图表覆盖年龄、婚姻、联系方式等关键维度方便理解数据分布。目前已有481人学习下载经过严格调试可直接运行。使用该资源可掌握预处理管道构建、多版本模型迭代与结果对比的完整思路模型文件与预测输出可直接用于毕设报告和答辩演示。1. 银行客户认购产品预测这个项目为什么值得用Python完整做一遍银行客户认购产品预测是机器学习在金融营销里最常被拿来做练手的场景之一。一个完整的 Python 实现通常包含客户档案、历史营销记录以及“是否购买某个金融产品”的标签训练一个分类模型最后把模型文件拿去生成可拨打的名单。只要把目标字段从定期存款换成理财、贷款或保险这套流程几乎可以复用。适合想从“调包”走向“完整项目”的数据分析师、金融从业者和课程设计的学生也适合第一次面对不平衡样本和模型交付的人。这类项目的门槛不在算法而在数据和评估口径。数据里往往会有大量“未认购”样本少数“认购”样本才是利润来源简单用准确率会得到虚假信心。下面会一起处理标签定义、特征工程、模型选型和模型文件管理并给出几段可以直接跑的Python代码。2. 先把银行客户数据变成模型能吃的样本标签、清洗与划分2.1 经典客户营销表里哪些字段是标签哪些藏着未来信息常见做法是用银行电话营销的客户数据来演示目标字段是客户是否认购定期存款。原始表一般长这样age、job、marital、education、default、balance、housing、loan这组字段描述客户基本财务状况contact、day、month、duration、campaign、pdays、previous、poutcome 描述这次电话营销怎么打、前几次打得怎么样最后一列 y 是标签yes 代表认购。字段名可能随业务改但逻辑一致。拿到数据第一件事是看标签分布。用 pandas 读入并统计import pandas as pd df pd.read_csv(bank.csv, sep;) print(df.shape) print(df[y].value_counts(normalizeTrue))逻辑说明sep; 是因为很多银行数据集用分号而不是逗号。value_counts(normalizeTrue)给出各类占比能立刻看清正样本是不是稀有类。我一般会在这一步顺便看df.info()确认有没有缺失值、类型有没有被读错。参数说明如果你的数据文件是逗号分隔把 sep 改成 , 就好。如果字段名带空格可以先看前两行再决定是否要skipinitialspaceTrue。这一步不做模型参数但决定后面的 Pipeline 能不能跑通。真正容易翻车的是字段的“时序身份”。duration 是这次通话的持续秒数它在通话结束前拿不到如果模型要预测的是“营销时谁会买”把 duration 丢进特征等于偷看答案。很多银行项目线上效果不如回测问题就出在这种字段。我会先建立一个建模时点假设只在客户成为本次营销名单时已知的信息才允许进特征因此 duration 不作为常规特征使用如果要保留必须在特征工程里定义成“事后已知”并明确告诉业务方。注意duration 这类“事后才知道”的字段和标签一样是未来信息。用它做回测能得到漂亮曲线上线后业务方拿不到预测就失真。2.2 用 pandas 清洗并用分层抽样划分代码与参数说明接下来把标签转成 0/1去掉无关主键然后划分训练集和测试集。代码骨架如下# 标签从 yes/no 转成 1/0 df[target] (df[y] yes).astype(int) df df.drop(columns[y]) # 建模特征与标签分离 X df.drop(columns[target]) y df[target] # 分层抽样保证训练集和测试集的正样本比例一致 from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) print(f训练集正样本占比: {y_train.mean():.3f}) print(f测试集正样本占比: {y_test.mean():.3f})逻辑说明stratifyy是必须写的参数。银行营销数据的认购比例往往只有 10% 到 15%如果不分层随机切分可能让测试集正样本只有几个评估曲线全是锯齿。random_state42固定随机种子便于复现不要每次跑得到不同结果再怀疑代码。参数说明test_size0.2 表示留 20% 做最终评估。对于四万行样本已经够用如果遇到百万级名单可以保持 0.2 但提示自己评估指标要按业务分层再看后面会提到。这里不急着把模型丢进来先把正样本占比记下来。比例低不是错误而是后面所有评估方式的前提准确率在这里是陷阱因为“全预测不认购”也有接近 90% 的准确率。真正要盯的是 AUC、召回率、精确率以及业务侧更关心的“一通电话的转化成本”。我在实际项目中会先把基线结果打出来用 DummyClassifier 判断特征到底有没有信息量from sklearn.dummy import DummyClassifier from sklearn.metrics import f1_score dummy DummyClassifier(strategymost_frequent) dummy.fit(X_train, y_train) y_pred dummy.predict(X_test) print(dummy f1:, f1_score(y_test, y_pred))逻辑说明strategymost_frequent 的 Dummy 会永远预测训练集中最多的类别也就是“不认购”。如果某个特征工程后的模型 F1 还不如它说明模型没有学到有意义的模式。因为无法输出概率我不对 Dummy 计算 AUC只用它作为最底线。之后上真实模型时再用 predict_proba。这一步还应该处理重复行和明显缺失。常见做法是你自己写一个清洗函数# 去重、检查空值、丢弃无业务意义的流水号 df df.drop_duplicates().reset_index(dropTrue) print(df.isnull().sum().sum()) df df.drop(columns[id], errorsignore)逻辑说明drop_duplicates()能清掉完全相同的营销记录避免训练集和测试集里同时出现同一条数据形成轻度数据泄露。isnull().sum().sum()只看全表缺失数不够细但足以决定下一步是否要接 SimpleImputer。参数说明errorsignore 让 drop 操作在列不存在时不报错适合写通用脚本。实际银行数据里很多“缺失”并不是空值而是用 -1、999、unknown 这种占位符比如下面的 pdays需要在特征工程里单独处理。3. 银行客户认购特征工程编码方式、业务衍生与管道固化3.1 类别特征别盲目 One-Hot职业、月份和教育水平怎么处理银行数据里类别字段不少job、marital、education、contact、month、poutcome。职业字段基数不小如果贪图简单全部 One-Hot特征会变得很宽对逻辑回归来说可以接受对随机森林和 LightGBM 来说太多稀疏列反而稀释特征重要性。通常我不直接对所有类别列 OneHot而是分组处理低基数且无顺序的字段如 contact用 OneHot高基数字段如 job可以用 OneHot 加高频类别合并或者改用目标编码有顺序语义的字段如 education可映射成学历等级month 是周期变量适合做 sin/cos 变换。from sklearn.preprocessing import OneHotEncoder, StandardScaler from sklearn.compose import ColumnTransformer num_cols [age, balance, loan_num, campaign, pdays_recent] cat_cols [job, marital, contact, poutcome] preprocessor ColumnTransformer([ (num, StandardScaler(), num_cols), (cat, OneHotEncoder(handle_unknownignore), cat_cols) ])逻辑说明ColumnTransformer 会分别对数值列和类别列做变换之后再拼接成模型输入。handle_unknownignore很关键它解决的是未来新数据里出现训练集没见过的类别时OneHot 不会报错而是全 0。银行名单会不断新增职业类型这个参数能避免上线时崩掉。参数说明StandardScaler 是逻辑回归的必需品对树模型不是必须但放进去不会坏事。如果你的特征里包含大量 0 值或者长尾分布可以先用FunctionTransformer(np.log1p)把正偏态数值特征压缩再进 StandardScaler这样对 balance、pdays 这类字段更稳。我在实际项目里习惯把loan_num这类衍生列先算好再交给管线。3.2 从历史营销次数和月份里构造业务特征pdays 的 999 陷阱银行营销数据里最容易误导模型的是 pdays。它表示这位客户上一次被营销后距离今天的天数如果没有被营销过数据里常常填 999。如果把 999 当普通数值交给模型模型会学到“pdays999 这个具体数”而不是“从未联系过”这个状态。常见做法是拆成两个特征是否从未联系过以及距离天数。import numpy as np # 先标记缺失和不适用 no_previous_contact (df[pdays] 999).astype(int) # 再把999换成-1让它和真实天数不在同一个量纲区间 pdays_recent df[pdays].replace(999, -1) df[no_previous_contact] no_previous_contact df[pdays_recent] pdays_recent逻辑说明拆成两个特征后逻辑回归可以学到“从未接触过会降低购买概率”同时 pdays_recent 对接触过的人继续保留时间间隔信息。如果不拆模型必须同时拟合“是否接触”和“接触多久”信息容易纠缠。参数说明把 999 替换成 -1 是一个务实做法。它不假装模型理解缺失值只是把不适用状态推到特征空间边缘。如果你用树模型直接保留 999 也能跑但解释性会变差如果后续要出评分卡尽量拆。还可以把 campaign 和 previous 合起来看campaign 是本次活动的第几次联系previous 是活动前联系过多少次。对同一个客户联系太多次通常是高拒绝率信号但单纯放进两个数字不够直观我会构造一个“联系密度”特征df[contact_pressure] df[campaign] / (df[previous] 1)逻辑说明除以历史次数加 1避免除零。联系密度高表示本次营销推力过大可能说明客户的兴趣在被消耗这个特征在业务上可比单独 campaign 更容易解释。遇到线上效果不好时这一列也可以直接用 SQL 统计回来方便运营复核。月份特征也值得处理。电话营销有明显的季节性但直接把 may 映射成 5 会丢失周期关系。简单做法是保留原始类别交给 OneHot更贴合业务的做法是做 sin/cos 周期编码month_map {jan:1, feb:2, mar:3, apr:4, may:5, jun:6, jul:7, aug:8, sep:9, oct:10, nov:11, dec:12} month_num df[month].map(month_map).astype(float) df[month_sin] np.sin(2 * np.pi * month_num / 12) df[month_cos] np.cos(2 * np.pi * month_num / 12)逻辑说明sin/cos 可以把 1 月和 12 月映射到相邻位置避免线性编码带来的“12月比1月大”的假象。这类特征在树模型里不够直观但对逻辑回归和评分卡场景很实用。3.3 用 sklearn Pipeline 把预处理、训练、预测串成一条线到这一步特征已经有了几十列。最怕的是训练时处理一版特征测试时再手工处理另一版数量对不上。我一般会用 Pipeline 固定全流程这样后续保存模型文件时预处理器和模型会一起被持久化。from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression num_cols [age, balance, campaign, pdays_recent, contact_pressure] cat_cols [job, marital, education, contact, poutcome] preprocessor ColumnTransformer([ (num, StandardScaler(), num_cols), (cat, OneHotEncoder(handle_unknownignore), cat_cols) ]) lr_pipeline Pipeline([ (prep, preprocessor), (clf, LogisticRegression(max_iter2000, class_weightbalanced)) ])逻辑说明Pipeline 除了简化代码还有一个关键好处是fit时只看到训练集transform测试集时不会把测试集统计量泄漏进模型。StandardScaler 的均值和标准差都来自训练集OneHot 的类别集合也来自训练集。这比手工在全体数据上做标准化更安全。参数说明class_weightbalanced 会自动按样本比例放大少数类惩罚逻辑回归用它比事后调阈值更省事。max_iter2000 是因为 OneHot 后特征维度变大默认 1000 偶尔不收敛。把prep和clf用双下划线分隔后面做网格搜索时可以直接写clf__C这样的参数路径。4. 候选模型怎么选、训练脚本怎么写、模型文件怎么存才不翻车4.1 逻辑回归、随机森林、LightGBM 在银行认购场景的定位差异银行客户认购预测很少有一上来就上深度模型的必要。以几万行结构化数据、几十个特征、以“可解释”和“可审计”为前提的场景候选模型通常在这三个之间选。逻辑回归的优点是系数直接对应业务含义能解释“年龄每增加一岁认购概率怎么变”。缺点是特征交互要靠手工做线性边界不一定够。随机森林优点是可以自动抓到非线性关系对异常值稳健缺点是特征重要性不稳定换一份数据排序会变上级问起来不好交代。LightGBM 在近期项目里往往 AUC 最高能处理高基数类别但需要认真调参过拟合后线上递减很快。我通常用一份简单的对比表决定方向模型适合情况交付时要多做的事LogisticRegression需要评分卡、业务方要求可解释标准化、分箱、系数产出RandomForest基线模型快速验证特征有效性检查随机种子、特征重要性LightGBM追求 AUC、特征多、样本足够早停、NumLeaves 约束、版本管理说明这不是说 LightGBM 一定更好。银行场景经常要回答“为什么给这个客户打电话”逻辑回归和评分卡更容易通过合规审查。如果时间只够跑一个模型我会先用逻辑回归做一个精简单线再快速看 LightGBM 有没有提升。4.2 交叉验证下的训练骨架网格搜索与参数怎么给有了 Pipeline训练脚本可以非常规整。以下是一个带网格搜索和 LightGBM 的骨架from sklearn.model_selection import StratifiedKFold, GridSearchCV from lightgbm import LGBMClassifier lgbm_pipeline Pipeline([ (prep, preprocessor), (clf, LGBMClassifier( n_estimators300, learning_rate0.05, class_weightbalanced, random_state42, verbosity-1 )) ]) param_grid { clf__num_leaves: [15, 31, 63], clf__min_child_samples: [20, 50, 100], clf__max_depth: [-1, 8, 12] } cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) grid GridSearchCV( lgbm_pipeline, param_grid, cvcv, scoringroc_auc, n_jobs-1, verbose1 ) grid.fit(X_train, y_train) print(grid.best_params_)逻辑说明StratifiedKFold 和前面 train_test_split 的理由一样都是为了在每折里保持正样本比例。scoringroc_auc 是银行认购这类不平衡分类的首选评估它只关心排序质量不受 0.5 阈值影响。参数说明num_leaves 是 LightGBM 最需要控的过拟合旋钮15 到 63 已经覆盖常见范围。min_child_samples 控制在 20 到 100防止叶子节点只用几十个样本。max_depth-1 表示不限制但配合 num_leaves 不会太深。learning_rate0.05 配 n_estimators300 是一个保守起步如果你用默认 0.1建议把 n_estimators 降低否则训练时间变长而且容易过拟合。网格搜索跑完后我会看一眼测试集 AUCfrom sklearn.metrics import roc_auc_score y_test_proba grid.predict_proba(X_test)[:, 1] print(test auc:, roc_auc_score(y_test, y_test_proba))逻辑说明predict_proba返回的是两个类别概率取第二列是“认购”概率。评估只用测试集不要用测试集参与调参。如果训练 AUC 很高而测试 AUC 明显下滑优先回到 num_leaves 和 min_child_samples 去约束模型复杂度。4.3 把模型文件、预处理器和特征列表一起保存标题里既然有“模型文件”这一节就特别值得说清楚。很多人只保存一个模型对象结果新环境加载后报特征数量不一致或者找不到类别编码中的某一列。正确做法是把整个 Pipeline 保存成一个 joblib 文件特征逻辑跟着走。import joblib import os os.makedirs(model, exist_okTrue) joblib.dump(grid.best_estimator_, model/bank_pipeline.joblib)逻辑说明dump 的是整个 Pipeline 而不是里面某个分类器。这样加载后直接predict不需要手工再拼一遍特征。模型文件大小通常几十到几百 MB如果你发现文件大得离谱检查是不是把训练 DataFrame 也放进了对象里。loaded_pipeline joblib.load(model/bank_pipeline.joblib) y_scores loaded_pipeline.predict_proba(new_data)[:, 1]参数说明joblib 比 pickle 更适合存 numpy 对象而且它按数组分块压缩。要保证能 load需要在同一套 Python 版本、scikit-learn 版本和 LightGBM 版本环境里。这个坑在银行客户预测项目里非常常见下一章专门说。5. 银行客户认购预测避坑从数据泄露到阈值幻觉的5个真实踩坑记录5.1 AUC 高但线上转化没涨未来字段泄露与业务指标错配现象训练测试 AUC 都是 0.88模型在回测里表现亮眼但业务人员按预测名单打电话转化率并没有提升。原因最常见的是把 duration 这种通话结束才有的字段放进了特征。回测时这些字段存在但营销前根本拿不到等于模型偷看了答案。另一个常见原因是用准确率做评估AUC 高不代表你去掉的低分客户真的是低分。解决回到建模时点把事后才知道的字段从特征里删除或者明确标注为“通话后模型”。如果业务要事前名单只能使用年龄、资产、历史接触次数这类历史数据。把 AUC 和“名单 Top10% 的转化率”一起看比单看 AUC 更贴近银行营销目标。5.2 预测结果全是“不认购”类别不平衡与固定阈值现象模型训练正常预测时发现概率超过 0.5 的样本几乎没有业务拿到名单只有几十个人根本没法外呼。原因正样本占比只有 10% 左右模型学习到的先验概率很低默认阈值 0.5 对这类场景过高。解决调阈值而不是改模型。对测试集算 precision_recall_curve选择 F1 最高点或者业务成本最低点作为 cutoff。示例from sklearn.metrics import precision_recall_curve precision, recall, thresholds precision_recall_curve( y_test, y_test_proba ) f1_scores 2 * precision * recall / (precision recall 1e-9) best_idx f1_scores.argmax() best_threshold thresholds[best_idx] print(best threshold:, best_threshold)逻辑说明precision_recall_curve返回随阈值变化的精确率和召回率F1 最高点就是平衡两者的经验选择。实际业务里如果外呼成本低而认购利润高可以把阈值往下压保证召回多数潜在客户如果外呼容易引起投诉就把阈值上调。参数说明1e-9只是防止除零。拿到 best_threshold 后还要在业务样本上验证 Top 比例比如 5% 或 10% 名单覆盖了多少正样本不要只看 F1。y_test_proba必须来自测试集或线上验证集的模型输出不能用训练集概率否则阈值会被高估。5.3 测试集特征数量报错手工特征工程没有纳入管道现象训练时模型好好能跑测试或新数据预测时报错提示期待 X 个特征实际输入 Y 个特征。原因特征工程全部手工写训练集多算了一列测试时漏算或顺序不一致或者 OneHot 编码时训练集和测试集类别不同新类别变成新列。解决把预处理集中到 ColumnTransformer新增特征也写成一个FunctionTransformer或普通函数放进 Pipeline。这样所有数据处理逻辑随模型文件一起保存不会再出现“测试集少一列”的问题。调试时可以用loaded_pipeline[:-1].transform(new_data)检查转换后的特征形状。5.4 新机器加载模型文件异常环境版本不一致现象模型文件拷到另一台机器joblib.load 后 predict 报错说找不到 LightGBM 的某个类或者 numpy 报错。原因joblib 和 pickle 会把类名和模块路径记进文件LightGBM、scikit-learn、numpy 版本不一致序列化对象恢复不出来。解决先记版本再交付。在训练机执行pip freeze requirements.txt然后新环境用同版安装。如果做不到完全同版本至少保证 lightgbm 和 scikit-learn 大版本一致。我在项目里的习惯是训练完模型文件后紧接着把 requirements.txt 也放进交付目录并在记录里写明 Python 版本。血泪经验是很多“模型文件加载失败”不是文件坏了是环境变了。5.5 目标编码做过全局均值看起来正常线上却失效现象用目标编码比如把 job 映射成“该职业历史认购率”后AUC 明显提升上线后预测分布整体偏移评分失真。原因在整个数据集上 compute 均值再去编码编码值里混入了当前样本标签信息切分训练测试后测试集的目标均值被偷看。解决目标编码只能在训练集上 fit再 transform 验证集和测试集最好把它放进 KFold 内部每个 fold 独立编码。如果不想自己写可以用sklearn.preprocessing.TargetEncoder或 category_encoders 里的 TargetEncoder但一定要确保它被包括在 Pipeline 中而不是先编码再划分。判断标准很简单改一行标签编码值就变说明它已经过拟合到标签上了。6. 从预测概率到银行可用的认购名单阈值、解释与模型文件的最后一公里6.1 用业务成本曲线选阈值而不是默认 0.5最后一公里是把概率转成外呼名单。我一般会做一张成本和收益的小表而不是直接取 0.5指标计算方式外呼成本每通电话人力成本单客户利润客户认购产品的期望利润名单规模超过阈值的客户数把y_test_proba按阈值切分计算不同阈值下的总利润选利润最大的阈值。这比 F1 更贴合银行场景因为它把“漏掉客户”和“打错电话”的代价直接量化了。6.2 交付目录和验证检查单交付一个银行客户认购预测模型时应该有模型文件、代码、环境依赖和说明一份目录。验证时我会按顺序检查新数据里的类别是否会报错、pdays 是否还有 999 被当数值、模型文件加载后预测是否稳定、Top 名单的认购率是否显著高于整体。用加载后的 Pipeline 跑一遍完整 SQL 拉出的最新客户表再和业务方认定的 top 特征做对照。6.3 下一步从“预测谁买”扩展到“买后价值”这个方向真正值钱的地方不在多提升一点 AUC而在于把模型结果接到业务动作上。后续可以加一个成本敏感决策高概率客户直接外呼中概率客户走短信或 App 推送低概率客户进入沉默观察。也可以从二分类升级成回归预测客户购买后的资产贡献让名单排序更贴近利润。我现在接手类似项目时会先定阈值和收益口径再开始建模而不是先把模型调到最好看。希望这些经验能帮你把银行客户认购产品预测做成一个能交付、能解释、能持续迭代的小工程。本文还有配套的精品资源点击获取
返回列表