随机森林实战:从银行客户预测到房价估计,理解 Bagging 集成学习

举报
yd_276081655 发表于 2026/10/21 10:17:27 2026/10/21
【摘要】 随机森林(Random Forest)是机器学习中非常经典的集成学习算法。它不依赖复杂的神经网络,而是通过训练大量存在差异的决策树,将它们的预测结果组合起来,从而提高模型的稳定性和泛化能力。相比 XGBoost,随机森林的训练机制更加直观、调参难度相对较低,特别适合作为结构化表格数据的基线模型。

本文通过两个真实数据集介绍随机森林的实际用法:银行营销客户转化预测(二分类)和加州房价预测(回归)。结合 Python 代码,重点理解 Bootstrap 抽样、特征随机选择、袋外评估(OOB)、特征重要性,以及随机森林与 XGBoost 的主要区别。

一、随机森林到底是什么?

先从最简单的决策树说起。

假设银行希望判断一位客户是否可能购买定期存款产品。一棵决策树可能按照以下规则逐层判断:

客户年龄 > 40 岁?
    ├── 是:历史营销是否成功?
    │       ├── 是:购买可能性较高
    │       └── 否:继续检查账户余额
    │
    └── 否:是否有住房贷款?
            ├── 是:购买可能性较低
            └── 否:继续检查其他特征

决策树容易理解,但存在一个问题:它对训练数据比较敏感。样本略有变化,生成的树可能明显不同。

随机森林的解决思路是:既然单棵树不够稳定,那就训练很多棵不同的树,再综合它们的判断。

它主要通过两种随机机制,让树与树之间产生差异。

第一种:Bootstrap 有放回抽样。

假设训练集有 10,000 条记录,每棵树都从中有放回地抽取 10,000 次。因此,部分样本会被重复抽到,也有部分样本不会出现。

平均来看,每棵树的训练样本大约覆盖原始训练集 63.2% 的不同记录,其余约 36.8% 的记录没有被该树抽中。

第二种:随机选择候选特征。

在每个节点寻找最佳分裂条件时,并不总是考察全部特征,而是随机抽取一部分候选特征。

例如,数据集有 100 个特征,分类模型可以在每个节点随机考虑约 10 个特征。

这样能够减少不同树之间的相似性,避免所有决策树都过度依赖某几个强特征。

多棵树如何产生最终预测?

对于分类任务,随机森林通常将各棵树预测的类别概率取平均,再决定最终类别。

对于回归任务,则直接对各棵树的预测值取平均:

[
\hat y(x)=\frac{1}{T}\sum_{t=1}^{T}f_t(x)
]

其中,(T) 是决策树数量,(f_t(x)) 是第 (t) 棵树对样本 (x) 的预测值。

这个过程被称为 Bagging(Bootstrap Aggregating)。

它的价值在于降低预测方差。即使某些树产生了较大的预测偏差,其他树也可能在一定程度上抵消这些波动。

但随机森林并非树越多就一定越准确。增加树的数量通常有助于稳定预测结果,但收益会逐渐减小,同时增加计算与内存开销。


二、案例一:银行营销——预测哪些客户更可能购买产品

1. 业务背景

第一个案例来自 UCI Machine Learning Repository 的经典数据集 Bank Marketing。

数据来源于葡萄牙某银行的电话营销活动,任务是根据客户信息及历史营销记录,预测客户是否订购定期存款产品。

其中 bank-full.csv 包含 45,211 条记录,主要特征如下:

特征 含义
age 客户年龄
job 职业类型
balance 账户平均年度余额
housing 是否有住房贷款
loan 是否有个人贷款
previous 历史营销联系次数
poutcome 过去营销活动的结果
y 是否购买定期存款产品

这是一个典型的二分类问题。

业务目标并不是简单地判断客户会不会购买,而是希望通过模型筛选潜在客户,将有限的营销资源分配给购买可能性更高的人群。

2. 数据准备

进入 UCI Machine Learning Repository,搜索 Bank Marketing,下载数据压缩包并解压,找到 bank-full.csv。

安装依赖:

pip install scikit-learn pandas numpy

这里有一个很值得注意的细节:原始数据包含 duration,表示最后一次通话的时长。

这个特征虽然对预测非常有效,但在拨打电话之前根本无法获得。

如果目标是预测“哪些客户值得提前联系”,就必须删除它,否则会引入数据泄漏(Data Leakage),导致离线评估过于乐观。

3. Python 完整实践

import numpy as np
import pandas as pd

from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    roc_auc_score,
    average_precision_score,
    precision_recall_curve,
    classification_report
)

# 1. 读取数据
df = pd.read_csv("bank-full.csv", sep=";")

# 排除标签和无法在营销前获得的通话时长
X = df.drop(columns=["y", "duration"])
y = (df["y"] == "yes").astype(int)

# 2. 按原始时间顺序划分数据
# bank-full.csv 原始记录按日期排序
n = len(df)
a, b = int(n * 0.7), int(n * 0.85)

X_train, X_valid, X_test = (
    X.iloc[:a], X.iloc[a:b], X.iloc[b:]
)
y_train, y_valid, y_test = (
    y.iloc[:a], y.iloc[a:b], y.iloc[b:]
)

# 3. 类别特征编码
cat_cols = X.select_dtypes(include="object").columns
num_cols = X.select_dtypes(exclude="object").columns

preprocessor = ColumnTransformer([
    ("categorical",
     OneHotEncoder(handle_unknown="ignore"),
     cat_cols),
    ("numeric", "passthrough", num_cols)
])

# 4. 构建随机森林
rf = RandomForestClassifier(
    n_estimators=300,
    max_depth=12,
    min_samples_leaf=10,
    max_features="sqrt",
    class_weight="balanced_subsample",
    n_jobs=-1,
    random_state=42
)

model = Pipeline([
    ("preprocessor", preprocessor),
    ("random_forest", rf)
])

# 5. 训练
model.fit(X_train, y_train)

# 6. 使用验证集选择分类阈值
valid_prob = model.predict_proba(X_valid)[:, 1]

precision, recall, thresholds = precision_recall_curve(
    y_valid, valid_prob
)

f1 = (
    2 * precision[:-1] * recall[:-1]
    / (precision[:-1] + recall[:-1] + 1e-12)
)

threshold = thresholds[np.argmax(f1)]

# 7. 在独立测试集上评估
test_prob = model.predict_proba(X_test)[:, 1]
test_pred = (test_prob >= threshold).astype(int)

print("分类阈值:", round(threshold, 4))
print("ROC-AUC:", roc_auc_score(y_test, test_prob))
print(
    "Average Precision:",
    average_precision_score(y_test, test_prob)
)

print(classification_report(
    y_test, test_pred, zero_division=0
))

代码有三个值得关注的地方。

第一,随机森林并不要求对数值特征进行标准化。

与部分依赖特征尺度的模型不同,决策树通过阈值划分样本。对于一般的数值变量,通常无需额外进行标准化。

不过,类别特征仍然需要适当处理。本案例使用 One-Hot 编码,避免直接把职业、教育等无序类别错误地当作连续数字。

第二,类别不平衡需要特别关注。

银行营销活动中,购买产品的客户通常只占少数。

因此代码设置 class_weight="balanced_subsample",在每棵树的 Bootstrap 样本中分别计算类别权重,以降低类别不平衡的影响。

但调整类别权重可能影响模型概率的校准程度,因此输出的概率不能不经检验就直接当作真实购买概率。

第三,不能只看 Accuracy。

本案例重点使用 ROC-AUC、Average Precision、Precision 和 Recall。

其中:

  • Precision:预测会购买的客户中,实际购买的比例。
  • Recall:实际购买的客户中,被模型识别出来的比例。
  • ROC-AUC:衡量模型整体区分正负样本的能力。
  • Average Precision:综合衡量不同阈值下的精确率与召回率。

选择营销名单时,还可以增加 Precision@K 或 Lift@K:例如,只联系评分最高的 10% 客户,实际购买率能达到总体平均购买率的多少倍。

对于存在明显时间变化的营销数据,按时间顺序验证通常比随机划分更贴近真实应用。本案例的阈值采用验证集 F1 最大化选择,实际部署时还需要结合营销成本和预期收益确定。


三、案例二:房地产估值——随机森林如何完成回归任务?

1. 业务背景

第二个案例使用 scikit-learn 提供的 California Housing 数据集。

数据来源于美国 1990 年人口普查,包含 20,640 条加州地区住房统计记录,提供 8 个数值特征,例如:

  • MedInc:地区家庭收入中位数。
  • HouseAge:房屋年龄中位数。
  • AveRooms:每户平均房间数。
  • AveOccup:每户平均居住人数。
  • Latitude、Longitude:地理坐标。

预测目标是地区房价中位数,而不是某套具体住宅的成交价格。

这是一个回归问题,输出是连续数值。

2. Python 实践:对比单棵决策树和随机森林

import numpy as np

from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import (
    mean_absolute_error,
    mean_squared_error,
    r2_score
)

# 1. 加载数据
# 首次运行需要联网下载,后续使用本地缓存
housing = fetch_california_housing(as_frame=True)

X = housing.data

# 目标单位换算为美元
y = housing.target * 100000

# 2. 划分训练和测试集
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 3. 单棵决策树
tree = DecisionTreeRegressor(
    max_depth=15,
    random_state=42
)

# 4. 随机森林
forest = RandomForestRegressor(
    n_estimators=300,
    max_depth=20,
    min_samples_leaf=3,
    max_features=0.8,
    n_jobs=-1,
    random_state=42
)

# 5. 训练并对比
for name, model in [
    ("Decision Tree", tree),
    ("Random Forest", forest)
]:
    model.fit(X_train, y_train)
    pred = model.predict(X_test)

    mae = mean_absolute_error(y_test, pred)
    rmse = np.sqrt(mean_squared_error(y_test, pred))
    r2 = r2_score(y_test, pred)

    print(f"\n{name}")
    print(f"MAE:  ${mae:,.0f}")
    print(f"RMSE: ${rmse:,.0f}")
    print(f"R²:   {r2:.4f}")

# 6. 查看随机森林的特征重要性
importances = sorted(
    zip(X.columns, forest.feature_importances_),
    key=lambda item: item[1],
    reverse=True
)

print("\n特征重要性:")
for feature, score in importances:
    print(f"{feature}: {score:.4f}")

3. 结果应该怎么看?

代码将输出两组回归评价指标。

指标 业务含义
MAE 预测房价与真实房价之间的平均绝对误差
RMSE 更强调较大预测误差的指标
R² 模型相对均值预测基准的解释能力

例如,假设某次实验得到:

Decision Tree
MAE:  $42,000
RMSE: $65,000
R²:   0.58

Random Forest
MAE:  $33,000
RMSE: $49,000
R²:   0.76

以上数字只是解释指标的假设示例,不是代码实测结果。

如果随机森林的 MAE、RMSE 更低,R² 更高,就说明在当前测试集上,多树集成比单棵树具有更好的预测效果。

这种改善主要来自不同决策树之间的误差互相抵消,从而降低模型预测方差。

需要注意,这个数据集包含大量地理位置接近的地区。随机划分训练集和测试集可能让模型借助相邻地区的信息获得较乐观的结果。若用于跨区域估值,应该增加按地区分组的验证方案。

另外,房价中位数目标存在上限截断,不能把它当作无截断的完整市场价格分布。


四、一个值得掌握的技巧:OOB 袋外评估

随机森林还有一个很实用的机制:Out-of-Bag,简称 OOB。

前面提到,每棵树通过 Bootstrap 抽样训练,平均约 36.8% 的原始样本不会被这棵树抽中。

对于某条训练记录,我们可以找到那些训练时没有使用过它的决策树,让这些树对它进行预测,从而获得一个袋外预测结果。

对全部训练样本重复这一过程,就能够计算 OOB 评价指标。

在 scikit-learn 中,只需增加一个参数:

rf = RandomForestClassifier(
    n_estimators=300,
    bootstrap=True,
    oob_score=True,
    random_state=42,
    n_jobs=-1
)

rf.fit(X_train_encoded, y_train)

print("OOB Score:", rf.oob_score_)

这里的 X_train_encoded 指已经完成类别编码的数值特征矩阵。

对于分类模型,默认的 oob_score_ 是 Accuracy;对于回归模型,默认是 R²。

OOB 的优势是可以利用训练阶段未被对应决策树使用的样本进行内部评估,而不必再专门为这项评估划分一部分训练数据。

不过,OOB 不能完全代替独立测试集。如果数据存在明显时间依赖、同一客户的重复观测或群组相关性,OOB 的随机抽样机制可能无法模拟真实部署环境。


五、随机森林的核心参数怎么调?

随机森林的调参可以优先关注以下几个参数。

参数 含义 实践建议
n_estimators 决策树数量 从 100~300 棵开始
max_depth 树的最大深度 控制模型复杂度
min_samples_split 节点继续分裂所需的最少样本数 增大通常使树更保守
min_samples_leaf 叶节点最少样本数 增大可降低局部预测波动
max_features 每次分裂考虑的候选特征数量 控制树之间的相关性
max_samples 每棵树 Bootstrap 抽样的规模 可用于降低训练成本
class_weight 分类任务的类别权重 可用于类别不平衡问题
oob_score 是否进行袋外评估 需要 bootstrap=True

这里最值得注意的是 max_features。

如果每次分裂都使用全部特征,不同树容易反复选择同一个强特征,造成树之间高度相似。

而随机森林的优势恰恰依赖不同树之间存在适度差异。

因此,减少每次分裂的候选特征数量,有时反而能够提升整体泛化效果。

此外,feature_importances_ 提供的是基于节点不纯度下降的特征重要性。这一指标容易偏向取值丰富的特征,也不能表示因果关系。

如果需要更可靠地评价模型对某个变量的依赖程度,可以在验证集上使用 permutation_importance:随机打乱某个特征,观察模型性能下降多少。但高度相关的特征仍可能使其结果难以解释。

六、随机森林和 XGBoost,究竟有什么区别?

这是实际建模时很常见的问题。

虽然两者都使用决策树,但核心思想并不相同。

维度 随机森林 XGBoost
集成方式 Bagging Gradient Boosting
训练关系 不同树通常可以并行训练 后续树依赖此前的模型
核心思路 通过集成降低方差 持续优化损失函数
树之间的关系 相对独立 逐轮迭代、有依赖关系
调参复杂度 相对较低 通常更复杂
对过拟合的控制 样本采样、特征采样、树复杂度限制 学习率、正则化、采样、树复杂度限制等
典型用途 表格分类、回归、基线模型 需要深入调优的表格预测任务

用一个比较形象的说法:

随机森林像是让 300 位分析师独立研究同一个问题,再综合他们的判断;XGBoost 则像是让后一位分析师不断修正此前分析师的错误。

随机森林的训练通常更容易并行化,也往往能以相对少的调参工作建立不错的基线。

XGBoost 则通过逐轮优化损失函数,在许多结构化数据任务上具有竞争力,但训练与调参机制更复杂。

它们没有绝对的优劣之分。实践中应当统一数据切分、评价指标和计算预算,再比较两个模型的泛化表现。


七、总结:什么时候应该优先考虑随机森林?

随机森林适合以下情况:

  • 数据主要是结构化表格,存在复杂的非线性特征关系。
  • 希望快速建立一个可靠的分类或回归基线模型。
  • 不希望投入过多精力进行超参数搜索。
  • 希望利用特征重要性、OOB 等方法辅助分析。
  • 数据规模适中,能够接受多棵树带来的内存和推理开销。

但它也存在局限。例如,在非常高维的稀疏数据上未必占优;回归预测通常不擅长外推训练范围之外的趋势;大规模森林还可能带来明显的内存和推理延迟成本。

从这两个案例可以发现,机器学习真正重要的并不是把算法调得多复杂,而是先定义正确的预测时点,再选择合理的数据切分方式和业务评价指标。

随机森林尤其适合作为 XGBoost、LightGBM 或神经网络的对照模型。

如果一个复杂模型经过大量调参后仍然无法稳定超过随机森林,就值得重新检查特征、数据质量、验证方法,以及额外复杂度是否真的产生了价值。

随机森林也因此成为值得长期保留在机器学习工具箱中的经典算法。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。