使用 KFold 交叉验证进行目标编码 - 如何转换测试集?

数据挖掘 分类编码 目标编码
2021-09-18 10:39:44

假设我有一个分类特征 ( cat):

import random
import pandas as pd
from sklearn.model_selection import train_test_split, StratifiedKFold

random.seed(1234)
y = random.choices([1, 0], weights=[0.2, 0.8], k=100)
cat = random.choices(["A", "B", "C"], k=100)
df = pd.DataFrame.from_dict({"y": y, "cat": cat})

我想使用目标编码和使用 CV 的正则化,如下所示:

X_train, X_test, y_train, y_test = train_test_split(df[["cat"]], df["y"], train_size=0.8, random_state=42)
df_train = pd.concat([X_train, y_train], axis=1).sort_index()
df_train["kfold"] = -1
idx = df_train.index
df_train = df_train.sample(frac=1)

skf = StratifiedKFold(n_splits=5)
for fold_id, (train_id, val_id) in enumerate(skf.split(X=df_train.drop("y", axis=1), y=df_train["y"])):
    df_train.iloc[val_id, df_train.columns.get_loc("kfold")] = fold_id

df_train = df_train.loc[idx]

encoded_dfs = []

for fold in df_train["kfold"].unique():
    df_train_cv = df_train[df_train["kfold"] != fold].copy()
    df_val_cv = df_train[df_train["kfold"] == fold].copy()

    means = df_train_cv.groupby('cat')['y'].mean()
    df_val_cv['cat'] = df_val_cv['cat'].map(means)
    encoded_dfs.append(df_val_cv)

encoded_dfs = pd.concat(encoded_dfs, axis=0).sort_index()
encoded_dfs.drop('kfold', axis=1, inplace=True)

但是,我对如何编码测试集的方式有一些疑问。由于没有从训练集推导出单个映射,我认为我们应该使用整个训练集来拟合编码,然后在测试集上使用它:

means = df_train.groupby('cat')['y'].mean()
X_test['cat'] = X_test['cat'].map(means)

这似乎是一种自然的方式,事实上,这正是 CV 步骤所模仿的。但是我得到的模型的结果是关闭的,这让我思考我是否遗漏了什么。请注意,为了简单起见,我也省略了额外的平滑。因此,我的问题是:它是编码测试集的正确方法吗?

1个回答

我对如何编码测试集的方式有一些疑问。由于没有从训练集中推导出单个映射,我认为我们应该使用整个训练集来拟合编码,然后在测试集上使用它

是的,这看起来很好,他们在那里做的方式比使用管道要复杂一些。拆分为训练和测试的想法是模仿模型在生产/看不见的数据中的行为方式。使用测试进行目标编码,是在进行数据泄漏,并获得模型在生产中的行为方式的错误表示。因此,您在 train 中获得目标值,然后进行测试。

如果你这样做,然后你有一个看不见的测试类别,它将通过一个错误。如果你看看类别编码器的目标编码库,你可以处理这个:

handle_missing:str选项有'error'、'return_nan'和'value',默认为'value',返回目标均值。

你可以用不同的方式处理它,最好的取决于你的问题。默认是返回目标均值。

他们的最佳实践是创建一个管道,其中目标编码是一个步骤(变压器)。这将允许您进行 CV、在测试中评估您的模型以及许多其他功能。这里有一个关于如何做的教程

一段代码:

import random
import pandas as pd
from sklearn.model_selection import train_test_split, StratifiedKFold
from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV
from category_encoders.target_encoder import TargetEncoder
from category_encoders.m_estimate import MEstimateEncoder
from sklearn.linear_model import ElasticNet,LogisticRegression

random.seed(1234)
y = random.choices([1, 0], weights=[0.2, 0.8], k=100)
cat = random.choices(["A", "B", "C"], k=100)
df = pd.DataFrame.from_dict({"y": y, "cat": cat})

X_train, X_test, y_train, y_test = train_test_split(df[["cat"]], df["y"], train_size=0.8, random_state=42)
skf = StratifiedKFold(n_splits=5)


clf = LogisticRegression()
te = TargetEncoder()

pipe = Pipeline(
        [
         ("te", te),
          ("clf", clf),
        ]
    )


#Grid to serch for the hyper parameters
pipe_grid = {
    "te__smoothing": [0.0001],
    }


# Instantiate the grid
pipe_cv = GridSearchCV(
        pipe,
        param_grid=pipe_grid,
        n_jobs=-1,
        cv=skf,
    )

pipe_cv.fit(X_train, y_train)

# Add some unseen category to the test.
X_test['cat'] = 'UUUUU'

pipe_cv.predict(X_test)

请注意,代码不是最优的,但它应该向您展示如何处理这个问题,即使用训练进行目标编码并使用管道进行测试,以及使用看不见的数据:)

请注意,类别是随机分配的。所以模型检测到最好的是预测最频繁的类别。如果您更改 ElasticNet(回归量),您将得到平均值。

如果您取出看不见的类别分配进行测试,您仍然会得到相同的结果