【精选优质专栏推荐】


每个专栏均配有案例与图文讲解,循序渐进,适合新手与进阶学习者,欢迎订阅。

在这里插入图片描述

前言

这篇文章深入探讨了树模型的应用,特别关注在 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 和随机森林的集成方法进行比较分析。文章通过实例演示了随着树的数量变化,模型性能的增量改善与差异,为读者提供了完整的树模型预测建模理解框架。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐