☰
Stacking模型融合实战:银行客户产品认购预测解析
2026/10/6 8:46:24 网站建设 项目流程

别人在调参炼丹的时候,我还在研究怎么把几个模型“组队打团”。今天想聊聊一个非常适合结构化数据竞赛和金融风控场景的进阶玩法:基于Stacking方法的银行客户产品认购预测。这个项目本质上是个二分类问题,但难点不在算法本身,而在于如何把多个模型的预测结果“拧成一股绳”,让最终的预测精度比任何单一模型都高。这篇文章会从模型选型、特征工程、交叉验证防止标签泄漏、到元模型设计,完整盘一遍我的实操思路和踩坑记录,希望能给你一点参考。

1. 内容整体设计与思路拆解

1.1 我为什么不用单个模型硬扛,而是选择Stacking方案

银行客户产品认购预测这个场景,我最早接触的时候,第一反应也是直接上XGBoost或者LightGBM。毕竟GBDT系列对付这种带大量离散特征的表格数据,效果一向很稳。但真正跑下来发现两个问题:第一个是单模型的上限很快就能看到,调参调到后期,每提升零点零零几个AUC都要付出巨大代价;第二个是客户认购行为本身包含的信号非常多元,有的客户偏好高频小额,有的偏好低频大额,还有相当一部分人属于“观望型”,单一模型的假设空间很难同时覆盖住这些不同形态的“用户画像”。

Stacking能解决什么问题?说白了就是让不同类型的模型各自表达“我看客户应该是这样”,然后把它们的判断结果作为新特征,再交给一个更聪明的模型去学“我该怎么结合大家的意见”。这个思想类似你做一个项目决策,不会只听一个专家的建议,而是把营销、运营、数据、财务几个岗位都叫到一个屋子里各说各的观点,然后你这个负责人再综合所有人的发言做最终拍板。Stacking就是把这个“专家会诊”的过程用算法实现了。

1.2 整体框架的四个模块

整个项目我拆成了四个模块:特征工程、基学习器训练、元特征生成、元模型融合。特征工程是地基,基学习器是“不同视角的专家团队”,元特征生成是“Experts 各自提交观点报告”,元模型融合是“最终决策委员会”。

特征工程方面,我构建了三类特征:基础客户画像特征(年龄、收入、职业状态)、行为特征(历史交易频次、平均金额、最近一次交易距今天数)、衍生组合特征(收入与交易金额的比值、产品偏好交叉项)。这里不做过的细节展开,但有一句话必须强调:Stacking对特征的敏感度非常高,基学习器如果全是同一个“信息源”训练出来的,融合效果会大打折扣。所以我在构造特征时专门让不同基模型使用不同侧重点的特征子集,人为制造“认知差异”。

基学习器我选了四个:XGBoost、LightGBM、CatBoost、RandomForest。这里有一个比较关键的经验:类别特征多的选CatBoost有天然优势,连续特征和缺失值模式复杂的选XGBoost和LightGBM更强,RandomForest可以起到“低方差基线”的作用。四个模型各有偏科,融合起来反而比四个“全才”效果更好。

2. 基学习器的选型逻辑与参数调优

2.1 不同基模型在银行认购场景下的“性格差异”

这四个模型在银行客户认购预测这件事上表现出的行为模式差异非常明显,我简单说下我在实际数据集上观察到的现象。

XGBoost对特征间的交互项捕捉能力很强。比如客户“年龄超过40岁且收入超过20万且已有两笔以上存款产品”这种组合型特征,XGBoost能很快挖掘出来。但它的缺点是训练时间长,尤其在数据量达到几十万条时,需要花较多时间调参数。

LightGBM的速度优势在这个场景下很实用。同样跑五折交叉验证,LightGBM比XGBoost能快三分之一左右,而且它对类别型特征和稀疏特征的处理很友好。银行数据里“职业类型”“婚姻状况”大家都懂,会被编码成十几个或几十个哑变量维度,LightGBM的直方图算法在这种高维稀疏特征下不会出现太多的性能退化。

CatBoost对类别特征的处理是“天生内置”。很多人在用其他GBDT模型前都要自己做LabelEncoder或者OneHotEncoder,总担心顺序编码会引入不存在的顺序关系。CatBoost直接在训练过程中处理类别特征,会自动计算类别间的目标统计信息并施加平滑处理来防止过拟合。客户认购数据里“地区”“职业”“学历”这些离散特征占比不低,CatBoost在这种场景下表现相当稳健。

RandomForest属于Bagging流派,它的特点是方差低,不容易对训练集的噪声产生过拟合。在Stacking框架里,它扮演的更像是一个“稳定型选手”,其它三个GBDT模型可能在某些局部分区上预测过头,RandomForest的输出可以起到很好的平衡作用。

2.2 防止标签泄漏的关键操作:K折交叉验证的OOF预测

这里我不妨细说下这段逻辑设计,因为这是我个人认为整个流程最关键的一步,也是新手最容易出问题的地方。

想象一下,如果你用全量训练集训练模型,再用这个模型去预测训练集本身,然后把预测结果当作元特征送给上层模型学习。看起来没毛病,实际上已经把部分答案“泄露”给了上层模型。上层模型看到元特征时,会直接把这些特征和标签的映射关系学进去,等真正预测测试集时,因为测试集的样本上的元特征分布和训练集不一致,最终结果往往虚高,上线后效果立刻打回原形。

标准做法是先把训练集分成K折(一般取5折),每一折里用其余K-1折的数据训练模型,对当前折的数据做预测,这个过程循环K次,最后得到每个训练样本一个“没见过我这个样本的模型”给出的预测值,这个预测值就是干净无泄漏的Out-of-Fold预测(简称OOF)。测试集也要做同样的K次预测,取平均得到测试集的元特征。

我最初自己手动实现这个逻辑时很容易为了化简代码而偷懒,结果就是元特征越训越漂亮,线下AUC能到0.95,线上/验证集一测直接打回原形。打过Kaggle或者参加过数据挖掘比赛的朋友应该都懂,线下验证指标如果高得离谱,先别高兴,回头检查是不是泄漏了。

2.3 参数调优的经验值参考

以LightGBM为例,我在这类银行数据集上常用的参数初始值供你参考:

import lightgbm as lgb params = { 'objective': 'binary', 'metric': 'auc', 'boosting_type': 'gbdt', 'learning_rate': 0.02, 'num_leaves': 31, 'max_depth': 7, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 5, 'min_data_in_leaf': 50, 'lambda_l1': 0.1, 'lambda_l2': 1.0, 'verbose': -1, 'seed': 42 }

这里我特别说明三个参数背后的考虑。learning_rate设置成0.02而不是默认的0.1,虽然会让训练轮数变多,但每棵树的贡献更小,降低单个模型过拟合的风险。Stacking框架内任何一个基学习器如果过拟合,它在OOF上产生的元特征就会“虚高”,进而拉低元模型的泛化能力。

feature_fraction和bagging_fraction两个都在0.8左右,这保证了每棵树只能看到80%的特征和80%的样本。银行客户数据往往存在大量相关特征(收入与资产、消费水平与信用卡额度等),随机抽样特征能让每个基学习器在更丰富的特征组合下学习,而不是每次都看到同一对高相关特征。

min_data_in_leaf设为50是一个很好的正则化手段。银行数据往往有噪音,尤其是一些历史行为特征存在缺失值,如果叶子节点样本太少,模型容易学进去个别异常客户的特殊模式。

XGBoost和CatBoost的参数设置也按类似逻辑,核心记住一条:在基学习器这一层,宁可欠拟合一点,也不要过于激进地拟合训练集。基学习器如果过拟合,产生的OOF预测值分布本身就趋向极端,0和1之间缺少中间地带,这样的元特征给到元模型,融合的增益几乎为零。

3. 实操过程与核心环节实现

3.1 数据准备与评估指标的选择

银行客户产品认购预测的数据集,我以葡萄牙银行机构的营销活动数据集为例(这是该领域非常经典的开源数据),特征是客户年龄、工作类型、婚姻状况、教育背景、余额、住房贷款、个人贷款、上次联系时长、过往营销接触次数、上次联系距离当前天数、之前的接触次数等,加上社会经济背景指标,目标变量是客户是否认购了定期存款。

样本量大约4万多条,一条关键的筛选逻辑要提前想清楚:这个数据集只在“有联系记录的客户”里做了标记,没被联系过的客户严格来说不构成“未认购”样本。建模前要把这个抽样逻辑讲清楚,否则后续评估指标会被干扰。

评估指标我选了AUC、LogLoss和KS值三个一起看。AUC衡量整体区分度,LogLoss则更侧重概率预测的校准度,而银行场景很关心KS值因为涉及人群排序和分层策略。不建议只看AUC,因为AUC对类别不平衡不敏感,而银行客户认购正样本占比往往只有百分之十几或者更低,AUC高不代表“预测为正的概率值”一定准,这对后续业务上圈选客户名单非常重要。

3.2 特征工程的核心处理细节

在真实项目里,数据预处理永远比调参重要。这块我吃过亏,说几个容易踩坑的细节。

缺失值处理要分类型。连续型特征像收入和交易金额用中位数填充,并用一个“是否缺失”的0/1标志位记录下来。类别型特征缺失则单独填一个“unknown”类别,不要盲目用众数填充——这在银行场景里很危险,因为“未知”本身往往意味着客户属性发生了改变或渠道信息断裂,包含对认购行为有预测力的信息。

日期型特征全部转成“距今多少天”而不是保留年月日。比如“上次联系日期”,在原始数据里是日期格式,实际建模必须转成“距离数据集截止日期多少天”。这样既能反映时效性,又能避免模型学习到跟具体年份有关的虚假关联。

特征归一化要不要做?分模型。RandomForest就不需要,树模型对特征的单调变换不敏感,做不做归一化不影响分裂点查找。如果你额外引入逻辑回归、或者元模型选择线性模型,归一化就必须要做。所以我在送入各基模型前,做了两套特征版本:一套原值直接给树模型,一套标准化后给线性模型或MLP。这个细节经常被忽略,但对融合效果有一定影响。

3.3 Stacking训练流程的完整代码逻辑

下面这段代码是整个Stacking训练流程的核心骨架,我用了完整注释,方便你直接参考和复现。

import numpy as np import pandas as pd from sklearn.model_selection import StratifiedKFold from sklearn.metrics import roc_auc_score, log_loss from xgboost import XGBClassifier from lightgbm import LGBMClassifier from catboost import CatBoostClassifier from sklearn.ensemble import RandomForestClassifier N_FOLDS = 5 SEED = 42 def stacking_oof_predict(model, X_train, y_train, X_test): skf = StratifiedKFold(n_splits=N_FOLDS, shuffle=True, random_state=SEED) oof_train = np.zeros((X_train.shape[0],)) oof_test = np.zeros((X_test.shape[0],)) for fold_idx, (trn_idx, val_idx) in enumerate(skf.split(X_train, y_train)): X_tr, y_tr = X_train.iloc[trn_idx], y_train.iloc[trn_idx] X_va, y_va = X_train.iloc[val_idx], y_train.iloc[val_idx] model_clone = model.__class__(**model.get_params(deep=False)) model_clone.fit(X_tr, y_tr) oof_train[val_idx] = model_clone.predict_proba(X_va)[:, 1] oof_test += model_clone.predict_proba(X_test)[:, 1] / N_FOLDS oof_auc = roc_auc_score(y_train, oof_train) print(f'{model.__class__.__name__} OOF AUC: {oof_auc:.5f}') return oof_train, oof_test # 假设已经完成了特征工程,得到以下变量 # X_train: 训练集特征 DataFrame # y_train: 训练集标签 Series # X_test: 测试集特征 DataFrame xgb_model = XGBClassifier(n_estimators=300, learning_rate=0.02, max_depth=6, subsample=0.8, colsample_bytree=0.8, reg_lambda=1.0, random_state=SEED, eval_metric='auc', use_label_encoder=False) lgb_model = LGBMClassifier(n_estimators=1000, learning_rate=0.02, num_leaves=31, max_depth=7, feature_fraction=0.8, bagging_fraction=0.8, bagging_freq=5, min_data_in_leaf=50, reg_alpha=0.1, reg_lambda=1.0, random_state=SEED) cat_model = CatBoostClassifier(iterations=800, learning_rate=0.03, depth=6, l2_leaf_reg=3.0, random_seed=SEED, verbose=0) rf_model = RandomForestClassifier(n_estimators=600, max_depth=12, min_samples_leaf=20, max_features=0.6, random_state=SEED, n_jobs=-1) models = [xgb_model, lgb_model, cat_model, rf_model] train_meta = np.zeros((X_train.shape[0], len(models))) test_meta = np.zeros((X_test.shape[0], len(models))) for i, model in enumerate(models): train_meta[:, i], test_meta[:, i] = stacking_oof_predict(model, X_train, y_train, X_test) # 保存元特征,方便后续调试元模型 np.save('train_meta.npy', train_meta) np.save('test_meta.npy', test_meta) np.save('y_train.npy', y_train.values)

3.4 元模型的选择与逻辑回归的意外优势

Stacking的最后一层元模型,我最终选择了带L1正则化的逻辑回归。这是我在几次对比实验后确定下来的方案。

很多做Stacking的人喜欢直接用LightGBM作为元模型,理由很简单——效果好。但问题在于这相当于“让一个专家去综合另一个专家的意见”,虽然灵活,但过度求强往往适得其反。逻辑回归作为元模型有几个明显的好处。第一,模型简单,不容易学过头。第二,输入特征只有四个(四个基模型的预测概率),维度极低,不需要复杂的非线性变换。第三,可解释性强,通过权重系数能直观看到每个基模型对最终预测的贡献度,这在银行场景里做模型风控审计非常有价值。

我训练逻辑回归时还额外加了一个操作——对元特征做标准化。虽然在大多数场景下逻辑回归对特征尺度不敏感,但加了L1正则化后,特征尺度会影响正则化惩罚的量级,标准化后更公平。我跑完归一化和不归一化的对照实验,归一化后AUC提升了0.003左右,幅度不大但属于白捡的收益。

from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline meta_model = make_pipeline( StandardScaler(), LogisticRegression(penalty='l1', solver='liblinear', C=1.0, random_state=SEED) ) meta_model.fit(train_meta, y_train) test_pred = meta_model.predict_proba(test_meta)[:, 1] # 打印元模型权重,观察各基模型的贡献度 lr_step = meta_model.named_steps['logisticregression'] for idx, weight in enumerate(lr_step.coef_[0]): print(f'Model {idx} ({models[idx].__class__.__name__}) weight: {weight:.4f}')

这里有一个比较有意思的结果:四个基模型的OOF AUC相差不大,XGBoost大约0.86,LightGBM约0.87,CatBoost约0.86,RandomForest略低约0.84。但逻辑回归学习到的权重并不是严格跟各自的OOF AUC成比例,CatBoost的权重反而比LightGBM还要高一点。原因是CatBoost和LightGBM之间的预测相关性更低,而XGBoost和LightGBM之间的相关性更高。所以Stacking选基学习器,差异性比单个模型精度更重要。这一点挺关键的,如果四个模型预测结果高度相关,那融合只是对同一个预测分布的微小扰动,全部用高分模型反而带来不了多样性的增益。

4. 常见问题与排查技巧实录

4.1 元特征一旦出现“完美线性可分”就要小心

有一次调参时我把基学习器的learning_rate从0.02改成0.1,同时把n_estimators调得很高,结果OOF AUC一路冲到了0.92。当时我还挺兴奋,但紧接着发现测试集预测成绩根本没涨,甚至有小幅下跌。

复盘时用混淆矩阵分析,发现元特征的分布出现了明显的“两边极端中间空心”形态。所有训练样本的预测概率都集中分布在0.1以下或者0.9以上,中间0.3到0.7区间几乎没人。这就是典型的基学习器过拟合后的表现——它面对任何样本都“信心爆棚”,但其实OOF预测是多个折的平均,出现这种分布说明每个单折都严重过拟合了当前训练折的内部噪声。

标准解法就是:回退基模型的复杂度,增加正则强度,比如加大min_data_in_leaf和reg_lambda,调低learning_rate并同步减少训练轮数。另外还可以对元特征做“截断”——把概率小于0.05的按0.05处理,大于0.95的按0.95处理,虽然这会损失一点极端信息,但对逻辑回归层的稳定性有帮助。

4.2 类别不平衡问题要不要用重采样或class_weight

银行客户认购数据正样本占比如果只有10%~15%,直接用原始比例喂给模型,模型的输出概率会偏向多数类。但在这个场景里,我实际验证后发现:不要在Stacking基学习器层使用过采样或欠采样。

原因是这样:过采样会增加正样本的重复,极易导致基模型对少数类过拟合,OOF预测出来的正样本概率虚高;欠采样会丢失大量负样本信息,而银行客户数据里负样本的信息量并不低——不认购的客户里面也分“完全不来往型”和“高潜力观望型”,扔掉负样本等于扔掉了这两类人之间的区别。

正确的做法是把类别不平衡问题交给“阈值调整”环节。模型输出的概率本身不需要做校正,业务侧的目标是在保证覆盖率的前提下提升响应率。所以我在训练时直接让class_weight保持默认均衡权重,训练结束后用验证集找使F1最大化的阈值。这一招比任何采样技巧都直接管用。

4.3 重复跑Stacking时OOF结果波动怎么办

Stacking训练过程中有个很尴尬的环节——训练一次要跑很久,如果中间发现某个基模型参数调得不合适需要重来,整个链路都要跟着重新跑。遇到这个问题时,我后来是这样处理的:

把元特征保存成文件(就像代码里展示的),每一个基模型训练完立刻单独保存一份OOF结果和测试集预测。这样后面调试元模型时不需要重新训练基学习器,大大节省验证时间。毕竟基学习器跑一次五折交叉验证,大概要十五到二十分钟,等待过程中配合这个保存机制,整体调试效率能提升不少。

不过这招有个前提:基学习器的参数一旦确定,不要轻易改动。如果你回头改了LightGBM的num_leaves,那它产生的OOF元特征变了,整条链路都得重跑,不可能只更新单列特征。

4.4 元模型要不要加入原始特征

这是一个曾经争论不休的问题,站在实际项目立场我给一个可以直接用的结论:如果基学习器数量很少(两三个),可以尝试把原始特征中最重要的前20个拼接到元特征后面,再喂给元模型。这相当于给最后一层的决策委员会补充上下文背景信息,而不是只听几个专家的二手结论。

但如果你已经有四个甚至更多基学习器,我建议不要加原始特征。因为元特征维度够大,再塞入原始特征会让元模型过于复杂,而且原始特征经过基学习器之后的信息已经高度压缩,再重复加入只会增大过拟合风险而不增加新信息。我在自己的实验里也验证过,四个基学习器基础训练下,加入原始特征后AUC反而下跌了约0.005。

5. 效果评估与业务场景的落地接入

5.1 线下评估的完整维度:不只是AUC

最终模型在这个银行数据集上得到的线下评估结果折合下来AUC大约在0.89到0.91区间,相比最佳单模型LightGBM提升了约0.02到0.03。但AUC只是其中一个维度,还有很多值得关注的指标。

KS值从单模型的约0.62提升到了0.66,说明正负样本群体的概率分布分离度更高,这直接决定了按预测概率排序后,在客户名单的“TOP 10%”区间里能圈到多少实际认购客户。实际业务中,营销触达成本是固定的,模型要帮助决策的是“按照概率排序后应该触达到前百分之多少”,KS值高意味着同样触达量下能圈中更多有效客户。

LogLoss从单模型的0.42降到了0.38左右。这个指标在银行风控场景里非常实用,因为后续如果要做预期损失计算或者利润优化,需要的不是排名而是校准良好的概率值。如果单个样本的预测概率从0.7变成0.8,对排序影响不大,但对于预计收入计算来说绝对是天壤之别。

5.2 银行场景下的模型落地:从预测到行动

模型做完之后与业务侧的衔接是一道非常需要经验处理的工作。我习惯把预测结果分成三档:高价值客户(预测概率前10%)、中价值客户(前10%~30%)、低潜力客户(其余70%)。对高价值客户建议客户经理一对一电话沟通;对中价值客户推送短信加App弹窗;低潜力客户保持常态触达即可。三档策略后模型的营销响应率提升在2到3倍区间,这是银行侧业务打标时经常看到的结果。

这里有个小教训:不要只给业务侧输出一个概率值,一定要同时输出预测背后最关键的三四个特征贡献项。比如某个客户预测概率很高,模型认为他收入高且持有理财产品超过两年且距离上次交易不超过七天。业务同事拿到这些特征就能针对性设计话术,而不是提供一个无法解释的黑盒分数。

5.3 上线监控与定期重训机制

银行数据有一个特点:客户行为随着市场环境和产品供给的变化而漂移。季度性理财产品节奏、利率调整、节假日红包活动等都会导致模型特征分布的变动。所以模型上线后至少每周监控三件事:特征的均值漂移(PSI,群体稳定性指数)、每天的预测概率分布、以及每周的实际响应率。

另外,在重训频率上,建议月度重训一次基学习器。这主要是为了让基模型学习最新几个月的客户行为特征,同时元模型也可以同步更新。注意不要每天重训,否则模型会对短期噪音过度响应,容易频繁波动。如果出现系统性事件,比如银行结构调整、利率体系变动导致所有客户行为模式短期扭转,要人工介入评估判断是否需要提前重训。

最后分享一个小技巧

Stacking在银行客户产品认购预测这种中等规模表格数据上,效果提升相当明确。但整个流程中最容易被人忽视的是验证逻辑的一致性。我在项目中发现不少人只检查了OOF的AUC,却没有检查测试集预测的分布是否合理——比如测试集预测均值如果和历史正样本率差距过大,往往说明训练测试分布已经发生了较大漂移,这时所有线下验证指标都不可靠。

另外,如果你打算在类似场景下复现这个方案,我建议优先保证特征工程质量,其次确保交叉验证没有泄漏,最后才去调Stacking的层数和模型组合。顺序搞反了的话,常常会出现模型怎么调都原地踏步的情况,那通常不是融合方法的问题,而是上游数据的问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询