【机器学习】从单棵树到森林:用集成方法提升房地产预测
【精选优质专栏推荐】
- 《AI 技术前沿》 —— 紧跟 AI 最新趋势与应用
- 《网络安全新手快速入门(附漏洞挖掘案例)》 —— 零基础安全入门必看
- 《BurpSuite 入门教程(附实战图文)》 —— 渗透测试必备工具详解
- 《网安渗透工具使用教程(全)》 —— 一站式工具手册
- 《CTF 新手入门实战教程》 —— 从题目讲解到实战技巧
- 《前后端项目开发(新手必知必会)》 —— 实战驱动快速上手
每个专栏均配有案例与图文讲解,循序渐进,适合新手与进阶学习者,欢迎订阅。

前言
这篇文章深入探讨了树模型的应用,特别关注在 Ames Housing 数据集上的决策树、Bagging 和随机森林。文章首先强调了数据预处理的重要性,这是确保数据满足这些模型要求的关键步骤。从单棵决策树到强大的树集成模型的路径,展示了多棵树对预测性能的显著提升作用。在接下来的模型评估与优化中,我们将为你提供实用见解和高级策略,帮助你优化机器学习方法及房价预测。
树模型的数据预处理技巧
在任何数据科学工作流中,数据预处理都是至关重要的,尤其是处理树模型时。
我们将以 Ames Housing 数据集为例,演示具体的预处理步骤:
# 导入预处理所需库
import pandas as pd
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OrdinalEncoder, OneHotEncoder, FunctionTransformer
from sklearn.compose import ColumnTransformer
# 加载数据集
Ames = pd.read_csv('Ames.csv')
# 将以下数值型特征转换为类别型
Ames['MSSubClass'] = Ames['MSSubClass'].astype('object')
Ames['YrSold'] = Ames['YrSold'].astype('object')
Ames['MoSold'] = Ames['MoSold'].astype('object')
# 排除 'PID' 和 'SalePrice' 特征,并单独处理 'Electrical' 列
numeric_features = Ames.select_dtypes(include=['int64', 'float64']).drop(columns=['PID', 'SalePrice']).columns
categorical_features = Ames.select_dtypes(include=['object']).columns.difference(['Electrical'])
electrical_feature = ['Electrical']
# 根据数据字典手动指定序数编码顺序
ordinal_order = {
'Electrical': ['Mix', 'FuseP', 'FuseF', 'FuseA', 'SBrkr'],
'LotShape': ['IR3', 'IR2', 'IR1', 'Reg'],
'Utilities': ['ELO', 'NoSeWa', 'NoSewr', 'AllPub'],
'LandSlope': ['Sev', 'Mod', 'Gtl'],
'ExterQual': ['Po', 'Fa', 'TA', 'Gd', 'Ex'],
'ExterCond': ['Po', 'Fa', 'TA', 'Gd', 'Ex'],
'BsmtQual': ['None', 'Po', 'Fa', 'TA', 'Gd', 'Ex'],
'BsmtCond': ['None', 'Po', 'Fa', 'TA', 'Gd', 'Ex'],
'BsmtExposure': ['None', 'No', 'Mn', 'Av', 'Gd'],
'BsmtFinType1': ['None', 'Unf', 'LwQ', 'Rec', 'BLQ', 'ALQ', 'GLQ'],
'BsmtFinType2': ['None', 'Unf', 'LwQ', 'Rec', 'BLQ', 'ALQ', 'GLQ'],
'HeatingQC': ['Po', 'Fa', 'TA', 'Gd', 'Ex'],
'KitchenQual': ['Po', 'Fa', 'TA', 'Gd', 'Ex'],
'Functional': ['Sal', 'Sev', 'Maj2', 'Maj1', 'Mod', 'Min2', 'Min1', 'Typ'],
'FireplaceQu': ['None', 'Po', 'Fa', 'TA', 'Gd', 'Ex'],
'GarageFinish': ['None', 'Unf', 'RFn', 'Fin'],
'GarageQual': ['None', 'Po', 'Fa', 'TA', 'Gd', 'Ex'],
'GarageCond': ['None', 'Po', 'Fa', 'TA', 'Gd', 'Ex'],
'PavedDrive': ['N', 'P', 'Y'],
'PoolQC': ['None', 'Fa', 'TA', 'Gd', 'Ex'],
'Fence': ['None', 'MnWw', 'GdWo', 'MnPrv', 'GdPrv']
}
# 提取所有序数特征
ordinal_features = list(ordinal_order.keys())
ordinal_except_electrical = [feature for feature in ordinal_features if feature != 'Electrical']
# 填充缺失值的辅助函数
def fill_none(X):
return X.fillna("None")
# 'Electrical' 特征处理流水线:缺失值用众数填充,然后进行序数编码
electrical_transformer = Pipeline(steps=[
('impute_electrical', SimpleImputer(strategy='most_frequent')),
('ordinal_electrical', OrdinalEncoder(categories=[ordinal_order['Electrical']]))
])
# 数值特征处理流水线:缺失值用均值填充
numeric_transformer = Pipeline(steps=[
('impute_mean', SimpleImputer(strategy='mean'))
])
# 序数特征处理流水线:缺失值填充 'None',再进行序数编码
ordinal_transformer = Pipeline(steps=[
('fill_none', FunctionTransformer(fill_none, validate=False)),
('ordinal', OrdinalEncoder(categories=[ordinal_order[feature] for feature in ordinal_features if feature in ordinal_except_electrical]))
])
# 名义类别特征处理流水线:缺失值填充 'None',再进行一热编码
nominal_features = [feature for feature in categorical_features if feature not in ordinal_features]
categorical_transformer = Pipeline(steps=[
('fill_none', FunctionTransformer(fill_none, validate=False)),
('onehot', OneHotEncoder(handle_unknown='ignore'))
])
# 综合预处理器:同时处理数值、序数、名义类别及 'Electrical' 特征
preprocessor = ColumnTransformer(
transformers=[
('electrical', electrical_transformer, ['Electrical']),
('num', numeric_transformer, numeric_features),
('ordinal', ordinal_transformer, ordinal_except_electrical),
('nominal', categorical_transformer, nominal_features)
]
)
# 对 Ames 数据应用预处理流水线
transformed_data = preprocessor.fit_transform(Ames).toarray()
# 生成一热编码特征的列名
onehot_features = preprocessor.named_transformers_['nominal'].named_steps['onehot'].get_feature_names_out()
# 合并所有特征名
all_feature_names = ['Electrical'] + list(numeric_features) + list(ordinal_except_electrical) + list(onehot_features)
# 将转换后的数组转为 DataFrame
transformed_df = pd.DataFrame(transformed_data, columns=all_feature_names)
有了这些预处理步骤,我们的数据结构就更加规范化,能够合理地处理缺失值,并对类别特征进行适当编码。以下总结了我们完成的关键预处理任务,为后续建模打下坚实基础:
数据分类:
将 “MSSubClass”、“YrSold” 和 “MoSold” 从数值型转换为类别型,以符合其真实数据特性。
排除无关特征:
移除 “PID” 和 “SalePrice”,专注于预测特征,避免使用唯一标识符。
缺失值处理:
- 数值特征:用均值填充缺失值,以保持分布。
- 类别特征:除 “Electrical” 外,缺失值填充 “None”。
- Electrical 特征:用众数填充唯一缺失值。
类别数据编码:
- 序数特征:按照预定义顺序进行编码,保留数据固有的等级信息(如 “ExterQual” 从差到优)。
- 名义特征:使用一热编码,将其转换为适合建模的二值列。
流水线处理:
针对数值、序数和名义特征分别构建流水线,实现一致、可复用的数据转换。
综合预处理:
使用 ColumnTransformer 一步完成所有特征的转换,提高处理效率和可管理性。
应用与结果检查:
将流水线应用于数据集,转换后的数组再转为 DataFrame,并为一热编码特征生成系统化列名,便于分析。
观察上面转换后的 DataFrame,可以清楚地看到我们的预处理步骤如何改变了数据结构。这样的转换确保每个特征都被适当格式化,为下一步分析做好准备。注意每个类别和数值特征都得到了合理处理,从而保留了尽可能多的信息。
# 可选命令:显示所有列
# pd.set_option('display.max_columns', None)
# 查看转换结果
print(transformed_df)
输出示例(前几行):
Electrical GrLivArea LotFrontage ... YrSold_2008 YrSold_2009 YrSold_2010
0 4.0 856.0 68.510628 ... 0.0 0.0 1.0
1 4.0 1049.0 42.000000 ... 0.0 1.0 0.0
...
[2579 rows x 2819 columns]
原始数据集现在扩展到 2819 列。我们可以通过快速计算验证转换后的列数是否正确:
print(len(numeric_features) + len(ordinal_features) + Ames[nominal_features].fillna("None").nunique().sum())
输出:
2819
这个验证确保所有预处理都已正确应用。数据的完整性在此阶段至关重要,以便构建可靠的模型。
基础评估:决策树回归器
接下来,我们使用基础决策树模型来评估预测性能,构建在前面处理好的数据基础上:
from sklearn.tree import DecisionTreeRegressor
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import Pipeline
# 定义完整模型流水线
model_pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('regressor', DecisionTreeRegressor(random_state=42))
])
# 使用交叉验证评估模型
scores = cross_val_score(model_pipeline, Ames.drop(columns='SalePrice'), Ames['SalePrice'])
# 输出结果
print("Decision Tree Regressor Mean CV R²:", round(scores.mean(),4))
输出示例:
Decision Tree Regressor Mean CV R²: 0.7663
R² 得分为 0.7663,说明模型能够解释大约 77% 的房价变异,这是一个不错的基础表现,但仍有提升空间。这个基础性能为我们后续探索更复杂的集成方法提供了参考。
提升预测:使用 Bagging 的决策树
在初始模型基础上,我们可以通过 Bagging(自助聚合) 提高预测性能。
Bagging 是一种集成方法,通过减少方差和防止过拟合来提升模型稳定性和准确性。不同于简单地复制同一棵决策树,Bagging 会在数据集的不同自助样本(有放回抽样)上训练多棵树,每棵树都能从略有不同的数据切片中学习,从而确保模型的多样性。
下面对比单棵决策树与使用多棵树的 Bagging 回归器的效果:
from sklearn.ensemble import BaggingRegressor
models = {
'Decision Tree (1 Tree)': DecisionTreeRegressor(random_state=42),
'Bagging Regressor (10 Trees)': BaggingRegressor(
base_estimator=DecisionTreeRegressor(random_state=42),
n_estimators=10,
random_state=42
)
}
results = {}
for name, model in models.items():
model_pipeline = Pipeline([
('preprocessor', preprocessor),
('regressor', model)
])
scores = cross_val_score(model_pipeline, Ames.drop(columns='SalePrice'), Ames['SalePrice'])
results[name] = round(scores.mean(), 4)
print("Cross-validation scores:", results)
输出示例:
Cross-validation scores: {'Decision Tree (1 Tree)': 0.7663, 'Bagging Regressor (10 Trees)': 0.8781}
可以看到,通过 Bagging,多棵决策树相比单棵树性能提升约 11%,显示了集成方法提升模型性能的效果。
我们进一步探索 Bagging 模型在不同树数量下的性能表现:
n_trees = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
models = {'Decision Tree (1 Tree)': DecisionTreeRegressor(random_state=42)}
for n in n_trees:
models[f'Bagging Regressor {n} Trees'] = BaggingRegressor(
base_estimator=DecisionTreeRegressor(random_state=42),
n_estimators=n,
random_state=42
)
results = {}
for name, model in models.items():
model_pipeline = Pipeline([
('preprocessor', preprocessor),
('regressor', model)
])
scores = cross_val_score(model_pipeline, Ames.drop(columns='SalePrice'), Ames['SalePrice'])
results[name] = round(scores.mean(), 4)
print("Cross-validation scores:")
for name, score in results.items():
print(f"{name}: {score}")
随着 Bagging 树数量增加,我们可以观察到模型性能在初期有明显提升。
然而,需要注意的是,边际收益在超过某个节点后会逐渐趋于平缓。例如,从 1 棵树增加到 20 棵树时,R² 得分的提升显著,但超过 20 棵树后的增量改善就明显减小了。
交叉验证得分如下:
Decision Tree (1 Tree): 0.7663
Bagging Regressor 10 Trees: 0.8781
Bagging Regressor 20 Trees: 0.8898
Bagging Regressor 30 Trees: 0.8911
Bagging Regressor 40 Trees: 0.8922
Bagging Regressor 50 Trees: 0.8931
Bagging Regressor 60 Trees: 0.8933
Bagging Regressor 70 Trees: 0.8936
Bagging Regressor 80 Trees: 0.895
Bagging Regressor 90 Trees: 0.8954
Bagging Regressor 100 Trees: 0.8957
这一趋势体现了模型复杂度收益递减的规律,也提醒我们在机器学习中:超过某一复杂度后,额外的计算成本可能不值得换取微小的性能提升。
高级集成:Bagging 与随机森林的比较
在树模型系列的最后部分,我们对两种常见的集成方法进行比较:Bagging 回归器与随机森林。两者都基于集成学习的理念,但在树的构建与组合方式上有所不同。
随机森林是对 Bagging 的扩展。在训练过程中,它也会构建多棵决策树,但与普通 Bagging 不同的是,随机森林在每个节点拆分时只考虑特征的随机子集。这种随机性增加了树的多样性,通常能得到更具泛化能力的模型。
我们用 Ames Housing 数据集来比较这两种方法的表现,并观察树的数量增加时,交叉验证 R² 分数的变化:
from sklearn.ensemble import RandomForestRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import BaggingRegressor
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score
n_trees = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
models = {'Decision Tree (1 Tree)': DecisionTreeRegressor(random_state=42)}
for n in n_trees:
models[f'Bagging Regressor {n} Trees'] = BaggingRegressor(
base_estimator=DecisionTreeRegressor(random_state=42),
n_estimators=n,
random_state=42
)
models[f'Random Forest {n} Trees'] = RandomForestRegressor(
n_estimators=n,
random_state=42
)
results = {}
for name, model in models.items():
model_pipeline = Pipeline([
('preprocessor', preprocessor),
('regressor', model)
])
scores = cross_val_score(model_pipeline, Ames.drop(columns='SalePrice'), Ames['SalePrice'])
results[name] = round(scores.mean(), 4)
print("Cross-validation scores:")
for name, score in results.items():
print(f"{name}: {score}")
交叉验证得分示例:
Decision Tree (1 Tree): 0.7663
Bagging Regressor 10 Trees: 0.8781
Random Forest 10 Trees: 0.8762
Bagging Regressor 20 Trees: 0.8898
Random Forest 20 Trees: 0.8893
Bagging Regressor 30 Trees: 0.8911
Random Forest 30 Trees: 0.8897
Bagging Regressor 40 Trees: 0.8922
Random Forest 40 Trees: 0.8909
Bagging Regressor 50 Trees: 0.8931
Random Forest 50 Trees: 0.8922
Bagging Regressor 60 Trees: 0.8933
Random Forest 60 Trees: 0.8931
Bagging Regressor 70 Trees: 0.8936
Random Forest 70 Trees: 0.8932
Bagging Regressor 80 Trees: 0.895
Random Forest 80 Trees: 0.8943
Bagging Regressor 90 Trees: 0.8954
Random Forest 90 Trees: 0.8948
Bagging Regressor 100 Trees: 0.8957
Random Forest 100 Trees: 0.8954
从结果可以看到,随着树数量增加,Bagging 与随机森林的表现都明显优于单棵决策树,但两者在多数情况下性能接近,没有明显的持续优势。这种现象可能与 Ames Housing 数据集的特性相关:如果数据集中存在少量高预测力特征,随机森林的特征随机选择不会显著提升模型泛化能力,相比之下,使用所有特征的 Bagging 已能取得相似效果。
这些观察表明,尽管随机森林通常能通过特征随机化降低树间相关性从而提升性能,但数据集特性与具体问题场景可能限制其优势。因此,在计算资源有限的情况下,Bagging 因其简单性和接近的性能,可能是更优选择。这个比较也强调了在选择集成策略时,理解数据与建模目标的重要性。
总结
本文深入探讨了基于树的建模技术,使用 Ames Housing 数据集作为示例。从基础的数据预处理开始,包括类别转换、缺失值处理和编码方法,然后逐步评估并改进决策树模型,最终引入 Bagging 和随机森林的集成方法进行比较分析。文章通过实例演示了随着树的数量变化,模型性能的增量改善与差异,为读者提供了完整的树模型预测建模理解框架。
更多推荐



所有评论(0)