银行客户产品认购预测:我用Stacking把响应率翻了一倍(R语言完整实现)
朋友在网点做理财经理,总行推了一款定期存款产品,分给他一条几万人的存量客户名单,让他挨个打电话。他打了三天,嗓子哑了,响应率不到5%。这其实是零售银行营销里最让人头疼的事:产品是好产品,名单也是合规引入的名单,但人力就这么多,先给谁打、不先给谁打,直接决定了这场营销的ROI。
我是做数据分析的,当时就跟他聊:与其让客户经理凭经验和运气打电话,不如让模型先给客户排个序,模型认为最有可能认购的人排在最前面。这个项目后来我落地为一次完整的建模实践——用银行公开的营销数据集,预测客户是否会认购定期存款产品,模型层面没有用最简单的逻辑回归,而是选择了集成学习里的Stacking方法,全程在R语言里实现。这篇文章就是这次实践的完整复盘,包含数据预处理的细节、Stacking的原理拆解、元特征生成层面对抗数据泄漏的完整链路,以及几段可以直接改用的R代码。适合正在接触集成学习、或者想用机器学习给业务方输出营销名单的读者参考。
1. 业务问题与建模目标:为什么银行需要"给客户排序"的能力
1.1 营销困境的本质是资源错配
银行的存量客户成百上千万,但客户经理的人数是固定的。一次产品营销,尤其是电话营销,拨打量有上限,因为一个电话平均要聊三到五分钟,能打的电话数量天然受限。传统做法是什么?按客户资产规模排序,钱多的先打。这个逻辑不能说错,但存在明显盲区:有钱不代表有意愿。一位资产千万的客户可能早被各种电话骚扰到反感,而一位存款只有几万块的年轻白领,可能正攒着首付,对定存产品兴趣很高。
所以,营销名单的排序,本质上是回答一个问题:在相同资产条件下,哪些客户当前对这个产品的潜在响应概率更高?这就是客户产品认购预测要做的事——给每个客户算一个"认购倾向分",再按分数降序分配营销资源。
1.2 建模目标不是"追求准确率",而是"提升排序质量"
这里要特别强调一个视角问题。很多入门者拿到这种二分类任务,第一反应是追求准确率(Accuracy),但在这个业务场景里,准确率是个陷阱。
原因很简单:正样本占比太低。这类电话营销数据里,实际认购的客户通常只占约10%上下。如果模型把所有客户都预测成"不认购",准确率能做到90%,但这对业务毫无价值,因为你等于一个电话都不用打。所以,在营销响应这种正负样本极不平衡的任务里,我们更关注的是模型能不能把"真正会认购的那批人"排到名单前面去。
业内衡量这个能力的标准一般是两个:
- AUC(ROC曲线下面积):衡量模型把正样本排在负样本前面的能力,不受阈值影响。
- KS值:衡量模型区分正负样本的最大差距,银行风控和营销模型里非常常见的指标。
我这次建模过程中,给业务方汇报时也从不说"模型准确率94%",而是说"如果只给前20%的客户打电话,我们能抓到全量响应客户中大约40%~50%的人"。这句话业务方一听就懂,因为直接对应着人力投入和营销产出。
1.3 数据集说明与关键字段
项目用的是UCI上的经典银行营销数据集,对应的是一个葡萄牙银行机构的电话营销活动记录,一共45211条样本,17个输入变量,目标变量是客户是否认购了定期存款。样本里正样本5289个,占比约11.7%。这个数据在学术和工业实践里都很常见,拿来做Stacking练习和复现都比较方便,不用自己费劲清洗脏数据。
关键字段大概分成几类:
| 字段类型 | 字段 | 业务含义 |
|---|---|---|
| 客户基本信息 | age, job, marital, education | 年龄、职业、婚姻、教育程度 |
| 财务状态 | default, balance, housing, loan | 违约记录、余额、房贷、个人贷款 |
| 本次营销信息 | contact, month, day_of_week, duration | 联系渠道、月份、星期、通话时长 |
| 历史营销信息 | campaign, pdays, previous, outcome | 本次营销拨打次数、上次联系距今天数、历史拨打次数、历史结果 |
| 目标变量 | y | 是否认购定期存款 |
这里每个字段都有讲究,后文我会重点讲几个容易出坑的字段,比如duration通话时长和pdays上次联系天数。
2. Stacking方法的核心原理:从"意见征询"到"两级模型协作"
2.1 单模型的瓶颈在哪里
说实话,如果只追求一个能用的模型,逻辑回归或者随机森林已经能应付这个场景。但问题是,银行客户对产品的认购行为,不是简单的线性关系。
举一个具体例子:年龄和认购定期存款的关系。太年轻的客户,积蓄少,对流动性要求高,不太可能买定存;年纪太大的客户,可能更倾向保守,但消费和医疗支出也要考虑,同样不一定买。也就是说,年龄和认购倾向大概率是倒U型关系,甚至更复杂。逻辑回归这种线性模型学不到这种弯弯绕绕的东西;单棵决策树又特别容易过拟合,对数据里的噪声非常敏感。
这就是我选择集成方法的出发点。集成学习的核心逻辑一句话:多个有差异的模型,犯错的位置往往不完全重叠,把它们组合起来,整体表现比任何一个单独模型都稳定。
2.2 集成学习的三个流派和Stacking的定位
业内主流的集成方法分三派:
- Bagging:并行训练多个模型,各自独立预测,最后投票或取平均。典型代表是随机森林。
- Boosting:串行训练,每一轮模型都在修正上一轮的预测残差。典型代表是XGBoost、LightGBM。
- Stacking(堆叠):不走投票,而是把多个模型的预测结果当作新的特征,再交给一个"元模型"去学习如何组合这些预测。
前两种大家接触得比较多,Stacking相对少一些,但理解难度并不高。
我常用一个比喻:一个老板要做重大决策,他不会只听一个顾问的意见,也不会简单地让几个顾问投票,而是会请几个风格不同的顾问,比如销售总监、技术总监、财务总监,分别说出他们的看法,然后老板结合当前公司的实际情况,自己再做一个综合判断。在Stacking里,那几个风格不同的顾问就是第一层基学习器,老板就是第二层元学习器。
2.3 为什么第二层不是简单平均
有人会问:既然要综合多个模型的意见,为什么不直接对几个模型的预测概率取算术平均,非得再训练一个模型?这个问题问得很好。
平均权重的问题是"一刀切"。这个逻辑回归模型整体AUC低一些,不代表它在某个客户群里不擅长;那个XGBoost整体很强,但不代表它在每个客户个体上都有把握。算术平均等于强行给所有顾问一样的发言权,无法根据每个客户的具体情况动态调整谁的判断更可信。
元学习器学习的是每个基学习器的"可信度模式"。比如逻辑回归更懂低收入客户群,XGBoost更擅长捕捉复杂交互,元模型会在不同区间给不同模型不同的权重。这就是Stacking相对于简单平均的本质优势:从"固定投票"升级为"看人下菜碟"。
3. R环境下数据清洗与特征工程:这一环比模型选择更重要
3.1 脏活累活:数据读入与基础清洗
项目里我用的R实现,第一步就是把数据读进来,统一处理好类型再往下走。这一步代码不复杂,但几个细节值得讲清楚:
library(caret) library(pROC) library(xgboost) library(randomForest) library(glmnet) bank <- read.csv("bank_marketing.csv", stringsAsFactors = TRUE) str(bank) summary(bank)stringsAsFactors = TRUE在这里是故意这么设的,因为后续建模希望字符型变量以因子(factor)形式存在,尤其对树类模型来说,因子处理比手动做一堆哑变量更方便。
这组数据的缺失值情况比较好,没有大面积的空值。但在做真实银行数据时,这一步千万不能跳过。我的习惯是先跑一遍summary,观察每个变量的取值范围和缺失比例,出现明显的异常值再单独处理。比如age出现200岁这种数据,肯定要清洗掉或者修正。
3.2 最容易让人误入歧途的duration字段
这个字段我在多个项目里见过有人踩坑,必须单独拿出来说。duration是"最后一次通话的时长",在模型里如果直接用,AUC会瞬间飙到0.9以上,看起来效果惊人。但你冷静想一下业务的时序关系:通话时长是在营销人员已经给客户打完电话之后才知道的,未打电话之前根本没有这个值。你拿着它去预测客户会不会买,等于拿着答案去猜答案,这是典型的数据泄漏。
实际建模时要处理这个字段,两种思路:一是直接去掉,因为预测阶段没有对应的真实值;二是如果业务上确实需要保留通话信息作为线索,也得分场景讨论,比如在"客户接起电话的前10秒能不能预判他会不会买"这种实时场景里,才可能用到部分通话信息。我在这个项目里选择直接剔除。
删掉这列之后,模型AUC回落到0.75~0.85的正常区间,这才是真实可落地的水平。
3.3 pdays的999编码处理
pdays表示"客户上次被联系过距今多少天",但这个字段存在一个特殊值999,业务含义是"该客户之前从未被联系过"。如果把999当成单纯的数值保留进模型,做逻辑回归时它会被当成一个巨大的连续数值,系数解释完全错乱;做树模型时,它又会被当成一个纯粹的分叉点,丢失掉"从未联系过"这个业务含义。
我的处理方式:新增一个哑变量pnever = ifelse(pdays == 999, 1, 0),同时把pdays在999处的值替换成0或缺失标志,让模型同时看到"是否联系过"和"距上次联系天数"两个维度的信息。这种"业务语义拆解"是特征工程里最基础也最有价值的一步,比调模型参数的影响大得多。
3.4 month和day_of_week这样的周期变量怎么处理
模型里还有月份和星期这样的变量。直接按数字编码是不对的,因为12月(12)和1月(1)其实是相邻的,数字编码会让模型把12和1的距离当成11去理解。直接做one-hot也有问题,因为月份是12个类别,展开后维度增加,且丢失了周期性。
实操中有两种主流方案:
- 如果用的是树模型,直接把month变成因子,让树自己去切分,比如发现"3月,9月,10月"这一类客户响应率高,这是树模型的自由切分能力,完全OK。
- 如果准备喂给逻辑回归这种线性模型,可以考虑用正弦余弦变换,比如
cos_month = cos(2 * pi * month / 12),把12月到1月的"周期性"保留下来。
我这里主要用树模型加逻辑回归混合的Stacking结构,所以month直接作为因子保留,逻辑回归那边依赖one-hot展开。这里有一个经验:不要怕变量多,但要怕变量含义混乱,含义理清楚了,展开成哑变量只是时间问题。
3.5 训练集与测试集的划分
数据划分我固定用分层抽样,确保训练集和测试集里正样本比例接近。R里直接:
set.seed(2024) train_idx <- createDataPartition(bank$y, p = 0.7, list = FALSE) train_data <- bank[train_idx, ] test_data <- bank[-train_idx, ]createDataPartition会按y这个因变量的类别比例进行分层抽样,这样训练集和测试集的响应率就不会出现"训练集6%、测试集20%"这种严重漂移。注意set.seed必须设,否则每次跑出来的结果随机波动太大,后面评估模型根本没法判断提升是来自算法还是来自运气。
4. 基学习器选型:多样性决定了Stacking的上限
4.1 为什么说"基学习器越像,Stacking越没用"
Stacking的核心收益来自基学习器之间的差异性。如果两个模型本质上是同一类算法(比如随机森林和梯度提升树,都是树模型),它们的预测结果高度相关,犯错模式也高度重合,那么第二层模型没有任何"分歧"可以利用,跟只用一个模型没区别。
所以我在选基学习器时,刻意挑选了不同算法族的代表,而不是清一色堆树模型。最终确定的组合是:随机森林、XGBoost、逻辑回归三件套。偶尔我也会加一个KNN进去,但经验表明,核心的三个已经够用。
4.2 三件套各自的定位
| 基学习器 | 所属流派 | 擅长处理的问题 | 在Stacking里的角色 |
|---|---|---|---|
| 逻辑回归 | 线性模型 | 线性关系、全局趋势、可解释性强 | 提供全局线性视角,补充树模型看不到的平滑判断 |
| 随机森林 | Bagging | 高维特征、类别特征、抗过拟合 | 提供稳定的基线预测,抵抗单模型噪声 |
| XGBoost | Boosting | 非线性交互、复杂模式 | 捕捉特征间的复杂交互关系,挖掘深层规律 |
逻辑回归的作用经常被忽略,但在Stacking里它是非常关键的一环。它虽然简单,但学习到的是全局线性的规律。树模型擅长把一个空间切分成很多块,而逻辑回归擅长给整个空间一个平滑的整体判断。两种思路互补性很强,在线性部分很强、非线性部分弱的数据上,加入逻辑回归之后Stacking的提升往往最明显。
随机森林的价值是稳。它对异常值和噪声的容忍度高,不容易被个别极端样本带偏,而且对类别型变量非常友好,不需要做标准化。
XGBoost是主要非线性主力,能发现变量之间的交互作用。但注意,XGBoost调参敏感,树太多容易过拟合,eta太大又学不到细节,稍后会给参数思路。
4.3 为什么不把所有模型都塞进去
有读者可能会问:既然Stacking鼓励多样性,那多放几个模型会不会更好?比如LightGBM、CatBoost、SVM全都放进去。
从原理上讲,基学习器确实越多越好,但前提是它们"都有独立价值"。实际做下来你会发现几个问题:
- 训练成本成倍增加,每个模型都要做交叉验证生成元特征,基学习器放5个以上,跑一轮实验的时间就是一个下午。
- 强相关模型塞多了,第二层元模型容易出现过拟合,因为它要拟合的不再是"不同意见的集合",而是一堆高度共线的变量。
我的个人标准是:先看两两模型的预测相关性。如果两个模型的相关系数超过0.85,只留其中一个就够了;如果相关性在0.7以下,说明互补性好,加进去有戏。这个原则比盲目堆模型可靠得多。
4.4 基学习器的参数设置思路
逻辑回归用glmnet做L2正则,alpha=0,这样能防止特征共线性把系数放大。正则强度用交叉验证选择lambda.min,防止逻辑回归在稀疏哑变量矩阵上过拟合。
随机森林里树的数量设500,mtry取默认sqrt(特征数),在实际数据上表现已经足够稳定。树太多收益递减,树太少方差偏大。
XGBoost这一块需要一点耐心。我初始设eta=0.05,max_depth=5,然后用内部交叉验证确定nrounds。这样配置的核心思路是:小学习率加合理深度,让模型学得稳但不死记。
rf_model <- randomForest(x = x_train, y = factor(y_train), ntree = 500, importance = TRUE) xgb_params <- list(eta = 0.05, max_depth = 5, objective = "binary:logistic", eval_metric = "auc") xgb_model <- xgboost(data = xgb.DMatrix(data = x_train, label = y_train), params = xgb_params, nrounds = 200, verbose = 0) lr_model <- cv.glmnet(x_train_dummy, y_train, family = "binomial", alpha = 0, type.measure = "auc")5. 第一层训练与元特征生成:防止数据泄漏的关键细节
5.1 最容易被忽略的坑:元特征来自"见过样本的模型"
Stacking如果只写一个"先训练几个模型,把预测结果拼起来再训练元模型",看似简单,但这里藏着一个足以毁掉整个模型的大坑。
假如我先在全部训练数据上训练随机森林,再去预测同一批训练数据,得到的预测结果是什么?是模型对"见过面的老朋友"的判断,而不是对"新客户"的判断。树模型在训练数据上的预测概率通常两极分化,接近0或者接近1,极其自信。用这种结果去训练元模型,元模型会以为基学习器很强大,但真正到了测试集、面对从没见过的客户时,基学习器的预测就会收敛得多,没这么极端,元模型的判断基础就崩塌了。
结果就是:Stacking在训练集上的表现漂亮得惊人,但验证集和测试集上立刻原形毕露。这是我见过最多人踩的坑,也是Stacking被说成"容易过拟合"的根本原因。
5.2 正确解法:K折交叉验证生成元特征
正确的做法是,对于训练集中的每个样本,它的元特征必须来自一个"从未见过该样本"的模型。具体方式就是K折交叉验证:
- 把训练集切成K份(我常用5份)。
- 每次取其中K-1份训练基学习器,用训练好的模型去预测剩下1份。
- 重复K次,让每个样本都恰好被一个没见过它的模型预测过一次。
- 把这些预测结果拼起来,才是可以喂给元模型的"干净"元特征。
而测试集的元特征,则是用K个模型中任一个(通常是全量训练集重训的模型)去预测测试集得到的。这样生成的训练元特征和测试元特征在分布上是一致的,第二层模型学到的规律才真实可信。
5.3 R语言完整实现
下面这段代码是项目里核心的元特征生成过程,用的是5折交叉验证,每一折训练三个基学习器,分别对验证折叠生成预测,最后合并成完整的元特征训练集。
set.seed(2024) K <- 5 folds <- createFolds(factor(y_train), k = K, list = TRUE) meta_rf <- numeric(length(y_train)) meta_xgb <- numeric(length(y_train)) meta_lr <- numeric(length(y_train)) for (i in 1:K) { train_idx <- unlist(folds[-i]) valid_idx <- unlist(folds[i]) # 基学习器1:随机森林 rf_fold <- randomForest(x = x_train[train_idx, ], y = factor(y_train[train_idx]), ntree = 500) meta_rf[valid_idx] <- predict(rf_fold, x_train[valid_idx, ], type = "prob")[, "1"] # 基学习器2:XGBoost xgb_fold <- xgboost( data = xgb.DMatrix(data = x_train[train_idx, ], label = y_train[train_idx]), params = list(eta = 0.05, max_depth = 5, objective = "binary:logistic", eval_metric = "auc"), nrounds = 200, verbose = 0 ) meta_xgb[valid_idx] <- predict(xgb_fold, xgb.DMatrix(data = x_train[valid_idx, ])) # 基学习器3:逻辑回归(L2正则) lr_fold <- cv.glmnet(x_train_dummy[train_idx, ], y_train[train_idx], family = "binomial", alpha = 0, type.measure = "auc") meta_lr[valid_idx] <- predict(lr_fold, newx = x_train_dummy[valid_idx, ], s = "lambda.min", type = "response")[, 1] } meta_train <- data.frame(rf = meta_rf, xgb = meta_xgb, lr = meta_lr, y = y_train)需要注意的是,这里逻辑回归的x_train_dummy是做了one-hot展开后的矩阵,而树模型用的是原始因子矩阵。Stacking的基学习器可以各自使用适合自己算法的预处理方式,这没关系,因为到元特征层面,所有模型的输出都已经统一成了概率值。
这段代码跑完检查一下meta_train这三列的相关性。我这次项目里,随机森林和XGBoost的元特征相关性大概在0.6~0.7之间,逻辑回归和两个树模型的相关性在0.5左右。这个水平比较理想,说明三个模型既没有完全重合,也没有弱到完全没有共识。
5.4 测试集的元特征生成
测试集的元特征不需要交叉验证,直接用"在全量训练集上重新训练的基学习器"预测测试集即可。但注意,必须用全量训练集重训,而不是交叉验证里某个单折模型,这样才能用上全部信息。
# 用全量训练集重训三个基学习器 rf_full <- randomForest(x = x_train, y = factor(y_train), ntree = 500) xgb_full <- xgboost(data = xgb.DMatrix(data = x_train, label = y_train), params = xgb_params, nrounds = 200, verbose = 0) lr_full <- cv.glmnet(x_train_dummy, y_train, family = "binomial", alpha = 0, type.measure = "auc") meta_test <- data.frame( rf = predict(rf_full, x_test, type = "prob")[, "1"], xgb = predict(xgb_full, xgb.DMatrix(data = x_test)), lr = predict(lr_full, newx = x_test_dummy, s = "lambda.min", type = "response")[, 1] )6. 元模型与两层模型的协作:从得分到营销名单
6.1 元模型选什么?逻辑回归是最优解
第一层基学习器训练好了,元特征的训练数据也准备好了,接下来就是第二层元模型的训练。
这里我的选择依然是逻辑回归,而且是带正则的逻辑回归。可能有人觉得不够刺激,会想再叠一层XGBoost。但从原理来讲,元模型的工作不是重新发现复杂规律,而是学习第一层模型的"权重组合"。它的输入只有几个概率值,维度很低,完全不需要复杂的非线性映射。逻辑回归既简单、又稳定,而且它还能告诉我们每个基学习器的权重,直接反映"谁在这个数据集上更可信"。
如果此时再用复杂的模型做元模型,第一层模型的预测误差会在第二层被进一步放大,过拟合风险比逻辑回归高得多。真正做好Stacking的诀窍,是"第一层强而各异,第二层弱而稳健"。
set.seed(123) meta_model <- glm(y ~ rf + xgb + lr, data = meta_train, family = binomial(link = "logit")) # 查看三个基学习器的系数 summary(meta_model)看模型系数通常会得到一个信息:XGBoost的系数最大、最显著,说明在这个数据集上XGBoost的信息量最高;随机森林次之;逻辑回归贡献也不错。这三个系数合在一起,就是元模型学到的"信任分配"。
6.2 效果评估:不能只看单点指标
模型训练结束后,常规操作是画ROC曲线、算AUC。我用pROC包在测试集上评估,结果大致是这个量级:
| 模型 | 测试集AUC |
|---|---|
| 逻辑回归(单独) | 0.78 |
| 随机森林(单独) | 0.80 |
| XGBoost(单独) | 0.82 |
| Stacking三模型集成 | 0.85 |
最基础的逻辑回归做基线大概在0.78左右,XGBoost单模型能到0.82,Stacking之后在0.85附近。这个提升幅度不是特别夸张,大概3个点的AUC。但请注意,在银行营销这种大规模存量名单场景里,AUC每提升一点,对营销名单头部人群的响应率提升都可能非常可观,这是量变到质变的过程。
再算KS值。把测试集预测概率按10个分箱,计算每个分箱里好样本(认购)和坏样本(不认购)累计占比差的绝对最大值。Stacking的KS大约在0.55~0.6之间,说明"认购"和"不认购"两个群体在得分分布上分离得比较明显。
不过要说明一下:具体数字和数据集有关,不同运行环境下可能浮动。我这篇文章里的核心价值在于完整的建模方法和验证思路,不必纠结数字本身。
6.3 从模型得分到营销名单:阈值怎么定
模型只是产出概率,真正对业务有价值的是那张"营销名单"。我通常按预测概率从高到低排序,让业务方按人力上限去切前多少比例。
假设全量名单有10000个客户,整体实际响应率约11.7%,随机拨打10000人,预计能约到1170个响应客户。如果把Stacking模型的预测概率降序排列,只打前20%也就是2000人,这2000人的实际响应率按模型头部提纯效果来看,大概率能做到30%左右,也就是约600个响应客户。同样的电话量,从1170降到600看起来是减少了,但注意,这里只打了2000个电话就抓到600个,而原先要打10000个电话才抓到1170个。如果把这2000人的额度换成功力,相当于用原来五分之一的电话量抓到了一半的响应客户。
这种对比口径财务人员一眼就能看懂,模型的价值也就落地了。
6.4 阈值不是拍脑袋定的
具体的阈值设定要结合成本和收益来算:
- 每通电话的营销成本(人力+通讯+时间)
- 单笔产品签约带来的平均利润
- 模型在每个阈值下的捕获率曲线
我的习惯是先算"预期收益 = 响应率 × 平均单户利润",然后算"营销成本 = 拨打量 × 单通成本",找到"投入产出比最高"的切分点,而不是机械地固定在top20%。有时候top30%的名单ROI反而比top20%更高,因为头部太强势的客户也许已经被其他渠道转化过了。
7. 实战过程中踩过的坑:从"AUC虚高"到"特征语义错乱"
7.1 虚拟变量和树模型的因素冲突
有一次我图省事,把所有分类变量一次性做了one-hot展开,然后把展开后的矩阵同时喂给随机森林和逻辑回归。结果树模型的表现反而下降了。原因在于,随机森林处理原始因子时可以利用"按类别切分"的优势做多路切分,而展开成大量0/1哑变量后,树的每一次切分只能选一个哑变量,逻辑上等价于只能"选某个类别",忽略了类别之间的组合关系。
解决办法:树模型用原始因子矩阵,线性模型用one-hot后的哑变量矩阵。基学习器可以有不同的输入形态,这在Stacking框架里完全成立,因为每个模型内部的处理逻辑本来就是独立的。
7.2 类别变量强行转数值,逻辑回归系数彻底失控
另一个类别变量处理上的坑是:图省事把职业、教育水平转成1、2、3、4这样的数字编码,然后直接喂给逻辑回归。逻辑回归会把它当成连续变量,比如"教育程度"从1变到4,模型会理解为"每升一级,认购概率变化一个固定量",但实际上从"小学"到"大学"的语义距离根本不是等距的。
解决方法是:除非变量天然有序且间隔确实近似相等,否则一律转成因子,线性模型侧做one-hot,树模型侧转为因子直接使用。
7.3 特征相关性和同质化的误判
还有一次我只用了两个树模型做Stacking,随机森林和GBDT。跑出来的结果和单模型XGBoost几乎一样,毫无提升。后来做了预测相关性分析才发现,这两个模型的相关系数在0.9以上——它们犯了几乎一样的错误,元模型学不到任何新东西。
从那次之后,我给自己定了个流程:每加一个基学习器之前,先算一遍它与现有模型在验证集预测上的Spearman相关系数。如果相关系数高,就果断放弃,不因为"多一个模型听上去更高级"而堆砌无意义的结构。
7.4 R语言环境依赖的坑:xgboost安装编译问题
R语言实现里还有一类跟模型无关但很折磨人的坑,就是依赖包的安装。xgboost包在部分R版本下需要编译,而这又牵扯到本地的C++编译器和相关系统库。我在Windows机器上装过几次都要折腾半天。
经验上两个办法最稳妥:
- 尽量用
install.packages("xgboost")走CRAN预编译版本,不要轻易去GitHub装开发版。 - 如果公司电脑不方便装编译器,直接在
anaconda环境里用conda install -c conda-forge r-xgboost,conda会帮忙处理动态库依赖,比裸R折腾少得多。
另外一个常见问题是rJava。如果你要用某些包(比如部分建模辅助工具)依赖Java环境,容易被环境变量问题卡住。解决办法也很简单:不用就坚决不装,R语言生态里同一个功能通常能找到无Java依赖的替代方案。
7.5 不要迷信"模型复杂度"
最后一个坑不是技术上的,而是心态上的。遇到过几次,同行为了追求Stacking的"全都要"效果,把LightGBM、CatBoost、SVM、神经网络全部拽进来,训练花了几个小时,最后AUC也就比三模型版本高了0.003,连统计显著性都过不了。
我的经验是:Stacking不是模型越复杂越好,而是信息互补性越强越好。三个不同算法族的模型已经能覆盖线性、Bagging、Boosting三大主流思想,性价比最高。在此基础上再加模型,边际收益递减而且引入共线风险。真正要投入精力的地方是特征工程和样本质量,那才是压杠杆的地方。
写到最后的一点经验
按我个人习惯,每次跑完一个模型,都会把三件事记录下来:基学习器的预测相关性矩阵、元模型的权重系数、各模型在测试集上的AUC。这三样东西是下一次建模时最有价值的输入。尤其是相关性矩阵,直接决定要不要替换某个基学习器,比单纯看单个AUC更有指导意义。
如果未来想把这套流程做得更系统,可以往两个方向扩展:一是把基学习器换成带业务约束的模型,比如把产品风险等级作为先验特征;二是在元特征里加入非模型信息,比如客户历史交互次数、渠道偏好等业务侧变量,让第二层的判断依据更丰满——这也是Stacking这个框架最有想象力的一点,它的输入不必只来自模型,任何你觉得有判别力的信息都可以作为"特征"参与第二层学习。