比较目标编码器与其他编码器#

TargetEncoder 使用目标值来编码每个分类特征。在本例中,我们将比较三种不同的处理分类特征的方法:TargetEncoderOrdinalEncoderOneHotEncoder 和删除分类特征。

注意

fit(X, y).transform(X) 不等于 fit_transform(X, y),因为 fit_transform 中使用了交叉拟合方案进行编码。有关详细信息,请参阅用户指南

# Authors: The scikit-learn developers
# SPDX-License-Identifier: BSD-3-Clause

从 OpenML 加载数据#

首先,我们加载葡萄酒评论数据集,其中目标是评论者给出的分数。

from sklearn.datasets import fetch_openml

wine_reviews = fetch_openml(data_id=42074, as_frame=True)

df = wine_reviews.frame
df.head()
国家 描述 名称 分数 价格 省份 地区_1 地区_2 品种 酒庄
0 美国 这款卓越的 100% 单一品种葡萄酒源自... 玛莎葡萄园 96 235.0 加利福尼亚 纳帕谷 纳帕 赤霞珠 海茨
1 西班牙 成熟的无花果、黑莓和黑醋栗香气... Carodorum Selección Especial Reserva 96 110.0 西班牙北部 托罗 NaN Tinta de Toro Bodega Carmen Rodríguez
2 美国 Mac Watson 纪念一款曾经酿造的葡萄酒... 精选晚收 96 90.0 加利福尼亚 骑士谷 索诺玛 长相思 马考利
3 美国 这款酒在 30% 新法国橡木桶中陈酿了 20 个月... 珍藏 96 65.0 俄勒冈 威拉米特谷 威拉米特谷 黑皮诺 Ponzi
4 法国 这是 La Bégude 的顶级葡萄酒,以... La Brûlade 95 66.0 普罗旺斯 邦多勒 NaN 普罗旺斯红混酿 Domaine de la Bégude


在本例中,我们使用数据中数值和分类特征的以下子集。目标是 80 到 100 的连续值。

numerical_features = ["price"]
categorical_features = [
    "country",
    "province",
    "region_1",
    "region_2",
    "variety",
    "winery",
]
target_name = "points"

X = df[numerical_features + categorical_features]
y = df[target_name]

_ = y.hist()
plot target encoder

使用不同编码器训练和评估管道#

在本节中,我们将评估使用不同编码策略的 HistGradientBoostingRegressor 管道。首先,我们列出将用于预处理分类特征的编码器。

from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder, TargetEncoder

categorical_preprocessors = [
    ("drop", "drop"),
    ("ordinal", OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-1)),
    (
        "one_hot",
        OneHotEncoder(handle_unknown="ignore", max_categories=20, sparse_output=False),
    ),
    ("target", TargetEncoder(target_type="continuous")),
]

接下来,我们使用交叉验证评估模型并记录结果。

from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.model_selection import cross_validate
from sklearn.pipeline import make_pipeline

n_cv_folds = 3
max_iter = 20
results = []


def evaluate_model_and_store(name, pipe):
    result = cross_validate(
        pipe,
        X,
        y,
        scoring="neg_root_mean_squared_error",
        cv=n_cv_folds,
        return_train_score=True,
    )
    rmse_test_score = -result["test_score"]
    rmse_train_score = -result["train_score"]
    results.append(
        {
            "preprocessor": name,
            "rmse_test_mean": rmse_test_score.mean(),
            "rmse_test_std": rmse_train_score.std(),
            "rmse_train_mean": rmse_train_score.mean(),
            "rmse_train_std": rmse_train_score.std(),
        }
    )


for name, categorical_preprocessor in categorical_preprocessors:
    preprocessor = ColumnTransformer(
        [
            ("numerical", "passthrough", numerical_features),
            ("categorical", categorical_preprocessor, categorical_features),
        ]
    )
    pipe = make_pipeline(
        preprocessor, HistGradientBoostingRegressor(random_state=0, max_iter=max_iter)
    )
    evaluate_model_and_store(name, pipe)

原生分类特征支持#

在本节中,我们构建并评估一个管道,该管道使用 HistGradientBoostingRegressor 中的原生分类特征支持,它只支持最多 255 个唯一类别。在我们的数据集中,大多数分类特征具有超过 255 个唯一类别。

n_unique_categories = df[categorical_features].nunique().sort_values(ascending=False)
n_unique_categories
winery      14810
region_1     1236
variety       632
province      455
country        48
region_2       18
dtype: int64

为了解决上述限制,我们将分类特征分为低基数特征和高基数特征。高基数特征将进行目标编码,而低基数特征将使用梯度提升中的原生分类特征。

high_cardinality_features = n_unique_categories[n_unique_categories > 255].index
low_cardinality_features = n_unique_categories[n_unique_categories <= 255].index
mixed_encoded_preprocessor = ColumnTransformer(
    [
        ("numerical", "passthrough", numerical_features),
        (
            "high_cardinality",
            TargetEncoder(target_type="continuous"),
            high_cardinality_features,
        ),
        (
            "low_cardinality",
            OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-1),
            low_cardinality_features,
        ),
    ],
    verbose_feature_names_out=False,
)

# The output of the of the preprocessor must be set to pandas so the
# gradient boosting model can detect the low cardinality features.
mixed_encoded_preprocessor.set_output(transform="pandas")
mixed_pipe = make_pipeline(
    mixed_encoded_preprocessor,
    HistGradientBoostingRegressor(
        random_state=0, max_iter=max_iter, categorical_features=low_cardinality_features
    ),
)
mixed_pipe
Pipeline(steps=[('columntransformer',
                 ColumnTransformer(transformers=[('numerical', 'passthrough',
                                                  ['price']),
                                                 ('high_cardinality',
                                                  TargetEncoder(target_type='continuous'),
                                                  Index(['winery', 'region_1', 'variety', 'province'], dtype='object')),
                                                 ('low_cardinality',
                                                  OrdinalEncoder(handle_unknown='use_encoded_value',
                                                                 unknown_value=-1),
                                                  Index(['country', 'region_2'], dtype='object'))],
                                   verbose_feature_names_out=False)),
                ('histgradientboostingregressor',
                 HistGradientBoostingRegressor(categorical_features=Index(['country', 'region_2'], dtype='object'),
                                               max_iter=20, random_state=0))])
在 Jupyter 环境中,请重新运行此单元格以显示 HTML 表示形式或信任 notebook。
在 GitHub 上,HTML 表示形式无法渲染,请尝试使用 nbviewer.org 加载此页面。


最后,我们使用交叉验证评估管道并记录结果。

evaluate_model_and_store("mixed_target", mixed_pipe)

绘制结果#

在本节中,我们通过绘制测试分数和训练分数来显示结果。

import matplotlib.pyplot as plt
import pandas as pd

results_df = (
    pd.DataFrame(results).set_index("preprocessor").sort_values("rmse_test_mean")
)

fig, (ax1, ax2) = plt.subplots(
    1, 2, figsize=(12, 8), sharey=True, constrained_layout=True
)
xticks = range(len(results_df))
name_to_color = dict(
    zip((r["preprocessor"] for r in results), ["C0", "C1", "C2", "C3", "C4"])
)

for subset, ax in zip(["test", "train"], [ax1, ax2]):
    mean, std = f"rmse_{subset}_mean", f"rmse_{subset}_std"
    data = results_df[[mean, std]].sort_values(mean)
    ax.bar(
        x=xticks,
        height=data[mean],
        yerr=data[std],
        width=0.9,
        color=[name_to_color[name] for name in data.index],
    )
    ax.set(
        title=f"RMSE ({subset.title()})",
        xlabel="Encoding Scheme",
        xticks=xticks,
        xticklabels=data.index,
    )
RMSE (Test), RMSE (Train)

在评估测试集上的预测性能时,删除类别表现最差,而目标编码器表现最好。这可以解释如下:

  • 删除分类特征使得管道的表达能力较差,导致欠拟合;

  • 由于高基数且为了减少训练时间,one-hot 编码方案使用 max_categories=20,这会阻止特征扩展太多,可能导致欠拟合。

  • 如果我们没有设置 max_categories=20,one-hot 编码方案很可能会导致管道过拟合,因为特征数量会随着罕见类别出现而爆炸,这些罕见类别偶然与目标相关(仅在训练集上);

  • 序数编码对特征施加了任意顺序,然后 HistGradientBoostingRegressor 将其视为数值。由于此模型将数值特征分组为每特征 256 个分箱,许多不相关的类别可能会被组合在一起,从而导致整个管道欠拟合;

  • 使用目标编码器时,会发生相同的分箱,但由于编码值在统计上按与目标变量的边际关联进行排序,HistGradientBoostingRegressor 使用的分箱是有意义的,并带来良好的结果:平滑目标编码和分箱的结合作为一种良好的正则化策略,既能防止过拟合,又不过度限制管道的表达能力。

脚本总运行时间: (0 分钟 21.022 秒)

相关示例

梯度提升中的分类特征支持

梯度提升中的分类特征支持

目标编码器的内部交叉拟合

目标编码器的内部交叉拟合

具有混合类型的列转换器

具有混合类型的列转换器

scikit-learn 1.4 发布亮点

scikit-learn 1.4 发布亮点

由 Sphinx-Gallery 生成的图库