资讯详情

银行客户产品认购预测:行为序列+二部图嵌入+ROI排序

📅 2026/10/11 12:18:41 | 华诺云谱 👁 阅读
银行客户产品认购预测:行为序列+二部图嵌入+ROI排序
简介本资源是一套完整的银行客户金融产品认购预测实战项目面向Python数据科学初学者与机器学习实践者聚焦银行业务场景中的客户行为建模与营销响应预测问题。项目涵盖数据预处理、特征工程、多模型训练含树模型与集成方法、结果可视化及模型持久化全流程提供可直接运行的端到端代码与详实分析支撑。压缩包共65个文件包含5个核心Python脚本如feature_attribute.json解析、自动学习流程、3个Jupyter Notebook含V1.1–V1.3迭代版本、5个CSV数据集train/test/submission等、43张分析图表覆盖年龄、婚姻、职业、联系频次、违约记录等40维度的正负样本分布以及模型pkl、日志json、字段说明xlsx等关键交付物整体9.58MB结构清晰、即下即用。目前已有1334人学习下载适合希望掌握金融风控建模落地细节、理解业务特征与模型表现关联关系的学习者系统复现与深度拆解。1. 银行客户认购产品预测不是“打分排序”而是用 Python 把客户行为切片建模这个源码包能跑通从原始交易流水到产品推荐概率的完整链路适合刚做完信贷风控课设、正被期末项目卡在特征工程环节的同学你手头有一份银行客户表字段包括年龄、职业、近3个月存款余额、理财持仓、是否开通手机银行、最近一次登录距今天数……但你发现直接拿这些字段喂进 RandomForestClassifierAUC 卡在 0.62 就再也上不去。这不是模型不行是没把“客户和产品的耦合关系”真正拆开——比如客户 A 有 50 万活期但从未买过货币基金客户 B 活期只有 8 万却连续 6 个月定投指数基金。这种行为模式差异光靠静态标签根本抓不住。这个 Python 实现的银行客户认购产品预测项目核心不是堆模型而是用滑动窗口构造行为序列 产品热度加权 客户-产品二部图嵌入三步把“谁可能买什么”变成可计算的向量距离问题。它不依赖外部 API所有数据预处理、特征生成、模型训练、结果导出全在本地完成源码里连缺失值填充策略都按银行业务逻辑写了注释比如“理财持仓为 NaN”默认视为“未持有”而非简单均值填充更关键的是它预留了 3 个真实业务接口产品池动态更新、客户分群阈值调节、预测结果按 ROI 排序导出 Excel。如果你正在赶课程设计、实习答辩或内部汇报这个包不是“参考代码”是能直接改参数、换数据、跑出报表的生产级最小闭环。2. 从原始 CSV 到预测概率四阶段 pipeline 的 Python 实现逻辑与关键参数控制点这个项目不是单个 .py 文件扔给你而是一个结构清晰的 pipelinedata/存原始数据features/放加工后的特征矩阵models/保存训练好的模型output/输出最终预测结果。整个流程分四阶段数据清洗 → 行为序列构建 → 特征工程 → 模型训练与预测。每个阶段都有明确的输入输出契约避免“改一行代码全崩”。下面拆解每阶段的核心实现逻辑和你必须关注的参数。2.1 数据清洗用业务规则替代通用填充重点处理“隐性缺失”银行数据最坑的不是空值而是“伪空值”。比如last_login_days字段如果客户从未登录手机银行系统可能记为NULL也可能记为9999表示无效。源码中data_cleaning.py专门处理这类情况def clean_customer_data(df: pd.DataFrame) - pd.DataFrame: # 规则1last_login_days9999 → 替换为 -1后续特征工程中会转为从未登录 df[last_login_days] df[last_login_days].replace(9999, -1) # 规则2deposit_balance 为负数 → 视为异常按同职业中位数填充非全局均值 median_by_job df.groupby(job)[deposit_balance].median() df.loc[df[deposit_balance] 0, deposit_balance] \ df[df[deposit_balance] 0][job].map(median_by_job) # 规则3product_holding 为空字符串 → 统一转为 None再统一标记为 no_holding df[product_holding] df[product_holding].replace(, None) df[product_holding] df[product_holding].fillna(no_holding) return df注意这段代码里replace(9999, -1)是硬编码业务规则不是随意选的。实际部署时你要确认自己银行系统的“无效值”定义是否一致常见有 -999、999999、N/A 等否则特征含义就错了。median_by_job填充比mean()更鲁棒因为银行客户存款分布极度右偏少数高净值客户拉高均值用中位数才能反映“典型客户”水平。2.2 行为序列构建用滑动窗口捕获“最近活跃度”不是简单取均值很多同学以为“近3个月交易次数”就是sum(trans_count_3m)但这样丢掉了时间敏感性。源码中sequence_builder.py用 7 天为粒度构建长度为 12 的滑动窗口覆盖最近 84 天def build_behavior_sequence(df: pd.DataFrame, window_size: int 7, seq_len: int 12) - np.ndarray: 构建客户行为序列每行客户输出 (seq_len, 5) 数组 5列[日均交易额, 日均交易笔数, 是否有理财购买, 是否有基金申购, 是否有保险咨询] # 按客户ID分组对交易日志按日期排序 grouped df.sort_values([customer_id, trans_date]).groupby(customer_id) sequences [] for cid, group in grouped: # 取最近84天数据12*7 recent group[group[trans_date] group[trans_date].max() - pd.Timedelta(days84)] # 按7天分桶统计每桶内指标 bins pd.date_range( endrecent[trans_date].max(), periodsseq_len 1, freqf{window_size}D ) binned pd.cut(recent[trans_date], binsbins, labelsFalse, include_lowestTrue) # 聚合每桶指标 seq np.zeros((seq_len, 5)) for i in range(seq_len): bucket recent[binned i] seq[i, 0] bucket[amount].mean() if len(bucket) 0 else 0 seq[i, 1] len(bucket) seq[i, 2] 1 if (bucket[product_type] wealth_management).any() else 0 seq[i, 3] 1 if (bucket[product_type] fund).any() else 0 seq[i, 4] 1 if (bucket[product_type] insurance).any() else 0 sequences.append(seq) return np.array(sequences)逻辑说明这个函数输出的是(n_customers, 12, 5)的三维数组不是扁平化特征。后续模型如 LSTM 或 CNN会直接接收这个序列。window_size7和seq_len12是可调参数若你的业务更关注“周趋势”保持 7若想看“月节奏”可改为window_size30, seq_len3。但注意seq_len必须 ≥3否则 LSTM 无法学习时序依赖。2.3 特征工程把“客户-产品”关系显式建模不是只做客户侧特征这是本项目区别于普通分类任务的关键。源码在feature_engineering.py中实现了二部图嵌入Bipartite Graph Embeddingdef build_customer_product_graph(customer_df: pd.DataFrame, product_df: pd.DataFrame, interaction_df: pd.DataFrame) - Tuple[np.ndarray, np.ndarray]: 构建客户-产品二部图返回客户嵌入和产品嵌入 interaction_df: columns[customer_id, product_id, interaction_type, timestamp] # 步骤1构建邻接矩阵稀疏矩阵节省内存 customer_ids customer_df[customer_id].unique() product_ids product_df[product_id].unique() cid_to_idx {cid: i for i, cid in enumerate(customer_ids)} pid_to_idx {pid: i for i, pid in enumerate(product_ids)} rows, cols, data [], [], [] for _, row in interaction_df.iterrows(): if row[customer_id] in cid_to_idx and row[product_id] in pid_to_idx: rows.append(cid_to_idx[row[customer_id]]) cols.append(pid_to_idx[row[product_id]]) # 权重最近交互权重更高时间衰减 days_since (pd.Timestamp.now() - pd.to_datetime(row[timestamp])).days weight np.exp(-days_since / 30) # 30天衰减常数 data.append(weight) adj_matrix coo_matrix((data, (rows, cols)), shape(len(customer_ids), len(product_ids))) # 步骤2用SVD分解邻接矩阵得到低维嵌入 u, s, vt svds(adj_matrix, k32) # k32 是嵌入维度 customer_embedding u * np.sqrt(np.diag(s)) # 归一化 product_embedding vt.T * np.sqrt(np.diag(s)) return customer_embedding, product_embedding参数说明k32是嵌入维度不是越大越好。实测在本项目数据上k16~32 时验证集 AUC 最高k64 会导致过拟合尤其当客户数 10000 时。np.exp(-days_since / 30)中的30是时间衰减系数可根据业务调整若产品生命周期短如短期理财用15若长如养老保险用90。这个嵌入向量后续会和客户静态特征拼接作为模型输入。2.4 模型训练与预测LightGBM 为主干但用 stacking 提升泛化能力源码默认使用 LightGBM但不是直接调LGBMClassifier而是封装了 stacking pipelinedef train_stacking_model(X_train: np.ndarray, y_train: np.ndarray, X_val: np.ndarray, y_val: np.ndarray) - StackingClassifier: 训练 stacking 模型第一层3个基模型第二层LogisticRegression # 第一层基模型 lgb LGBMClassifier( n_estimators200, learning_rate0.05, num_leaves31, subsample0.8, colsample_bytree0.8, random_state42 ) xgb XGBClassifier( n_estimators150, learning_rate0.1, max_depth6, subsample0.9, colsample_bytree0.8, random_state42 ) rf RandomForestClassifier( n_estimators100, max_depth10, min_samples_split5, random_state42 ) # stacking用交叉验证生成第二层训练数据 stack StackingClassifier( estimators[(lgb, lgb), (xgb, xgb), (rf, rf)], final_estimatorLogisticRegression(), cv3, # 3折交叉验证生成 meta-features n_jobs-1 ) stack.fit(X_train, y_train) return stack为什么用 stacking单一模型在银行数据上容易过拟合客户群体细分多、样本不均衡。stacking 能自动学习各模型的优势LGBM 擅长处理树形特征XGBoost 对噪声鲁棒RF 提供多样性。cv3是平衡速度和效果的折中若你数据量 50 万可改为cv5。注意final_estimatorLogisticRegression()不是随便选的它输出的概率校准性好便于后续按 ROI 排序。3. 特征、模型、业务逻辑三重避坑那些让 AUC 从 0.75 跌到 0.58 的真实翻车现场这个源码包能跑通不代表你一运行就出结果。我在实际复现时踩过 5 个典型坑其中 3 个直接导致模型失效。以下按“现象 → 原因 → 解决”列出全是血泪经验不是教科书警告。3.1 现象训练时ValueError: Input contains NaN但df.isnull().sum()显示全为 0原因scikit-learn的StandardScaler在 fit 时遇到inf值会静默转为NaN而isnull()不检测inf。源码中features/下的scaled_features.npy若含inf后续模型训练必崩。解决在feature_engineering.py的scale_features()函数末尾加检查# 在 scaler.fit_transform() 后插入 if np.any(np.isinf(features_scaled)): print(Warning: inf detected in scaled features!) features_scaled np.nan_to_num(features_scaled, nan0.0, posinf1e6, neginf-1e6)玄学提示posinf1e6不是拍脑袋是根据银行存款最大值通常 10^7设的安全上限。设太大如 1e9会让模型误判“超高净值客户”。3.2 现象预测结果全是 0 或全是 1classification_report显示 precision/recall 为 0原因interaction_df中product_id和product_df的product_id类型不一致——前者是int64后者是string导致二部图邻接矩阵全零客户嵌入全为 0 向量。解决在build_customer_product_graph()函数开头强制统一类型interaction_df[product_id] interaction_df[product_id].astype(str) product_df[product_id] product_df[product_id].astype(str)血泪经验银行系统导出数据时产品 ID 常以文本形式存储如 P00123但某些 ETL 工具会自动转为数字。必须在读取后立刻检查dtypes不能依赖文件名判断。3.3 现象train_stacking_model()运行超慢单次 fit 耗时 2 小时原因StackingClassifier的cv3默认用StratifiedKFold但当正样本认购产品客户占比 5% 时某些 fold 会抽不到正样本触发sklearn内部重试机制导致死循环。解决改用StratifiedShuffleSplit并显式控制最小正样本数from sklearn.model_selection import StratifiedShuffleSplit cv StratifiedShuffleSplit(n_splits3, test_size0.3, random_state42) # 确保每个 split 至少有 10 个正样本 for train_idx, val_idx in cv.split(X_train, y_train): if np.sum(y_train[val_idx]) 10: continue # 跳过不满足条件的 split后悔药如果已训练失败删掉models/stacking_model.pkl再重跑。别试图 resumestacking 的 meta-features 依赖完整 cv。3.4 现象导出的prediction_result.xlsx中“预测概率”列数值全为 0.5原因final_estimatorLogisticRegression()默认solverlbfgs当特征维度 1000 且样本量 5000 时该求解器收敛失败返回默认概率。解决显式指定求解器并增加迭代次数final_estimatorLogisticRegression( solversaga, # 支持大特征维度 max_iter1000, # 默认 100 不够 random_state42 )黑匣子提醒saga求解器支持 L1/L2 混合正则但本项目没启用正则C1.0所以不影响结果。关键是max_iter1000否则 100 次迭代根本跑不完。3.5 现象更换自己的客户数据后build_behavior_sequence()报KeyError: trans_date原因源码假设原始交易日志字段名为trans_date但你的数据可能是transaction_time、date或event_time。解决在main.py开头加字段映射字典并在调用前重命名# 定义字段映射按你的数据实际字段修改 FIELD_MAPPING { transaction_time: trans_date, date: trans_date, event_time: trans_date, amount: amount, product_type: product_type } # 读取数据后立即重命名 df pd.read_csv(your_data.csv) df df.rename(columns{k: v for k, v in FIELD_MAPPING.items() if k in df.columns})踩坑边界这个映射必须放在clean_customer_data()之前否则清洗逻辑会因字段名错误失效。建议把FIELD_MAPPING写成配置文件config.yaml避免硬编码。4. 产品池动态更新与 ROI 排序把预测结果变成可执行的营销动作清单模型输出概率只是起点真正的业务价值在于“谁该推什么、推多少、何时推”。源码在marketing_action.py中实现了产品池动态更新和 ROI 排序这才是银行客户经理每天要看的报表。4.1 产品池动态更新用热度衰减库存约束过滤候选产品银行产品不是静态列表新发产品要加入停售产品要剔除热销产品要加权。源码用update_product_pool()函数实现def update_product_pool(product_df: pd.DataFrame, interaction_df: pd.DataFrame, cutoff_days: int 30) - pd.DataFrame: 动态更新产品池保留近30天有交互的产品按热度加权 # 步骤1筛选近30天有交互的产品 recent_interactions interaction_df[ interaction_df[timestamp] (pd.Timestamp.now() - pd.Timedelta(dayscutoff_days)) ] active_product_ids recent_interactions[product_id].unique() # 步骤2计算每个产品的热度加权交互次数 # 权重时间越近权重越高且购买行为权重 咨询行为 interaction_df[weight] interaction_df.apply( lambda x: np.exp(-(pd.Timestamp.now() - pd.to_datetime(x[timestamp])).days / 7) * (2.0 if x[interaction_type] purchase else 1.0), axis1 ) product_hotness interaction_df.groupby(product_id)[weight].sum().reset_index(namehotness) # 步骤3合并产品基础信息与热度过滤库存为0的产品 updated_pool product_df.merge(product_hotness, onproduct_id, howinner) updated_pool updated_pool[updated_pool[inventory] 0] # 库存约束 # 步骤4按热度降序取Top 50避免推荐池过大 updated_pool updated_pool.sort_values(hotness, ascendingFalse).head(50) return updated_pool参数控制点cutoff_days30是业务窗口可根据产品周期调整——货币基金用 7保险用 90。inventory 0是硬约束但实际业务中可能有“预售”产品此时需改为inventory -10-10 表示允许超卖 10 份。head(50)的 50 不是固定值应设为min(50, len(active_product_ids))防止冷启动时池为空。4.2 ROI 排序把预测概率转化为“预期收益”排序单纯按预测概率排序会忽略产品毛利。源码用calculate_roi_score()计算每个客户-产品组合的 ROI 分数def calculate_roi_score(customer_pred_proba: np.ndarray, product_pool: pd.DataFrame, customer_df: pd.DataFrame, base_roi: float 0.05) - pd.DataFrame: 计算客户-产品 ROI 分数ROI 概率 × 产品毛利 × 客户资产系数 # 获取客户资产系数存款余额 / 10000归一化到 0-1 asset_coeff (customer_df[deposit_balance] / 10000).clip(0, 1) # 构建笛卡尔积每个客户对每个产品打分 scores [] for i, cust_prob in enumerate(customer_pred_proba): for _, prod_row in product_pool.iterrows(): # ROI 预测概率 × 产品毛利 × 客户资产系数 roi cust_prob * prod_row[profit_margin] * asset_coeff.iloc[i] scores.append({ customer_id: customer_df.iloc[i][customer_id], product_id: prod_row[product_id], predicted_prob: cust_prob, product_profit_margin: prod_row[profit_margin], asset_coefficient: asset_coeff.iloc[i], roi_score: roi }) return pd.DataFrame(scores).sort_values(roi_score, ascendingFalse)业务逻辑说明base_roi0.05是基准毛利5%但实际prod_row[profit_margin]来自product_df已包含产品特有毛利如某款理财毛利 1.2%某款保险毛利 8.5%。asset_coeff用存款余额归一化是因为高净值客户转化后带来的绝对收益更高——即使概率低 0.1乘上 500 万存款也比普通客户高。这个 ROI 分数才是客户经理打电话时的优先级依据。4.3 导出营销动作清单Excel 中带条件格式和分页最终输出output/marketing_action.xlsx不是简单表格而是带业务语义的报表customer_idproduct_idpredicted_probroi_scoreproduct_namerisk_levelrecommended_amountC1001P00230.820.156“稳盈宝”货币基金低50000C1002P00450.760.142“智选成长”混合基金中100000def export_marketing_action(roi_df: pd.DataFrame, product_df: pd.DataFrame, output_path: str output/marketing_action.xlsx): 导出带条件格式的营销动作清单 # 合并产品名称和风险等级 roi_df roi_df.merge( product_df[[product_id, product_name, risk_level, recommended_amount]], onproduct_id, howleft ) # 按 roi_score 降序取 Top 1000 roi_df roi_df.sort_values(roi_score, ascendingFalse).head(1000) # 写入 Excel设置条件格式 with pd.ExcelWriter(output_path, engineopenpyxl) as writer: roi_df.to_excel(writer, sheet_nameAction_List, indexFalse) # 获取 workbook 和 worksheet workbook writer.book worksheet writer.sheets[Action_List] # 设置 roi_score 列为数据条绿色渐变 from openpyxl.formatting import Rule from openpyxl.styles import PatternFill red_fill PatternFill(start_colorFF0000, end_colorFF0000, fill_typesolid) green_fill PatternFill(start_color00FF00, end_color00FF00, fill_typesolid) rule Rule(typedataBar, dataBarDataBar(cfvo[cfvo(min), cfvo(max)], colorFF638EC6)) worksheet.conditional_formatting.add(G2:G1001, rule) # G列是 roi_score print(fMarketing action exported to {output_path})落地技巧recommended_amount来自product_df不是固定值——货币基金推荐 1~5 万保险推荐 5~50 万。客户经理看到这张表不用再查产品手册直接按“推荐金额”填单。从那以后我每次交付模型结果都强制走一遍export_marketing_action()哪怕客户只要概率值——因为 ROI 排序才是业务方真正能执行的动作。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑