ARTICLE DETAIL

资讯详情

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

银行客户认购预测:Python机器学习二分类全流程源码解析

银行客户认购预测:Python机器学习二分类全流程源码解析 简介面向计算机专业毕业设计及机器学习实战学习者这是一套银行客户认购产品预测完整项目。项目围绕银行电话营销场景基于客户基本属性、历史交互与经济指标构建分类模型覆盖数据探索、特征工程、模型训练与评估全流程并包含字段说明与多轮实验迭代记录。压缩包共65个文件以脚本、交互式实验笔记、数据集、模型文件及数十张特征分布图为主体积约9.58MB目录按实验笔记、原始数据、图表、模型、结果等模块划分便于对照学习。资源包含可直接运行的完整源码、预处理流水线、最优模型及预测结果文件还提供多版本实验笔记、可视化分析图和字段说明表适合作为毕业设计或课程作业的参考蓝本。项目经导师指导并认可通过代码经过严格调试可帮助读者快速复现预测流程并理解银行营销场景下的建模思路。已有572人学习下载。1. 银行客户认购产品预测一份能跑通全流程的Python机器学习毕设源码银行客户认购产品预测本质是银行电话营销里的一个经典二分类问题用客户画像、历史联系记录和宏观经济指标预测客户是否会认购定期存款产品。这份Python机器学习毕业设计源码把机器学习应用流程完整落了一遍——从数据集EDA、特征工程、模型训练调参到生成submission.csv每一步都有可运行的脚本和分布图支撑。它适合正在找可复现毕设项目的计算机专业学生也适合课程设计、期末大作业或者想拿真实表格数据练手机器学习全流程的从业者。需要先说清楚的是这份数据自带类别不平衡和明显的时序属性直接跑一个模型看准确率很容易翻车后面几章会把这几个坑逐个拆开。2. 先读透数据再谈模型字段语义、正样本差异与不平衡问题2.1 数据文件与字段语义train.csv、test.csv和submission.csv怎么配合dataset目录下有三份核心数据文件train.csv是完整训练集test.csv用于后续预测submission.csv是提交结果参考模板。字段说明.png和字段说明.xlsx对每个特征都做了语义解释这是写毕业设计文档时最省时间的一份材料。整体看下来特征可以分成三类。特征类别特征名业务含义客户画像age、job、marital、education、default、housing、loan年龄、职业、婚姻、教育水平以及是否有信用卡违约、房贷、个人贷款营销行为contact、month、day_of_week、duration、campaign、pdays、previous、poutcome联系方式、上次联系月份与星期几、通话时长、活动期间联系次数、距上次联系间隔天数、之前营销活动结果宏观指标emp_var_rate、cons_price_idx、cons_conf_idx、euribor3m、nr_employed就业变动率、消费者价格指数、消费者信心指数、银行同业拆借3个月利率、雇员人数这三类特征的业务逻辑很清晰客户本身有没有资金实力画像、这次营销触达得怎么样行为、大环境好不好宏观。实际建模时宏观指标经常被忽略但它们与时间强相关是后期验证集分数崩掉的隐患第5章会专门讲。2.2 figures目录的EDA图整体分布与正样本分布要对比着看figures目录里有一大批分布图文件名带了编号这个习惯很实用。001开头的是全量样本的整体分布002开头的大多是连续特征分布003开头的是正样本分布也就是认购了定期存款的那部分客户在各维度上的结构。对比方法很简单拿003_职业正样本分布和001_职业分布放在一起看。如果在正样本里某类职业的占比明显高于全量占比说明这个职业群体更倾向认购同样的逻辑可以套到婚姻、教育水平、联系方式上。003_婚姻正样本分布、003_教育水平正样本分布这类图很多银行营销报告里的结论都能直接从里面读出来。联系时长、联系次数、间隔天数这三个行为特征正样本与全量的分布差异通常最明显是后续特征工程的重点关注对象。读图时要注意一个细节所有分布图都是单个特征的边际分布只能给方向不能给强度。真正要量化特征重要性得等模型训练完看feature_importance。另一个细节是003系列正样本图的基数通常比001系列小个别类别可能只有几十个样本看占比容易被小样本波动带偏所以每张图旁边最好标注正样本总量。2.3 目标分布与类别不平衡为什么默认不能只看准确率001_目标分布.png就是标签列y的分布。参考同类型银行电话营销公开数据集的常见情况认购率通常在4%到11%之间这是一个明显的不平衡分类问题。如果图省事模型全部预测“不认购”准确率也能到90%以上。所以这类项目在评估上默认不能只用accuracy至少要看AUC和F1-score如果业务更关心“把真正会买的人捞出来”还要重点盯正样本的recall。预处理和训练阶段有两个地方为不平衡服务一个是切分数据时用StratifiedKFold保证每一折正负样本比例一致另一个是训练时设置scale_pos_weight或class_weightautomatic_learning.py里已经体现了一部分第4章会展开。验证方式同样要考虑。银行场景有时间先后宏观指标是时间序列客户联系记录也有先后。如果直接把train.csv随机切分很容易把“后来”的样本混进训练集和验证集得到的分数偏乐观。我一般在交叉验证之外再补一个按时间顺序切分的验证专门观察模型的泛化能力尤其是宏观指标变化明显的场景。3. 特征工程与预处理category_bin.py到preprocessing_pipeline.pkl的落地链路3.1 category_bin.py稀有类别合并与编码做了什么处理类别特征的第一步是看基数。job、marital、education这些字段里有些类别出现次数很少比如冷门职业或“unknown”。如果不处理直接做标签编码树模型会在这些类别上长出很长的分支而实际上这些分支只是在拟合几十个样本的噪声。常见做法是先统计每个类别的频数把频数低于某个阈值的类别合并成“other”再做编码。category_bin.py的核心逻辑大概是这样import pandas as pd from sklearn.preprocessing import LabelEncoder import joblib df pd.read_csv(dataset/train.csv) cat_cols [job, marital, education, default, housing, loan, contact, month, day_of_week, poutcome] encoders {} for col in cat_cols: vc df[col].value_counts() rare vc[vc 50].index # 出现次数小于50的类别合并 df[col] df[col].replace(rare, other) le LabelEncoder() df[col _enc] le.fit_transform(df[col].astype(str)) encoders[col] le # 编码器要存下来预测时复用 joblib.dump(encoders, models/label_encoders.pkl)这段代码先对每个类别特征做频数统计把频数低于50的类别替换成other再用LabelEncoder把字符串转成整数编号。每个特征训练出来的编码器单独保存预测test.csv时要用同一个映射不能让新数据重新fit。阈值50不是固定值取决于训练样本量。样本量在几万级别时我一般卡在30到100之间数据量更大可以适当提高。另外LabelEncoder会给类别强加大小关系对树模型来说问题不大但对线性模型不友好所以更稳妥的做法是用OneHotEncoder或者直接交给后面讲的ColumnTransformer统一处理。这份脚本可以当简化版本理解复现时建议按后续Pipeline方案来。3.2 tree_transform_bin.py用浅层决策树给连续特征找分箱阈值连续特征方面年龄、通话时长、联系次数这几列原始值进模型也能用但直接让树模型自己切分切出来的点往往没有业务含义。所以有些方案会先对连续特征分箱。tree_transform_bin.py的思路是用一棵浅层决策树去拟合目标变量拿树的分裂阈值当分箱边界这个做法在表格类竞赛里很常见。import numpy as np from sklearn.tree import DecisionTreeRegressor def tree_bin(x, y, max_depth3, min_samples_leaf0.05): model DecisionTreeRegressor( max_depthmax_depth, min_samples_leafmin_samples_leaf, random_state42 ) model.fit(x.values.reshape(-1, 1), y.values) # 内部节点的分裂阈值就是分箱边界 thresholds model.tree_.threshold feature model.tree_.feature bins sorted(set(thresholds[feature 0].round(4))) return bins bins tree_bin(df[age], df[y_binary]) print(bins)这里用一棵很浅的决策树以单个特征为输入去拟合标签树在递归分裂时选出的阈值就是分箱边界。比如age列可能得到[28, 34, 47, 59]这样的阈值把年龄切成5个区间每个区间对应不同的认购概率水平。max_depth控制在3左右太深会分得过细导致过拟合min_samples_leaf设为0.05表示每个叶子至少包含5%的样本防止小样本区间。这里的y_binary是把标签转成0/1后的列。分箱阈值一旦确定必须序列化保存预测集要按同一组边界切分如果重新在test.csv上算边界结果就完全对不上了。tree_transform_bin.py的方法适合作为特征工程补充不建议作为唯一特征来源原始连续列最好保留一起进模型。另外决策树对连续值里的离群点敏感分箱前先看一眼分布图把极端值截断会更稳。3.3 preprocessing_pipeline.pkl把预处理固化成一条流水线预处理部分最值得复用的是preprocessing_pipeline.pkl。它把数值列填充、标准化、类别列编码整合成一个sklearn Pipeline。训练时fit一次预测新数据时直接transform所有统计量都固化在对象里。import joblib import pandas as pd from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.impute import SimpleImputer num_cols [age, duration, campaign, pdays, previous, emp_var_rate, cons_price_idx, cons_conf_idx, euribor3m, nr_employed] cat_cols [job, marital, education, default, housing, loan, contact, month, day_of_week, poutcome] num_pipe Pipeline([ (imputer, SimpleImputer(strategymedian)), (scaler, StandardScaler()) ]) cat_pipe Pipeline([ (imputer, SimpleImputer(strategymost_frequent)), (onehot, OneHotEncoder(handle_unknownignore)) ]) prep ColumnTransformer([ (num, num_pipe, num_cols), (cat, cat_pipe, cat_cols) ]) df pd.read_csv(dataset/train.csv) y df[y].map({no: 0, yes: 1}) prep.fit(df, y) joblib.dump(prep, models/preprocessing_pipeline.pkl)ColumnTransformer把特征矩阵分成数值和类别两条通道。数值通道先做中位数填充再标准化类别通道先做众数填充再做独热编码。prep在训练集上fit之后所有统计量都固化在对象里后面加载pkl直接transform就行。SimpleImputer的strategy参数数值列用median对离群点更稳健类别列用most_frequentOneHotEncoder的handle_unknownignore非常关键预测数据里如果出现训练集没见过的新类别不会报错而是输出全零向量这一点在真实数据集上踩过坑的人应该懂。管道里尽量不要放依赖外部参数的步骤。比如tree_transform_bin算出来的分箱阈值如果想放进Pipeline得用FunctionTransformer并提前把阈值作为外部参数传进去否则无法正确序列化。preprocessing_pipeline.pkl里没有包含树分箱原因就在这里。4. 模型训练与三版迭代automatic_learning.py与best_model.pkl的完整链路4.1 选型理由逻辑回归、随机森林与XGBoost之间怎么选表格类二分类问题候选模型无非是线性模型、随机森林、梯度提升树这三条线它们各自的特点和在本项目里的位置不太一样。模型优势在本项目里的问题逻辑回归训练快、系数可解释非线性表达弱需要手动做特征交互随机森林不用标准化、抗过拟合精度通常弱于梯度提升XGBoost/LightGBM表格数据默认首选处理类别特征好参数多需要调我的习惯是逻辑回归和XGBoost两条线并行逻辑回归当baseline用来确认数据处理有没有问题XGBoost当主模型用来冲分数。automatic_learning.py里重点需要把握的是scale_pos_weight和搜索参数的范围。4.2 automatic_learning.py网格搜索、不平衡参数与最优模型保存automatic_learning.py做的事情是在预处理完成后用网格搜索跑一批XGBoost参数组合选验证集AUC最高的模型存成best_model.pkl并把结果写进best_score_log.json。核心流程如下import joblib import xgboost as xgb from sklearn.model_selection import GridSearchCV, StratifiedKFold X prep.transform(df) # 复用 preprocessing_pipeline.pkl pos y.sum() neg len(y) - pos param_grid { max_depth: [3, 5, 7], min_child_weight: [1, 3, 5], subsample: [0.8, 1.0], colsample_bytree: [0.8, 1.0] } model xgb.XGBClassifier( n_estimators300, learning_rate0.05, scale_pos_weightneg / pos, # 处理正负样本不平衡 random_state42, eval_metricauc, early_stopping_rounds20, use_label_encoderFalse ) cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) gs GridSearchCV(model, param_grid, cvcv, scoringroc_auc, n_jobs-1) gs.fit(X, y) print(gs.best_params_, gs.best_score_) joblib.dump(gs.best_estimator_, models/best_model.pkl)这里先复用第3章建好的prep做特征转换然后统计正负样本数量算出scale_pos_weight。网格搜索在5折分层交叉验证上跑评分用roc_auc最后把最优模型保存为best_model.pkl。scale_pos_weightneg/pos是处理不平衡的核心参数。比如正样本5000、负样本45000这个值就是9左右相当于给正样本损失函数加权。early_stopping_rounds20配合eval_metricauc验证集上连续20轮AUC不提升就停。use_label_encoderFalse是新版XGBoost的要求否则会报警告如果你的版本已经移除了这个参数去掉即可。GridSearchCV的cv参数传StratifiedKFold对象而不是直接传整数5这样做能保证每折正负样本比例一致这是类别不平衡场景下交叉验证的基本功。4.3 从V1.1到V1.3三版Notebook的迭代逻辑与实验记录压缩包里有三个Notebook版本认购产品_V1.1.ipynb、认购产品_V1.2.ipynb和认购产品_V1.3.ipynb这个迭代路径值得照着学。V1.1是基础EDA和逻辑回归基线重点解决“数据能不能跑通”001和002开头的基础分布图大多是这一阶段生成的。V1.2加入category_bin.py和tree_transform_bin.py的特征工程换成XGBoost做调参003系列正样本分布图从文件名看是这个阶段的产物。V1.3把预处理固化成preprocessing_pipeline.pkl完成最优阈值搜索并输出submission_v1.3.csv。logging目录下的best_score_log.json记录了每次实验的最优参数和分数。这个习惯很值得抄作业哪怕是临时跑的实验只要把参数和得分写进日志对比就有依据答辩时也能清楚讲出“基线-优化-固化”三段式改进过程。4.4 两个pkl文件的关系预处理与模型必须配套使用models目录下有两个关键文件preprocessing_pipeline.pkl负责把原始字段转换成模型输入best_model.pkl负责预测。两个文件必须配套使用不能用另外一份数据的预处理管道来转换当前测试集。因为它们是一起fit出来的统计量、编码映射、列顺序都绑定在同一套训练数据上。项目里还包含一个dot转png.txt它是把树结构导出成dot格式再转成图片的辅助脚本画模型结构图或者答辩展示树的分裂路径时很实用不属于主线流程。到这一步训练链路已经完整了原始数据进prep转换结果进model输出概率后按阈值切分成0/1。5. 避坑指南类别不平衡、时间泄漏与Pipeline复用里的五个常见问题5.1 准确率94%但正样本recall不到20%模型根本不能用现象模型准确率很高混淆矩阵一看几乎全是负样本被正确预测真正认购的那部分客户绝大多数没被捞出来。原因认购率本身只有百分之几模型学到“全部预测0”这条捷径就能拿到高准确率。准确率指标在这种分布下没有意义。解决评估以AUC和F1为主。训练时设置scale_pos_weightneg/pos或者在样本层面做下采样。最终阈值不要用默认0.5放到验证集上按F1最大值或业务成本函数重新选。5.2 随机切分验证AUC高按时间切分就掉现象5折交叉验证AUC约0.92按时间顺序把后半段数据作为验证集AUC掉到0.8以下。原因emp_var_rate、cons_conf_idx、euribor3m这些宏观指标随时间漂移。随机切分时同期的宏观数据会同时出现在训练集和验证集模型实际记住了“某个利率对应某个时期”而不是客户真实的认购意愿。解决建模和验证都按时间切分比如用前面80%的数据训练、后面20%验证。如果跨时间稳定性是重点考虑用时间序列交叉验证同时单独观察宏观特征的重要性判断模型是否过度依赖时期信息。5.3 预测阶段报错编码器遇到训练时没见过的类别现象加载模型后对test.csv做predictOneHotEncoder或LabelEncoder直接抛unknown category错误。原因test.csv和train.csv的类别分布不完全一致个别类别只在测试集里出现编码器训练时用的handle_unknown默认值是error。解决预处理阶段统一用OneHotEncoder(handle_unknownignore)未知类别变成全零向量或者在category_bin.py阶段就把所有低频类别合并成other从源头减少未知类别出现的概率。5.4 重复训练结果对不上随机种子没固定现象同一份代码连续跑两次best_score_log.json里记录的AUC差0.01到0.02参数看起来没变分数却不一致。原因XGBoost内部有随机性部分模型和交叉验证切分没有固定random_state每次运行的数据划分和采样都不一样。解决所有树模型显式设置random_state42GridSearchCV的cv传入带固定种子的StratifiedKFold对象每次实验前把随机种子、数据版本、最优参数一起写进日志。5.5 加载best_model.pkl后predict_proba输出异常现象模型加载正常但predict_proba的负样本概率全在0.5附近输出分布和训练时完全不一样。原因常见的是训练时保存的不是最终booster比如在早停之前保存了中间版本或者XGBoost版本升级后加载旧模型内部参数解释出现偏差。解决确认保存的是gs.best_estimator_而不是某个中途模型joblib.load之后打印model.get_params()核对关键参数如果问题依旧固定XGBoost版本重训一次并把版本号写进日志。6. 用模型文件做一次完整预测submission生成与阈值验证技巧6.1 加载两个pkl直接生成submission.csvimport joblib import numpy as np import pandas as pd prep joblib.load(models/preprocessing_pipeline.pkl) model joblib.load(models/best_model.pkl) test_df pd.read_csv(dataset/test.csv) X_test prep.transform(test_df) proba model.predict_proba(X_test)[:, 1] y_pred (proba 0.38).astype(int) pd.DataFrame({id: test_df[id], y: y_pred}).to_csv( results/submission_final.csv, indexFalse )先加载预处理管道和模型test.csv经过prep.transform完成与训练集同样的编码和标准化得到预测概率后用验证集上选好的阈值转成0/1。0.38只是示例具体以你的验证结果为准。6.2 阈值别用0.5在PR曲线上找最优切点AUC高不等于切的点合适在类别不平衡的营销场景里阈值选择要按业务成本来。常见做法是在验证集上算precision-recall曲线取F1最大的点作为阈值from sklearn.metrics import precision_recall_curve p, r, t precision_recall_curve(y_val, proba) f1 2 * p * r / (p r) best_t t[np.argmax(f1)] print(best_t)如果业务上能估算“一个电话成本”和“一个认购客户收益”也可以用成本函数替代F1来选点这样切出来的阈值才是真正对业务负责的。从那以后我每次跑完一版模型都强制走一遍“训练-保存-加载-提交”的完整流程确认生成csv的行数、列名、0/1分布都合理再进入下一轮调参。这个习惯帮我挡掉了不少低级错误也希望帮到你。本文还有配套的精品资源点击获取
返回列表