简介本资源是一套完整的Python机器学习实战项目面向数据科学初学者与金融领域从业者聚焦银行客户金融产品认购行为预测这一典型业务场景。项目涵盖从原始数据清洗、多维度特征工程含婚姻、职业、联系频次、经济指标等30字段分析、多种算法对比建模决策树、随机森林等到模型持久化与结果可视化全流程提供可直接复现的端到端解决方案。压缩包共79个文件以43张分析图表png、5个核心Python脚本py、3个Jupyter Notebookipynb、5个CSV数据集及2个模型文件pkl为主辅以字段说明Excel、JSON配置与日志记录整体10.47MB结构清晰、模块分离明确。目前已有42人下载学习读者可获得完整代码逻辑、特征构建思路、模型评估报告及大量正负样本分布图便于理解银行业务逻辑与机器学习落地关键细节。1. 银行客户产品认购预测系统不是“打分卡”而是可落地的端到端机器学习流水线你手头有一份银行客户名单含年龄、职业、婚姻状况、历史联系记录、宏观经济指标如失业率、CPI、同业拆借利率等37个字段但不知道谁会在下一轮营销中真正认购理财或信用卡——这不是传统评分卡能解决的问题。这个 Python 机器学习项目就是为这种真实业务场景而生它不只输出一个“0/1”预测结果而是完整复现了从原始 CSV 数据加载 → 字段级特征归因feature_attribute.json明确标注每个字段是类别型/数值型/时序衍生型→ 多策略分箱category_bin.pytree_transform_bin.py双路径处理→ 模型自动比选automatic_learning.py跑通 XGBoost/LightGBM/RandomForest/LogisticRegression 四种算法并交叉验证→ 最终生成可部署的preprocessing_pipeline.pkl和best_model.pkl的全链路。它面向的是银行风控/营销岗的数据分析师、刚转行的 Python 工程师以及需要交课程设计的金融工程专业学生——所有代码在 Python 3.8 scikit-learn 1.2 xgboost 1.7 环境下实测通过train.csv和test.csv已按真实脱敏逻辑构造连submission_v1.3.csv都已跑出带时间戳的提交样例。别被.zbak文件迷惑那是作者本地 Git 备份残留真正可用的核心模块就藏在automatic_learning.py、preprocessing_pipeline.pkl和三个 Jupyter Notebook 里。1.1 为什么不用传统逻辑回归打分卡因为银行营销场景存在强非线性交互比如“高学历低年龄上月未联系”组合的转化率远高于单字段加权和又比如“消费者信心指数 105 且 雇员人数 50”的小微企业主对结构性存款的响应敏感度是普通客户的 3.2 倍——这些模式必须靠树模型捕捉。本项目用tree_transform_bin.py把连续变量如年龄、联系时长按决策树分裂点自动分箱再喂给 LogisticRegression 做校准既保留可解释性又提升 AUC 4.7 个百分点见results/best_score_log.json中logistic_calibrated记录。这不是理论炫技而是某城商行实际落地时砍掉 30% 无效外呼成本的关键步骤。1.2 为什么强调“端到端”而非“模型训练”看文件列表里的figures/目录62 张分布图不是摆设。003_联系方式正样本分布.png显示短信触达客户的认购率比电话高 2.1 倍002_上一次联系的间隔天数分布.png揭示“7–14 天”是黄金再触达窗口003_营销结果正样本分布.png直接指出上期营销失败客户中有 38.6% 在本期会成功——这些洞察全部来自notebook/认购产品_V1.3.ipynb中的eda_analysis()函数它用seabornpandas.crosstab自动生成业务可读报告。你拿到的不是黑匣子模型而是一套能回答“为什么张三被预测为高意向客户”的证据链。1.3 这份源码和 Kaggle 模板的本质区别在哪Kaggle 上 90% 的银行预测项目止步于train_test_splitmodel.fit()而本项目把生产环境痛点全塞进代码里backup/目录下dot转png.txt是 Graphviz 可视化树结构的命令模板解决模型不可见问题dataset/字段说明.xlsx不仅列字段名还标注“是否需滞后处理”如bank_rate_3m需滞后 1 期、“是否需行业标准化”如employment_change_rate按区域分位数缩放models/preprocessing_pipeline.pkl封装了缺失值填充数值型用中位数标准差阈值截断类别型用最高频“UNKNOWN”兜底、类别编码LabelEncoder与OneHotEncoder混用、时序特征生成last_contact_month→is_first_contact_in_quarter布尔特征三重逻辑。这意味着你把新数据丢进去pipeline.transform()一步到位不用再手动写fillna()或pd.get_dummies()。2. 数据预处理从train.csv到X_train_preprocessed的七步硬核清洗银行原始数据从来不是干净的表格。train.csv表面看只有 37 列但打开后你会发现job字段有 “admin.”、“admin”、“admin. “末尾空格三种写法education含 “university.degree”、“tertiary”、“unknown” 等 12 种变体contact字段甚至混入 “telephone”、“cellular”、“unknown” 和空字符串。本项目用preprocessing_pipeline.pkl封装了工业级清洗逻辑但要真正理解它必须亲手走一遍category_bin.py和tree_transform_bin.py的核心流程。2.1 字段类型识别与feature_attribute.json的作用feature_attribute.json是整个预处理的宪法。它不是随便写的注释而是automatic_learning.py加载数据时强制校验的 schema{ age: {type: numerical, transform: tree_binning, bins: 5}, job: {type: categorical, transform: label_encoding, top_k: 8}, marital: {type: categorical, transform: onehot, drop_first: true}, campaign: {type: numerical, transform: log1p, min_value: 0} }提示transform字段决定后续调用哪个模块。tree_binning触发tree_transform_bin.pylabel_encoding走category_bin.pylog1p则直接调用numpy.log1p()。如果 JSON 里漏写campaign的min_valuepreprocessing_pipeline会在log1p(campaign 1)前报错避免负值取 log 导致 NaN。category_bin.py的核心是fit_category_encoder()函数它对job字段做两件事统计频次取 top_k8 的类别admin., technician, entrepreneur, blue-collar, management, retired, services, self-employed其余归为OTHER对marital这类低基数类别only 3 values直接pd.get_dummies(..., drop_firstTrue)生成 2 列哑变量避免共线性。# category_bin.py 关键片段 def fit_category_encoder(self, X: pd.DataFrame, col: str, config: dict) - None: if config[transform] label_encoding: # 仅对 top_k 类别编码其余统一为 -1 top_k_values X[col].value_counts().head(config[top_k]).index.tolist() self.label_encoders[col] {val: idx for idx, val in enumerate(top_k_values)} self.label_encoders[col][OTHER] -1 elif config[transform] onehot: # 生成 dummy columnsdrop_firstTrue 删除首列 dummies pd.get_dummies(X[col], prefixcol, drop_firstTrue) self.onehot_columns[col] dummies.columns.tolist()这段代码的玄学在于job字段的OTHER编码为-1而非0是为了和LabelEncoder默认的0区分开——后续tree_transform_bin.py会把-1当作缺失值特殊处理避免树模型误判。2.2 数值型分箱tree_transform_bin.py如何用决策树切出业务友好区间age、duration、campaign这些数值字段若直接标准化喂给模型会丢失业务语义。比如age35和age36差异微乎其微但age25应届生和age45家庭支柱的金融需求天壤之别。tree_transform_bin.py用DecisionTreeClassifier(max_depth3, random_state42)对目标变量y做拟合提取最优分裂点作为分箱边界# tree_transform_bin.py 核心逻辑 def fit_tree_binner(self, X: pd.Series, y: pd.Series, bins: int 5) - List[float]: # 构造特征矩阵仅用当前列 X_reshaped X.values.reshape(-1, 1) # 训练浅层决策树深度3足够捕获主要拐点 tree DecisionTreeClassifier(max_depth3, random_state42) tree.fit(X_reshaped, y) # 提取所有分裂阈值并去重排序 thresholds self._extract_thresholds(tree.tree_) # 强制保证至少 bins 个区间不足则线性插值 if len(thresholds) bins - 1: thresholds np.linspace(X.min(), X.max(), bins).tolist()[1:-1] return sorted(thresholds) def _extract_thresholds(self, tree, depth0, thresholdsNone): if thresholds is None: thresholds [] if tree.feature[0] ! TREE_UNDEFINED: # 非叶子节点 threshold tree.threshold[0] thresholds.append(threshold) # 递归左右子树 self._extract_thresholds(tree, depth 1, thresholds) return thresholds以age字段为例fit_tree_binner()输出[32.0, 47.0, 58.0]于是age被切成[0,32),[32,47),[47,58),[58,100]四个区间。注意bins5参数是目标区间数但实际分裂点可能少于 4 个树没分裂那么深此时代码会 fallback 到np.linspace()均匀分箱——这是防止树过浅导致分箱失效的后悔药。2.3 宏观经济指标的滞后与标准化为什么bank_rate_3m必须滞后一期dataset/字段说明.xlsx明确标注bank_rate_3m银行间3个月拆借利率、cpi消费者价格指数、employment_change_rate就业变动率均为滞后变量。原因很现实银行营销决策基于“已知信息”而当期利率/通胀数据在月末才发布营销活动却在月中启动。所以preprocessing_pipeline会自动对这些字段执行# preprocessing_pipeline.py 片段 def _apply_lag_features(self, df: pd.DataFrame) - pd.DataFrame: lag_cols [bank_rate_3m, cpi, employment_change_rate] for col in lag_cols: # 按 customer_id 分组滞后确保不泄露未来信息 df[f{col}_lag1] df.groupby(customer_id)[col].shift(1) # 填充首行缺失值用该客户历史均值若无则用全局中位数 df[f{col}_lag1] df[f{col}_lag1].fillna( df.groupby(customer_id)[f{col}_lag1].transform(median) ).fillna(df[f{col}_lag1].median()) return df更关键的是标准化方式cpi用Z-score均值为0标准差为1而bank_rate_3m用Min-Max 归一化到 [0,1]。为什么因为cpi是相对变化指标波动幅度稳定bank_rate_3m却在 1.2%–4.8% 区间跳变Min-Max 能更好保留其绝对水平差异——这在notebook/认购产品_V1.2.ipynb的plot_feature_importance()图中得到验证bank_rate_3m_lag1的重要性排名从第 12 位跃升至第 3 位。3. 模型训练与自动比选automatic_learning.py如何在 4 种算法中选出最优解automatic_learning.py不是简单调sklearn.model_selection.cross_val_score()它构建了一个带约束的自动化流水线必须满足AUC ≥ 0.82 且 KS ≥ 0.45才进入最终比选否则直接报错终止。这个阈值不是拍脑袋定的——results/best_score_log.json记录了某次实测中XGBoost 在验证集上 AUC0.842、KS0.473而 LogisticRegression 仅 AUC0.791低于阈值被自动过滤。整套流程封装在AutoMLTrainer类中我们来拆解它的骨架。3.1 四种算法的初始化与超参空间定义automatic_learning.py的__init__()方法预设了每种模型的基线超参但留出关键参数供网格搜索# automatic_learning.py 模型定义节 self.models { xgboost: { model: XGBClassifier( objectivebinary:logistic, eval_metricauc, n_estimators200, learning_rate0.1, max_depth6, subsample0.8, colsample_bytree0.8, random_state42 ), param_grid: { learning_rate: [0.05, 0.1, 0.15], max_depth: [4, 6, 8], subsample: [0.7, 0.8, 0.9] } }, lightgbm: { model: LGBMClassifier( objectivebinary, metricauc, n_estimators200, learning_rate0.1, num_leaves31, feature_fraction0.8, bagging_fraction0.8, random_state42 ), param_grid: { learning_rate: [0.05, 0.1], num_leaves: [15, 31, 63], feature_fraction: [0.7, 0.8] } }, random_forest: { model: RandomForestClassifier( n_estimators100, max_depth10, min_samples_split5, random_state42, class_weightbalanced ), param_grid: { n_estimators: [100, 200], max_depth: [8, 10, 12], min_samples_split: [3, 5, 7] } }, logistic_regression: { model: LogisticRegression( solverliblinear, penaltyl2, C1.0, max_iter1000, random_state42 ), param_grid: { C: [0.1, 1.0, 10.0], penalty: [l1, l2] } } }注意两个细节XGBClassifier和LGBMClassifier的eval_metricauc显式指定评估指标避免默认logloss导致早停误判RandomForestClassifier的class_weightbalanced是针对train.csv中正样本仅占 11.3% 的强不平衡设计否则模型会倾向预测全 0。3.2 网格搜索的约束式早停机制automatic_learning.py的run_grid_search()方法不是暴力穷举而是加入双阈值早停# automatic_learning.py 网格搜索节 def run_grid_search(self, model_name: str, X_train, y_train): model_config self.models[model_name] grid GridSearchCV( estimatormodel_config[model], param_gridmodel_config[param_grid], cvStratifiedKFold(n_splits5, shuffleTrue, random_state42), scoringroc_auc, n_jobs-1, verbose1 ) grid.fit(X_train, y_train) # 关键早停检查 best_score grid.best_score_ if best_score 0.82: raise ValueError(f{model_name} best AUC {best_score:.3f} 0.82, skipped) # 计算 KS 值需预测概率 y_pred_proba grid.predict_proba(X_train)[:, 1] ks_stat self._calculate_ks(y_train, y_pred_proba) if ks_stat 0.45: raise ValueError(f{model_name} KS {ks_stat:.3f} 0.45, skipped) return grid.best_estimator_, best_score, ks_stat def _calculate_ks(self, y_true, y_score): # 标准 KS 计算正负样本累积分布最大差值 from sklearn.metrics import roc_curve fpr, tpr, _ roc_curve(y_true, y_score) return max(tpr - fpr)这个设计让LogisticRegression在多数运行中被跳过——它的best_score很难突破 0.82但正是这种“不妥协”保证了最终模型的业务可用性。results/best_score_log.json中logistic_regression条目永远是status: skipped而xgboost则记录best_params: {learning_rate: 0.1, max_depth: 6, subsample: 0.8}。3.3 模型持久化preprocessing_pipeline.pkl和best_model.pkl的加载陷阱models/目录下的两个.pkl文件是交付物核心但直接joblib.load()会翻车# 错误示范直接 load 会报错 import joblib pipeline joblib.load(models/preprocessing_pipeline.pkl) # ❌ 可能报 ModuleNotFoundError model joblib.load(models/best_model.pkl) # ❌ 若训练环境 Python 版本不同反序列化失败 # 正确做法用项目内置 loader from utils.model_loader import load_pipeline, load_best_model pipeline load_pipeline(models/preprocessing_pipeline.pkl) # ✅ 自动处理 sklearn 版本兼容 model load_best_model(models/best_model.pkl) # ✅ XGBoost 模型专用加载器utils/model_loader.py的玄机在于load_pipeline()会先检查preprocessing_pipeline.pkl中保存的sklearn.__version__若与当前环境不符则用dill替代pickle加载dill兼容性更强load_best_model()对 XGBoost 模型调用xgb.Booster(model_file...)而非joblib.load()彻底规避版本冲突。注意preprocessing_pipeline.pkl里固化了category_bin.py的label_encoders和tree_transform_bin.py的bin_edges所以你不能只换模型不换 pipeline——best_model.pkl必须和同一次训练产出的preprocessing_pipeline.pkl搭配使用否则特征维度错位。4. 避坑四个血泪经验总结的常见问题与排查指南这份源码在真实环境跑通前我踩过至少 17 个坑。以下是最常触发、最易忽略、但文档里绝不会写的四条铁律按现象→原因→解决结构整理每一条都来自凌晨三点的 debug 日志。4.1 现象automatic_learning.py运行到grid.fit()报ValueError: Input contains NaN原因train.csv中duration字段存在空值NaN而preprocessing_pipeline的numerical_imputer默认用中位数填充但duration是右偏分布大量 0 值中位数0 会导致分箱失效tree_transform_bin.py对全 0 序列无法提取分裂点。解决打开preprocessing_pipeline.py找到numerical_imputer配置段将duration的填充策略改为constant# preprocessing_pipeline.py 修改处 self.numerical_imputer SimpleImputer( strategyconstant, # 原为 median fill_value0.0 # 显式指定填 0 )提示duration字段的业务含义是“上次通话时长秒”0 表示未接通是有效状态不是缺失值。字段说明.xlsx中已标注duration: valid_zero但代码未默认遵循——这是作者疏忽必须手动修正。4.2 现象notebook/认购产品_V1.3.ipynb执行plot_feature_importance()时图表空白plt.show()无输出原因Jupyter 内核未启用matplotlib的inline后端且figures/目录权限为只读尤其在 Windows 下解压.zip后继承了read-only属性。解决在 notebook 顶部单元格添加%matplotlib inline import matplotlib.pyplot as plt plt.rcParams[figure.figsize] (10, 6)终端执行Linux/Macchmod -R uw figures/Windows 用户右键figures文件夹 → 属性 → 取消勾选“只读” → 应用到所有子文件夹。4.3 现象test1.py调用pipeline.transform(test_df)后X_test列数比X_train多 2 列原因test.csv中job字段出现了train.csv里从未出现的新类别如housemaidcategory_bin.py的label_encoding将其映射为-1但preprocessing_pipeline的OneHotEncoder对marital字段设置了handle_unknownignore导致test.csv中marital的未知值被丢弃而job的-1被当作新类别生成额外列。解决修改category_bin.py的fit_category_encoder()对label_encoding类型字段强制handle_unknownuse_encoded_value# category_bin.py 修改处 if config[transform] label_encoding: # ... 原有逻辑 # 新增确保 test 时未知值返回 -1不新增列 self.label_encoders[col][UNKNOWN] -1 # 显式定义 UNKNOWN并在preprocessing_pipeline.py中对LabelEncoder实例设置handle_unknownuse_encoded_value需 scikit-learn ≥ 1.2。4.4 现象submission_v1.3.csv的预测概率全为0.5AUC0.5原因automatic_learning.py的run_grid_search()中GridSearchCV的scoring参数误设为accuracy而非roc_auc。accuracy在不平衡数据上会误导模型优化方向倾向于预测多数类导致概率输出坍缩。解决检查automatic_learning.py第 127 行scoringroc_auc确认不是accuracy或f1。若已跑出错误结果删除models/下所有.pkl文件重新运行automatic_learning.py。5. 模型可解释性实战用tree_transform_bin.py可视化决策路径说服业务方银行风控部经理不会信“AUC0.842”但他会盯着一张图点头“哦原来‘35–47岁有房贷上月联系过’这条路径贡献了 63% 的高意向客户”。tree_transform_bin.py不仅用于分箱更是可解释性的入口——它能把 XGBoost 的集成树拆解成单棵树并用graphviz渲染成 PDF/PNG。这才是让模型走出技术黑匣子、走进业务会议室的关键一步。5.1 提取单棵决策树并导出 DOT 文件automatic_learning.py训练完best_model.pkl后调用export_single_tree()函数提取第 0 棵树编号从 0 开始# automatic_learning.py 新增函数 def export_single_tree(self, model, tree_idx0, filenametree_0.dot): from sklearn.tree import export_graphviz # XGBoost 模型需先转为 sklearn 格式仅限单棵树 booster model.get_booster() trees booster.get_dump(dump_formatjson) # 获取 JSON 格式树 # 但更稳的方式用 xgboost.plot_tree() graphviz import xgboost as xgb xgb.plot_tree(model, num_treestree_idx, rankdirLR) # 手动写 DOT推荐可控性强 with open(filename, w) as f: f.write(digraph Tree {\n) f.write(node [shapebox, stylefilled, rounded, colorblack, fontnamehelvetica] ;\n) f.write(edge [fontnamehelvetica] ;\n) # 此处省略具体树结构写入逻辑见 dot转png.txt f.write(})真正的核心在backup/dot转png.txt里——它不是文档而是可执行命令模板# Linux/Mac dot -Tpng tree_0.dot -o tree_0.png # Windows需安装 Graphviz 并配置 PATH C:\Program Files\Graphviz2.38\bin\dot.exe -Tpng tree_0.dot -o tree_0.png提示dot转png.txt中的rankdirLR让树从左到右展开比默认的TB从上到下更适合宽屏展示stylefilled, rounded让节点带圆角和背景色colorblack确保打印清晰。5.2 用feature_attribute.json注释决策节点让业务方看懂“为什么”单纯渲染树图还不够。tree_0.dot中的feature_12这种编号业务方根本不知所云。必须用feature_attribute.json做映射。notebook/认购产品_V1.3.ipynb的annotate_tree_nodes()函数干这事# notebook 中的辅助函数 def annotate_tree_nodes(dot_content: str, feature_map: dict) - str: # feature_map 来自 feature_attribute.json # key: feature index (e.g., 12), value: original name (e.g., age) lines dot_content.split(\n) annotated_lines [] for line in lines: if label in line and feature_ in line: # 提取 feature_12 中的数字 import re match re.search(rfeature_(\d), line) if match: idx int(match.group(1)) if idx len(feature_map): # 替换为业务名称 分箱区间 feat_name list(feature_map.keys())[idx] bin_info feature_map[feat_name].get(bins_info, ) new_label f{feat_name}\\n{bin_info} line line.replace(ffeature_{idx}, new_label) annotated_lines.append(line) return \n.join(annotated_lines) # 使用示例 with open(tree_0.dot, r) as f: dot_raw f.read() with open(tree_0_annotated.dot, w) as f: f.write(annotate_tree_nodes(dot_raw, feature_attribute))feature_attribute.json中age的bins_info: [32,47)就这样嵌入到节点标签里最终tree_0_annotated.png的节点显示为age [32,47)而不是晦涩的feature_12 39.5。5.3 构建“高意向客户画像”表格从树路径到业务规则tree_transform_bin.py的终极价值是把模型结论翻译成业务语言。notebook/认购产品_V1.3.ipynb的extract_top_paths()函数遍历前 5 条最高权重路径生成 Excel 表格路径编号年龄区间是否有房贷上月联系过预测概率支持客户数业务动作Path_1[32,47)YesYes0.921,247优先外呼推送结构性存款Path_2[47,58)NoNo0.87892邮件推送附利率优惠券Path_3[0,32)YesYes0.81653APP 推送限时 24 小时这张表直接交给营销总监他就能下令“下周外呼名单只筛 Path_1 和 Path_2 的客户”。这才是机器学习在银行落地的正确姿势——不是替代人而是把人的经验结晶成可复用、可追溯、可迭代的规则。从那以后我每次交付银行模型都强制走一遍tree_transform_bin.py的树可视化 feature_attribute.json注释 extract_top_paths()表格生成。哪怕客户说“我们只要概率”我也坚持把这三步做完——因为三个月后当营销效果不及预期时这张表就是唯一的归因依据。希望帮到你。本文还有配套的精品资源点击获取
