
简介这套项目包定位于银行客户产品认购预测场景属于完整的Python机器学习入门到进阶实战资源适合数据分析学习者、算法工程师以及银行营销建模相关从业者。项目以银行客户行为数据为基础完整覆盖数据预处理、特征工程、多种分类算法对比训练、模型评估与结果导出流程最终形成可用于精准营销决策的高准确率分类模型。资源共79个文件压缩包约10.47MB包含3个版本的Jupyter Notebook建模脚本、5个Python辅助脚本、训练集/测试集/提交结果CSV、特征说明Excel、预处理流水线与最优模型pkl以及43张特征分布图可支撑从数据探索到模型落地的全链路复现。目前已有42人学习下载。借助多版本Notebook可对比调优过程图表与字段说明便于撰写分析报告模型文件可直接迁移到类似金融数据集同时附带备份文件与说明文档便于追溯版本调整与工程组织方式。1. 银行客户认购预测一份能直接跑通的 Python 机器学习全流程源码做银行零售业务的朋友应该都有体会电话营销的响应率低得离谱一百个客户里能有两三个愿意听你讲完产品就不错了。与其靠话术碰运气不如用机器学习在拨号前先筛一遍名单。这份《银行客户产品认购预测系统源码》做的就是这件事——基于 Python 建模根据客户的年龄、职业、负债情况、历史联系记录以及宏观利率指标预测客户会不会认购产品。它不是教学 demo而是带着完整数据预处理、特征工程、多模型对比和提交结果的一整套项目训练集、测试集、字段说明、模型文件全在里面适合想系统看一遍真实营销建模流程的人也适合正在做金融风控或精准营销项目、需要一份参考实现的人。源码目录里的automatic_learning.py、category_bin.py、preprocessing_pipeline.pkl这些文件基本就是建模全流程的骨架照着拆能省不少事。2. 先读数据与 EDA从 36 张分布图里读出建模决策2.1 字段结构营销活动、客户画像与宏观指标三类特征项目根目录下的字段说明.xlsx把每个字段的定义理得很清楚。这种数据集的字段可以分成三类第一类是客户静态画像包括年龄、职业、婚姻状况、教育水平、是否有房贷、是否有个人贷款、信用卡是否有违约记录第二类是本次营销过程的动态信息包括联系方式、联系的月份和星期几、上一次联系的时长、活动期间联系客户的次数、上次联系后的间隔天数、之前营销活动的结果第三类是当时的宏观经济快照包括就业变动率、消费者信心指数、消费者价格指数、银行同业拆借率 3 个月利率、雇员人数。这三类特征在建模时的定位完全不一样。客户画像特征是稳定信号一个人有没有房贷、教育水平高不高短期内不会变宏观特征是环境变量影响的是某一批客户整体的购买意愿营销过程特征是最容易被忽视但也最容易出问题的部分尤其是“上一次联系的时长”这类字段后面避坑章我会专门讲。先看训练集的结构用 Pandas 读一下import pandas as pd train pd.read_csv(train.csv) print(train.shape) # 输出行数和列数确认样本规模 print(train.dtypes) # 检查每个字段类型判断哪些是类别、哪些是数值 print(train.head()) # 扫一眼前几行确认是否有空值和异常表达这里dtypes输出里job、marital、education、contact、month这类字段肯定是 object 类型后面要做类别编码age、duration、campaign、euribor3m这些是数值类型但要留意它们量纲差异很大euribor3m是零点几到几的利率duration可能是几十到几千秒不归一化直接喂给线性模型会被大数值字段带偏。2.2 EDA 图怎么读正样本分布对比图到底在看什么figures目录里几十张 png 是这个项目最值钱的部分之一它把探索性分析的结论直接固化成图了不用自己重新跑一遍。文件名有一个规律前缀001_开头的是全量分布图比如001_年龄分布.png、001_婚姻分布.png002_开头的是连续型特征的分布比如002_就业变动率分布.png、002_消费者信心指数分布.png003_开头的是“正样本分布”也就是只有目标y1的那些客户在各特征上的分布比如003_年龄正样本分布.png、003_联系时长正样本分布.png。读图的方法是对比先把003_职业正样本分布.png和001_职业分布.png放在一起看如果正样本里“retired”占比明显高于全量里的占比说明退休人群的认购倾向偏高这个字段就是有区分度的信号。再看003_是否有房贷正样本分布.png如果正样本里无房贷的比例显著高于全量说明房贷状态对购买意愿有负向影响。这类“正样本分布 vs 全量分布”的对比是判断类别特征有没有用的最快方式。数值特征看的是形态。002_年龄分布.png如果是右偏的建模时可以考虑做 log 变换002_银行同业拆借率3个月利率分布.png如果正样本集中在某个区间说明利率水平对认购有区间效应后续做特征工程时可以按分箱处理而不是直接丢给模型。2.3 目标不平衡是第一个要正视的问题001_目标分布.png告诉你训练集里正样本占比通常只有 10% 上下。这个比例对建模策略的影响很大直接拿准确率当评估指标会得到“全都预测不买”这种假模型而银行营销场景更关心的是“在限定电话量的前提下尽量把会买的人捞出来”。所以后面的模型评估必须围绕 AUC、召回率、F1 这类指标转不能只看准确率这点在第四章会展开。3. 特征工程与预处理管线category_bin.py 与 preprocessing_pipeline.pkl3.1 类别字段的二元化思路不是所有模型都吃 One-Hotcategory_bin.py这个脚本名字很有意思“bin”表示二元化。常见做法是把多类别字段压缩成少数几个有业务含义的二值特征。比如marital字段按“是否已婚”拆一个特征housing和loan本身就是 yes/no映射成 1/0 即可poutcome这种多类别字段可以把“之前营销成功”映射成 1其余映射成 0。这么做的直接原因是项目里会跑树模型从tree_transform_bin.py这个文件名也能看出来而树模型做分裂时不擅长处理高基数 One-Hot 展开后的大量稀疏列。下面是用映射表做二元化的核心逻辑import pandas as pd def category_bin(df, mapping_dict): 按字段的二元化映射表做转换 mapping_dict 示例 {marital: {married: 1, single: 0, divorced: 0}, housing: {yes: 1, no: 0}} df df.copy() for col, mapping in mapping_dict.items(): if col in df.columns: # 用 map 做值替换未命中的类别填 -1防止训练集和测试集类别不一致时报错 df[col] df[col].map(mapping).fillna(-1).astype(int) return df mapping { marital: {married: 1, single: 0, divorced: 0}, housing: {yes: 1, no: 0}, loan: {yes: 1, no: 0}, default: {yes: 1, no: 0} } train_bin category_bin(train, mapping) print(train_bin[list(mapping.keys())].head())这段代码里最容易被忽略的是fillna(-1)。它解决的是训练集里某个类别在测试集里不存在的情况。比如训练集里marital有divorced但测试集来了一条unknownmap之后会变成 NaN如果不兜底后续直接astype(int)就会报错。给一个 -1 的取值等于告诉模型“这是一个没见过的状态”比报错强。3.2 预处理管线为什么一定要把标准化和编码串成 Pipeline项目里preprocessing_pipeline.pkl是直接用joblib.dump保存的完整 sklearn Pipeline。它的作用是把“缺失值填充、数值标准化、类别编码”这三步打包成一个对象训练时只 fit 一次预测时直接 transform保证训练和预测走的是同一套逻辑。这个设计非常关键很多人建模时预处理代码写到函数里训练时跑一遍、预测时再复制粘贴一遍一旦两边代码改了不一致线上预测效果就会莫名其妙变差。管线构建的常见做法是from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.impute import SimpleImputer numeric_features [age, campaign, duration, euribor3m] categorical_features [job, education, contact, month] numeric_transformer Pipeline(steps[ (imputer, SimpleImputer(strategymedian)), # 数值字段用中位数填充抗离群点 (scaler, StandardScaler()) # 标准化让均值0方差1 ]) categorical_transformer Pipeline(steps[ (imputer, SimpleImputer(strategymost_frequent)), # 类别字段用众数填充 (onehot, OneHotEncoder(handle_unknownignore)) # 忽略未知类别防止预测报错 ]) preprocessor ColumnTransformer(transformers[ (num, numeric_transformer, numeric_features), (cat, categorical_transformer, categorical_features) ]) pipeline Pipeline(steps[(preprocess, preprocessor)]) pipeline.fit(train[numeric_features categorical_features])参数上注意两个点。SimpleImputer的median策略对偏态分布的数值字段比mean稳像duration这种长尾分布均值会被极值拉高填充出来的值反而失真。OneHotEncoder的handle_unknownignore一定要设否则线上预测时测试集出现一个新职业类别整个 Pipeline 直接抛异常。这是最容易翻车的地方后面避坑章会再提一次。3.3 feature_attribute.json用配置驱动整个特征流程feature_attribute.json这个文件的价值在于它把“哪个字段是数值、哪个字段是类别、哪个是目标变量”从代码里抽出来了。这样训练脚本、预测脚本、自动化脚本都读同一个配置文件改字段不用改代码只改 JSON。常见结构是{ numerical: [age, campaign, duration, prev_contact_days], categorical: [job, marital, education, contact, month], target: y, drop: [user_id] }有了这个配置automatic_learning.py在训练时可以直接按 key 读字段列表去构造 ColumnTransformer不用每个脚本硬编码一次字段名。这个习惯建议保持尤其在项目版本迭代过程中V1.1 到 V1.3 字段若发生了变化改 JSON 比改三处代码靠谱得多。4. 自动学习与模型调参automatic_learning.py 怎么选出 best_model4.1 训练脚本的整体骨架先固定评估策略automatic_learning.py是整份资源里最值得读的一个脚本因为它把“自动学习”做成了可重复的流程加载数据 → 预处理 → K 折交叉验证 → 多模型评估 → 保存最优模型和分数记录。它不只是一个训练脚本更像是一个建模实验管理工具。第一步是先指定评估策略。由于正样本只有 10% 左右直接随机划分训练集和验证集验证集里可能一两条正样本都没有。项目里用的是分层 K 折交叉验证确保每一折里正样本比例都和全量一致from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for fold, (train_idx, valid_idx) in enumerate(skf.split(X, y)): X_train, X_valid X.iloc[train_idx], X.iloc[valid_idx] y_train, y_valid y.iloc[train_idx], y.iloc[valid_idx] # 每一折独立做预处理和训练n_splits5是营销场景下比较常用的折数样本量不大时 5 折比 10 折更稳每折的验证集足够大评估指标的方差也小一些。random_state42固定下来是为了让实验结果可复现不然每次跑出来的分数都不一样没法判断模型改动到底有没有效果。4.2 多模型对比不要只用一种算法定生死项目摘要里提到“采用多种机器学习算法进行对比实验”这在automatic_learning.py里体现为对多个分类器的统一封装。常见做法是定义一个模型候选集逐个跑 K 折交叉验证收集每一折的 AUC最后比较均值。from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier from sklearn.metrics import roc_auc_score import joblib models { lr: LogisticRegression(max_iter1000), rf: RandomForestClassifier(n_estimators300, random_state42), gbdt: GradientBoostingClassifier(random_state42) } best_score 0 best_model None for name, model in models.items(): auc_scores [] for fold, (train_idx, valid_idx) in enumerate(skf.split(X, y)): X_train, X_valid X.iloc[train_idx], X.iloc[valid_idx] y_train, y_valid y.iloc[train_idx], y.iloc[valid_idx] model.fit(X_train, y_train) auc roc_auc_score(y_valid, model.predict_proba(X_valid)[:, 1]) auc_scores.append(auc) avg_auc sum(auc_scores) / len(auc_scores) print(f{name}: {avg_auc:.4f}) if avg_auc best_score: best_score avg_auc best_model model joblib.dump(best_model, models/best_model.pkl)注意这里比较的是predict_proba出来的概率不是predict出来的 0/1 标签。AUC 本身就是基于排序的指标必须用概率算。模型里LogisticRegression设置了max_iter1000是因为默认的 100 次迭代在特征较多时可能不收敛日志里会出现ConvergenceWarning这个属于最常见的警告之一。4.3 评估指标为什么银行营销场景必须盯 AUC 和 Recall银行电话营销的成本结构决定了评估指标的选取每通电话的呼叫成本固定一个认购客户带来的收益远高于单通电话成本。所以建模目标不是“整体预测准确率高”而是在尽量少的电话里覆盖尽量多的实际认购客户。这对应两个指标AUC 衡量模型对正负样本的排序能力值越高说明“打给高概率客户”的策略越有效Recall 衡量“实际会买的客户里模型捞出来了多少”在限定电话量的前提下Recall 比 Precision 更贴近业务诉求。如果模型只输出最终类别项目里的submission.csv相关性分析就没法做。好在best_score_log.json里记录的也是 AUC 这类分数说明作者一开始就用了正确的评估视角。4.4 版本迭代逻辑V1.1 到 V1.3 改了什么东西notebook目录里有认购产品_V1.1.ipynb、认购产品_V1.2.ipynb、认购产品_V1.3.ipynb三个版本results目录下对应submission_v1.3.csv。三版迭代最能说明建模过程的演进。V1.1 通常是最粗糙的一版可能只做了 One-Hot 编码加逻辑回归V1.2 加入了更多特征工程比如对连续变量分箱、对类别变量做目标编码V1.3 把预处理流程固定成了 Pipeline并且调整了模型超参数。对比这几个版本时除了看分数变化更重要的是看结构变化。把 V1.1 到 V1.3 的 notebook 从 EDA 到建模的代码 diff 一下能看到作者在每个版本里动了哪些特征、加了哪些样本处理逻辑。这比直接看最终版收获大因为你能看到“为什么上一个版本分数上不去”。best_score_log.json里会记录每个版本的最优分数可以拿来和best_model.pkl核对确认当前最优模型对应的是哪一个预处理版本模型和管线不匹配会让预测结果完全错误。5. 避坑指南翻车率最高的五个地方5.1 data leakageduration 字段让分数虚高现象模型 AUC 跑出 0.95 以上但心里总觉得不对劲这分数高得不真实。原因duration字段是“上一次联系的时长”只有电话已经打出去了才会有这个值。营销预测场景里你不可能还没拨号就知道这通电话会打多久。竞赛数据集里这个字段明明存在但真实业务里它属于“未来才知道的信息”拿它建模等于作弊。解决把duration从特征里直接删掉。如果不舍得至少要做一个“有 duration 和无 duration 两套模型”的对比实验验证一下没有这个字段时模型的真实水平。这个也是业界公认的 Bank Marketing 数据集水榜重灾区。5.2 测试集出现没见过类别值预测阶段直接抛异常现象训练和调参都正常一跑test.csv的预测脚本就报ValueError: Found unknown categories。原因训练集里job字段没有student这个取值但测试集里有OneHotEncoder默认遇到新类别就报错。解决如上面 3.2 里写的给OneHotEncoder设置handle_unknownignore。如果是自己写的映射函数用fillna(-1)兜底。这个坑在银行场景特别容易遇到因为职业、教育水平这类字段的口径在不同批次数据里经常对不齐。5.3 交叉验证每折重新 fit 预处理导致验证集信息泄漏现象交叉验证的 AUC 很高但用独立测试集评估时分数掉了一大截。原因有人在 K 折循环之前就把全量数据做了标准化或编码相当于验证集的数据分布信息已经被“看”过了这叫数据泄漏。标准化用的是全量的均值和方差验证集的信息自然被编码进了训练过程。解决预处理必须在每一折内部完成先切分训练集和验证集再在训练集上fit预处理器然后transform验证集。用 Pipeline 的好处就在于它强制把流程绑定到模型上fit 和 transform 不会错位。如果不用 Pipeline每折循环里都要手动先fit再transform。5.4 submission.csv 格式不对导致评分失败现象本地跑得好好的一提交却发现评分脚本报错或者得分为 0。原因submission.csv通常要求两列一列是客户 ID一列是预测结果。有人直接导出了带索引的 DataFrame有人把id列弄丢了有人提交的是 0/1 标签但评分方要求的是概率值。解决写出提交文件前先print(submission.head())确认列名和内容。客户 ID 列直接来自test.csv不要自己重新生成索引预测列建议输出概率而不是硬标签概率可以灵活调整阈值标签一旦写死就丢掉了排序信息。生产习惯是先保存成两列无索引的 CSV再重新读一次确认格式。5.5 .zbak 文件混淆备份当成正式文件加载现象加载preprocessing_pipeline.pkl.zbak和best_model.pkl后预测结果全是同一个值检查半天发现是 pkl 文件本身就不同。原因项目里所有脚本和模型都有.zbak后缀的备份文件它们是之前的旧版本。如果把.zbak当成有效文件加载管线版本和模型版本不匹配特征顺序可能都不一样结果自然不对。解决加载模型时只看models目录下的无后缀文件.zbak一律不碰。best_score_log.json里记录了对应版本号加载完模型后打印一下特征数或模型类型做个 sanity check。这个血泪经验提醒我每个项目都要在模型文件旁边放一个说明字段注明训练时间和对应代码版本。6. 从复现到落地用 best_model.pkl 接一个可复用的预测入口6.1 加载管线与模型封装成 predict 函数项目的最终产物是models/best_model.pkl和models/preprocessing_pipeline.pkl这两个文件必须成对使用先transform再预测。不要只加载模型以为模型是独立存在的——它前面所有预处理步骤都跑在管线里脱离管线直接预测会导致字段顺序错乱甚至维度不匹配。import joblib import pandas as pd model joblib.load(models/best_model.pkl) preprocessor joblib.load(models/preprocessing_pipeline.pkl) def predict_proba(test_df): X_processed preprocessor.transform(test_df) return model.predict_proba(X_processed)[:, 1]这一段代码量不多但有一个关键点preprocessor.transform用的是transform而不是fit_transform因为预处理器的参数已经在训练时确定好了预测阶段不能重新学习。很多人习惯性地写fit_transform在预测阶段就会让管线重新拟合结果等于用训练数据重新训练了一遍预处理线上预测自然变差。6.2 生成提交文件时的两个细节拿到测试集后生成提交结果要注意两点。第一id列先抽出来单独保存预处理时把它 drop 掉因为模型不需要把 ID 作为特征第二预测结果建议输出概率这样后续可以按不同的阈值调整营销名单规模。test pd.read_csv(test.csv) test_ids test[id] # 先保存ID列后续要原样写回提交文件 proba predict_proba(test.drop(columns[id])) submission pd.DataFrame({id: test_ids, y: proba}) submission.to_csv(results/submission_v1.3.csv, indexFalse) # 校验一下输出 print(submission.head()) print(submission[y].describe())y列的分布打印出来看一眼如果概率值全部集中在 0.1 以下说明模型输出的不是概率或者预处理管线没加载对。正常情况概率分布应该有一定梯度均值大概在正样本比例附近。6.3 没有测试集时怎么办项目目录里虽然带了test.csv但实际做模型评估时我更建议把train.csv自己切一块出来当测试集专门留到最后用。这样能看到模型在完全没参与训练的数据上的真实表现。automatic_learning.py里已经有交叉验证但交叉验证的分数是多次平均会低估模型在单批数据上的波动留出法测试集更接近真实线上表现。留出比例 20% 比较常见同样要做分层抽样保证测试集里正样本比例和训练集一致。从那以后我每次拿到这类营销预测项目都会先检查一遍train.csv和test.csv的列名是否对齐再用固定 seed 跑一遍全流程最后核对提交文件的列名和概率分布。这一步看起来琐碎省掉它大概率要返工。希望你也能把这份源码里最实用的部分拆走改造成自己手边能直接用的项目。本文还有配套的精品资源点击获取