银行客户认购产品预测:从特征工程到模型部署的避坑指南

发布时间:2026/10/18 12:12:10
银行客户认购产品预测:从特征工程到模型部署的避坑指南
简介这份资源面向计算机相关专业正在做毕业设计的学生以及需要机器学习项目实战练习的学习者也可用于课程设计或期末大作业。项目以银行客户认购产品预测为主题完整覆盖从数据探索、特征工程到模型训练与结果提交的全流程适合具备一定Python基础、希望积累真实业务建模经验的中高级学习者。压缩包共66个文件约9.39MB包含5个py脚本、3个ipynb笔记本、5个csv数据集、2个pkl模型文件以及43张png可视化图表和4个md说明文档另附json、xlsx等配置与字段说明文件。项目源码、数据集与训练好的模型一应俱全可直接运行并作为毕设使用。目前已有481人学习下载。读者可借助可视化图表理解各特征分布与正样本规律通过notebook逐步复现建模思路并参考预处理管道与最优模型文件快速完成预测与结果提交省去从零搭建的时间成本。1. 银行客户认购产品预测为什么“准确率 95%”的模型一上线就翻车很多做 Python 机器学习的朋友第一次拿到银行客户认购产品预测这类数据集时都会经历同一个心理落差训练集上准确率刷到 95%AUC 逼近 0.95感觉自己已经摸到了天花板结果把模型交给业务方对方跑了一批真实名单转化率还不如随机抽。问题不在模型在于这个任务从根上就不是一个“追求准确率”的问题。银行客户认购产品预测本质是一个典型的二分类营销响应模型输入是客户画像、历史接触记录、宏观经济指标等特征输出是“这个客户会不会认购定期存款/理财/保险产品”的概率。它的业务价值在于把有限的电话外呼、客户经理跟进资源优先砸给最可能成交的那批人。所以真正要看的指标是提升度Lift、召回率、以及 Top 10% 名单里的命中率而不是整体准确率。这个项目适合三类人想完整走一遍“数据清洗→特征工程→建模→评估→保存模型”流程的 Python 学习者需要给营销部门交付一个可解释评分卡的数据分析师以及想搞清楚“为什么我的模型指标好看但没用”的算法工程师。下面我按自己实际做这类项目的顺序把源码、数据集、模型文件这条链路拆开讲清楚。2. 数据集拆解与特征工程先搞清楚哪些列是“事后信息”拿到一份银行客户认购数据集第一件事不是急着pd.read_csv然后train_test_split而是逐列判断这一列在预测时刻到底拿不拿得到。这是营销响应模型最容易翻车的地方也是很多人模型指标虚高的根源。2.1 先分清“预测时可得”和“事后才知道”的列典型的银行客户认购数据集一般包含这几类字段客户基本信息年龄、职业、婚姻状况、教育程度、账户信息是否有房贷、是否有个人贷款、违约记录、本次营销接触信息接触方式、接触月份、接触时长、接触次数、距上次接触天数、上次接触结果、以及宏观经济指标就业率、消费者价格指数等。最后是标签列表示本次营销是否认购成功。这里有个血泪经验接触时长这一列在训练集里和标签的相关性极高因为聊得久往往意味着客户有兴趣。但如果你要预测的是“下一批还没打电话的客户”这个字段在预测时刻根本不存在——你还没打呢哪来的时长。所以做真实可用的模型时这一列必须删掉或者单独做一版“已接触客户二次筛选”的模型。很多人不删模型 AUC 能到 0.93一上线就废这就是典型的“事后信息泄漏”。import pandas as pd import numpy as np # 读取原始数据分隔符按实际数据集调整常见是分号 df pd.read_csv(bank_marketing.csv, sep;) # 先看标签分布判断是否类别不平衡 print(df[y].value_counts(normalizeTrue)) # 明确要删除的“事后信息”列接触时长在预测未接触客户时不可得 leak_cols [duration] df df.drop(columnsleak_cols) # 把标签转成 0/1 df[y] df[y].map({no: 0, yes: 1}) # 检查缺失值这个数据集通常比较干净但宏观指标列可能有缺失 print(df.isnull().sum().sort_values(ascendingFalse).head(10))这段代码做了三件事看标签比例、删泄漏列、转标签。参数上sep;是因为这类数据集常见分号分隔读之前先用文本编辑器看一眼第一行normalizeTrue让你直接看到正样本占比通常只有 10% 上下这决定了后面不能用准确率当主指标。删duration这一步我一般会在代码里写死并加注释防止后面有人手贱加回来。2.2 类别特征编码独热、序数还是目标编码这个数据集里大量是类别特征比如职业、婚姻、教育、接触月份、上次接触结果。处理方式直接决定模型上限。我的选择逻辑是无序且类别少的用独热编码有天然顺序的教育程度、月份用序数编码类别多且高频长尾的职业可以试目标编码但必须做交叉验证内的编码否则一样泄漏。from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline # 无序类别独热编码 nominal_cols [job, marital, contact, poutcome, loan, housing, default] # 有序类别教育程度有明确高低月份有循环性但先按序数处理 ordinal_cols [education, month] education_order [unknown, primary, secondary, tertiary] month_order [jan,feb,mar,apr,may,jun, jul,aug,sep,oct,nov,dec] preprocessor ColumnTransformer( transformers[ (nom, OneHotEncoder(handle_unknownignore), nominal_cols), (ord, OrdinalEncoder(categories[education_order, month_order]), ordinal_cols), ], remainderpassthrough # 数值列直接保留 )handle_unknownignore是关键参数保证线上出现训练集没见过的类别时不报错直接编码成全零。OrdinalEncoder的categories必须手动指定顺序不指定的话它按字母序排教育程度会变成 primary secondary tertiary 这种碰巧对的但月份会乱。remainderpassthrough把年龄、次数、宏观指标这些数值列原样保留省得一个个列出来。这套预处理我一般封进Pipeline保证训练和推理用同一套逻辑避免线上线下不一致这个经典坑。2.3 数值特征分箱与不平衡处理数值列里年龄、接触次数、距上次接触天数这些分布往往长尾严重。树模型对单调变换不敏感但对极端值还是敏感我一般会做分位数分箱或者直接截断。更重要的是类别不平衡正样本只有 10% 左右直接训练会让模型偏向预测负类。from sklearn.preprocessing import KBinsDiscretizer # 对长尾数值列做分位数分箱n_bins 按业务可解释性定一般 5 到 10 binner KBinsDiscretizer( n_bins8, encodeordinal, strategyquantile ) # 不平衡处理优先用 class_weight不轻易上 SMOTE # 因为 SMOTE 在类别特征独热后生成的样本可能不合法不平衡处理我的习惯是先试class_weightbalanced它不改变数据分布只是调损失权重稳定且可解释。SMOTE 这类过采样在纯数值场景好用但独热编码后的稀疏特征上插值容易造出“婚姻状况 0.3 已婚 0.7 单身”这种没意义的样本反而掉点。如果非要用就在 Pipeline 里做且只在训练折上做绝不能在全量数据上做。3. 模型训练与评估为什么我最后选了逻辑回归而不是 XGBoost这一章讲建模和评估。很多人一上来就 XGBoost、LightGBM 调参指标确实好看但交付给业务方时解释不了“为什么这个客户被排前面”。银行营销场景对可解释性要求高所以我的默认起点是逻辑回归评分卡再用树模型做对照。3.1 逻辑回归基线可解释、可交付、可复现逻辑回归在这个任务上的优势不是精度而是每个特征的系数可以直接翻译成“这个因素让认购概率上升还是下降”。业务方看得懂风控也认。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler # 完整流程预处理 标准化 逻辑回归 clf Pipeline(steps[ (prep, preprocessor), (scaler, StandardScaler(with_meanFalse)), # 稀疏矩阵不能去中心化 (model, LogisticRegression( C1.0, # 正则强度越小正则越强 class_weightbalanced, # 处理不平衡 max_iter1000, # 防止不收敛 solverliblinear # 小数据集上用 liblinear 稳 )) ]) # 分层交叉验证保证每折正样本比例一致 cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(clf, X, y, cvcv, scoringroc_auc) print(AUC:, scores.mean(), scores.std())参数说明C1.0是默认正则强度实际调参时我会在 0.01 到 10 之间网格搜class_weightbalanced自动按类别频率反比加权solverliblinear在几万行以内的小数据集上比lbfgs更稳且支持 L1/L2max_iter1000是防止标准化后仍不收敛的后悔药。StratifiedKFold必须用分层的否则某折可能正样本极少AUC 波动巨大。with_meanFalse是因为独热后是稀疏矩阵去中心化会把它变稠密内存爆炸。3.2 树模型对照LightGBM 调参的三个关键旋钮逻辑回归跑通后我会用 LightGBM 做一版对照看非线性特征交互能不能带来提升。如果能再考虑用 SHAP 做解释后上线如果不能就老老实实用逻辑回归。import lightgbm as lgb from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) # 注意LightGBM 需要先做预处理这里用预处理后的稠密矩阵 X_train_enc preprocessor.fit_transform(X_train) X_test_enc preprocessor.transform(X_test) model lgb.LGBMClassifier( n_estimators500, # 树的数量配合早停用 learning_rate0.05, # 学习率小一点更稳 num_leaves31, # 叶子数控制复杂度别超过 2^depth min_child_samples20, # 叶子最小样本防过拟合 subsample0.8, # 行采样 colsample_bytree0.8, # 列采样 class_weightbalanced, random_state42 ) model.fit( X_train_enc, y_train, eval_set[(X_test_enc, y_test)], eval_metricauc, callbacks[lgb.early_stopping(50), lgb.log_evaluation(100)] )三个关键旋钮num_leaves是最重要的复杂度控制31 是安全起点调大容易过拟合min_child_samples在正样本少的时候要适当调大20 到 50 之间试learning_rate配n_estimators和早停0.05 配 500 棵通常够。early_stopping(50)表示验证集 AUC 50 轮不提升就停这是防止白跑和过拟合的后悔药。注意 LightGBM 对独热后的稀疏矩阵支持一般如果特征维度很高建议改用类别特征原生支持把类别列直接标成category类型效果往往更好。3.3 评估指标别再看准确率看 Lift 和 Top-K 命中率模型训完评估才是重头戏。业务方关心的是“给我 1000 个名单能命中多少”。所以我会算 Lift 曲线和 Top-K 命中率。from sklearn.metrics import roc_auc_score, precision_recall_curve import numpy as np # 预测概率 y_prob model.predict_proba(X_test_enc)[:, 1] # AUC整体排序能力 print(AUC:, roc_auc_score(y_test, y_prob)) # Top-K 命中率按概率降序取前 K 个看正样本占比 def top_k_hit(y_true, y_prob, k_ratio0.1): k int(len(y_true) * k_ratio) idx np.argsort(y_prob)[::-1][:k] return y_true.iloc[idx].mean() base_rate y_test.mean() for ratio in [0.05, 0.1, 0.2, 0.3]: hit top_k_hit(y_test, y_prob, ratio) print(fTop {ratio:.0%} 命中率: {hit:.3f}, Lift: {hit/base_rate:.2f})top_k_hit的逻辑是按预测概率从高到低排序取前 K 个算这批人里真实认购的比例。Lift是这个比例除以整体正样本率。比如整体认购率 10%Top 10% 名单命中率 30%Lift 就是 3意思是“比随机抽好 3 倍”。这个数字业务方一听就懂比 AUC 0.9 有说服力得多。我一般要求 Top 20% 的 Lift 至少到 2.5 以上才算这个模型有实际投放价值。4. 模型保存与推理服务pickle、joblib 还是 PMML模型训好只是半成品能稳定加载、对新数据打分才算交付。这一章讲保存格式选择、推理封装和版本管理。4.1 保存格式对比别用 pickle 存跨版本模型格式适用场景优点坑pickle同环境临时用简单跨 Python/sklearn 版本易失败joblibsklearn 模型首选对 numpy 数组高效仍有版本依赖ONNX跨框架部署通用、快转换可能丢预处理逻辑PMML银行风控传统可读、可审计工具链老旧我的习惯训练阶段用 joblib 存整个 Pipeline含预处理保证推理时逻辑一致如果要交给 Java 团队部署再转 PMML 或 ONNX。绝对不要只存模型不存预处理器否则线上打分时编码方式对不上结果全错。import joblib # 保存整个 Pipeline包含预处理和模型 joblib.dump(clf, bank_subscribe_pipeline_v1.joblib) # 加载并推理 loaded joblib.load(bank_subscribe_pipeline_v1.joblib) new_prob loaded.predict_proba(X_new)[:, 1]joblib.dump存的是整个对象图包括ColumnTransformer里的编码器状态。加载后直接predict_proba预处理自动走一遍不会出现“训练时独热了、推理时忘了”的低级错误。文件名带版本号v1配合一个简单的model_registry.csv记录训练日期、AUC、特征列表方便回滚。4.2 推理封装批量打分脚本与阈值选择线上推理一般两种批量打分每天跑一批名单和实时打分客户经理查询时实时返回。批量场景写个脚本就够。def score_batch(input_csv, output_csv, model_path, threshold0.5): 批量打分读入客户特征输出认购概率和是否推荐 model joblib.load(model_path) df pd.read_csv(input_csv, sep;) prob model.predict_proba(df)[:, 1] df[subscribe_prob] prob df[recommend] (prob threshold).astype(int) # 按概率降序方便业务方从高到低跟进 df.sort_values(subscribe_prob, ascendingFalse).to_csv(output_csv, indexFalse) return df score_batch(new_customers.csv, scored_output.csv, bank_subscribe_pipeline_v1.joblib)阈值threshold不是拍脑袋定的要结合业务成本打一个电话有成本漏掉一个意向客户也有成本。我一般会画一条阈值-利润曲线选利润最大点。默认 0.5 只是占位实际项目里往往调到 0.2 到 0.3因为正样本少宁可多打几个也别漏。输出按概率降序业务方从前往后跟进资源利用率最高。5. 避坑与排查这类项目最常见的五个翻车点这一章是我踩过的坑合集每条按“现象→原因→解决”写照着排查能省不少时间。现象一交叉验证 AUC 0.95测试集只有 0.7。原因特征泄漏最常见的是把duration或标签衍生列带进去了或者做了全量数据的标准化/编码。解决所有预处理必须在Pipeline里且只在训练折上fit逐列检查有没有“预测时刻拿不到”的字段。现象二模型预测概率全是 0 或全是 1。原因逻辑回归没收敛或者标准化时对稀疏矩阵做了去中心化导致数值异常。解决max_iter调大solver换liblinearStandardScaler设with_meanFalse检查特征里有没有极端离群值。现象三线上打分报“特征数量不匹配”。原因训练时独热编码产生了 N 列线上新数据某个类别没出现编码后列数少了。解决OneHotEncoder(handle_unknownignore)并且保存整个 Pipeline 而不是单独模型上线前用一批真实数据做回归测试。现象四Top 10% 名单命中率还不如随机。原因评估时用了未来信息或者标签定义和业务口径不一致比如把“咨询过”当成“认购”。解决和业务方对齐标签定义重新确认预测时刻用时间切分而不是随机切分验证模拟真实投放。现象五模型文件换台机器加载失败。原因pickle/joblib 对 Python 和库版本敏感。解决固定环境版本用requirements.txt锁死跨语言部署转 ONNX 或 PMML加载失败时先看报错里的版本号差异。6. 进阶技巧用 SHAP 把黑箱模型讲成业务语言模型能跑、指标好看只是及格线。真正让这个项目在团队里站住脚的是你能对着一个具体客户说清楚“为什么他排在前 100”。逻辑回归靠系数树模型就得靠 SHAP。这一章讲怎么把 SHAP 值翻译成业务方能用的名单解释。import shap # 用训练好的 LightGBM 模型对测试集做解释 explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_test_enc) # 单个客户的解释列出贡献最大的几个特征 def explain_one(idx, top_n5): sv shap_values[1][idx] # 正类认购的 SHAP 值 feature_names preprocessor.get_feature_names_out() pairs sorted(zip(feature_names, sv), keylambda x: abs(x[1]), reverseTrue) return pairs[:top_n] print(explain_one(0))TreeExplainer对 LightGBM 是精确解速度快。shap_values[1]取正类因为二分类会返回两个数组。get_feature_names_out()拿到独热后的特征名比如nom__job_management。输出的是“这个特征把预测概率推高/拉低了多少”。业务方看到“上次接触成功 0.15接触次数 3 次 0.08”就明白该优先跟谁。我一般会做两件事一是对 Top 100 名单逐个生成解释附在名单后面给客户经理二是做全局 SHAP 摘要图看哪些特征整体最重要用来指导下一轮特征采集。有个细节SHAP 值是在编码后的空间算的独热特征要合并回原始类别再展示否则业务方看到job_management和job_technician分开列会懵。最后说个我自己的习惯每次交付模型我都会附一份“模型卡片”写清楚训练数据时间范围、标签定义、Top 20% Lift、已知局限比如对某类职业预测偏差大。这份卡片比模型本身更能建立信任也是下次迭代的基线。做银行客户认购预测这类项目技术只是入场券把模型讲清楚、用起来、持续监控才是真正值钱的部分。希望帮到你。本文还有配套的精品资源点击获取