偏依赖和个体条件期望图#

偏依赖图显示目标函数[2]与一组感兴趣的特征之间的依赖关系,对所有其他特征(补充特征)的值进行边缘化。由于人类感知的局限性,感兴趣的特征集的大小必须很小(通常是一个或两个),因此它们通常在最重要的特征中选择。

类似地,个体条件期望 (ICE) 图[3]显示目标函数与感兴趣特征之间的依赖关系。但是,与显示感兴趣特征平均效应的偏依赖图不同,ICE 图分别为每个样本显示预测对特征的依赖性,每个样本一条线。ICE 图仅支持一个感兴趣的特征。

本示例演示如何从在自行车共享数据集上训练的MLPRegressorHistGradientBoostingRegressor获得偏依赖和 ICE 图。本示例的灵感来自[1]

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

自行车共享数据集预处理#

我们将使用自行车共享数据集。目标是使用天气和季节数据以及日期时间信息来预测自行车租赁数量。

from sklearn.datasets import fetch_openml

bikes = fetch_openml("Bike_Sharing_Demand", version=2, as_frame=True)
# Make an explicit copy to avoid "SettingWithCopyWarning" from pandas
X, y = bikes.data.copy(), bikes.target

# We use only a subset of the data to speed up the example.
X = X.iloc[::5, :]
y = y[::5]

特征"weather"有一个特性:类别"heavy_rain"是一个罕见类别。

X["weather"].value_counts()
weather
clear         2284
misty          904
rain           287
heavy_rain       1
Name: count, dtype: int64

由于这个罕见类别,我们将它合并到"rain"中。

X["weather"] = (
    X["weather"]
    .astype(object)
    .replace(to_replace="heavy_rain", value="rain")
    .astype("category")
)

我们现在仔细看看"year"特征

X["year"].value_counts()
year
1    1747
0    1729
Name: count, dtype: int64

我们看到我们有两年的数据。我们使用第一年训练模型,第二年测试模型。

mask_training = X["year"] == 0.0
X = X.drop(columns=["year"])
X_train, y_train = X[mask_training], y[mask_training]
X_test, y_test = X[~mask_training], y[~mask_training]

我们可以检查数据集信息,看看我们是否有异构数据类型。我们必须相应地预处理不同的列。

X_train.info()
<class 'pandas.core.frame.DataFrame'>
Index: 1729 entries, 0 to 8640
Data columns (total 11 columns):
 #   Column      Non-Null Count  Dtype
---  ------      --------------  -----
 0   season      1729 non-null   category
 1   month       1729 non-null   int64
 2   hour        1729 non-null   int64
 3   holiday     1729 non-null   category
 4   weekday     1729 non-null   int64
 5   workingday  1729 non-null   category
 6   weather     1729 non-null   category
 7   temp        1729 non-null   float64
 8   feel_temp   1729 non-null   float64
 9   humidity    1729 non-null   float64
 10  windspeed   1729 non-null   float64
dtypes: category(4), float64(4), int64(3)
memory usage: 115.4 KB

根据前面的信息,我们将category列视为名义类别特征。此外,我们也将日期和时间信息视为类别特征。

我们手动定义包含数值和类别特征的列。

numerical_features = [
    "temp",
    "feel_temp",
    "humidity",
    "windspeed",
]
categorical_features = X_train.columns.drop(numerical_features)

在我们详细介绍不同机器学习管道的预处理之前,我们将尝试获得有关数据集的一些额外直觉,这将有助于理解模型的统计性能和偏依赖分析的结果。

我们通过按季节和年份对数据进行分组来绘制平均自行车租赁数量。

from itertools import product

import matplotlib.pyplot as plt
import numpy as np

days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")
hours = tuple(range(24))
xticklabels = [f"{day}\n{hour}:00" for day, hour in product(days, hours)]
xtick_start, xtick_period = 6, 12

fig, axs = plt.subplots(nrows=2, figsize=(8, 6), sharey=True, sharex=True)
average_bike_rentals = bikes.frame.groupby(
    ["year", "season", "weekday", "hour"], observed=True
).mean(numeric_only=True)["count"]
for ax, (idx, df) in zip(axs, average_bike_rentals.groupby("year")):
    df.groupby("season", observed=True).plot(ax=ax, legend=True)

    # decorate the plot
    ax.set_xticks(
        np.linspace(
            start=xtick_start,
            stop=len(xticklabels),
            num=len(xticklabels) // xtick_period,
        )
    )
    ax.set_xticklabels(xticklabels[xtick_start::xtick_period])
    ax.set_xlabel("")
    ax.set_ylabel("Average number of bike rentals")
    ax.set_title(
        f"Bike rental for {'2010 (train set)' if idx == 0.0 else '2011 (test set)'}"
    )
    ax.set_ylim(0, 1_000)
    ax.set_xlim(0, len(xticklabels))
    ax.legend(loc=2)
Bike rental for 2010 (train set), Bike rental for 2011 (test set)

训练集和测试集之间第一个显著差异是测试集中的自行车租赁数量更高。因此,获得低估自行车租赁数量的机器学习模型不足为奇。我们还观察到,春季的自行车租赁数量较低。此外,我们看到在工作日,上午 6-7 点和下午 5-6 点左右会出现特定的模式,自行车租赁量会出现一些峰值。我们可以记住这些不同的见解,并用它们来理解偏依赖图。

机器学习模型的预处理器#

由于我们稍后使用两种不同的模型,MLPRegressorHistGradientBoostingRegressor,我们为每个模型创建两个不同的预处理器。

神经网络模型的预处理器#

我们将使用QuantileTransformer来缩放数值特征,并使用OneHotEncoder对类别特征进行编码。

from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, QuantileTransformer

mlp_preprocessor = ColumnTransformer(
    transformers=[
        ("num", QuantileTransformer(n_quantiles=100), numerical_features),
        ("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features),
    ]
)
mlp_preprocessor
ColumnTransformer(transformers=[('num', QuantileTransformer(n_quantiles=100),
                                 ['temp', 'feel_temp', 'humidity',
                                  'windspeed']),
                                ('cat', OneHotEncoder(handle_unknown='ignore'),
                                 Index(['season', 'month', 'hour', 'holiday', 'weekday', 'workingday',
       'weather'],
      dtype='object'))])
在 Jupyter 环境中,请重新运行此单元格以显示 HTML 表示形式或信任笔记本。
在 GitHub 上,HTML 表示形式无法呈现,请尝试使用 nbviewer.org 加载此页面。


梯度提升模型的预处理器#

对于梯度提升模型,我们将数值特征保持原样,只使用OrdinalEncoder对类别特征进行编码。

from sklearn.preprocessing import OrdinalEncoder

hgbdt_preprocessor = ColumnTransformer(
    transformers=[
        ("cat", OrdinalEncoder(), categorical_features),
        ("num", "passthrough", numerical_features),
    ],
    sparse_threshold=1,
    verbose_feature_names_out=False,
).set_output(transform="pandas")
hgbdt_preprocessor
ColumnTransformer(sparse_threshold=1,
                  transformers=[('cat', OrdinalEncoder(),
                                 Index(['season', 'month', 'hour', 'holiday', 'weekday', 'workingday',
       'weather'],
      dtype='object')),
                                ('num', 'passthrough',
                                 ['temp', 'feel_temp', 'humidity',
                                  'windspeed'])],
                  verbose_feature_names_out=False)
在 Jupyter 环境中,请重新运行此单元格以显示 HTML 表示形式或信任笔记本。
在 GitHub 上,HTML 表示形式无法呈现,请尝试使用 nbviewer.org 加载此页面。


使用不同模型的一维偏依赖#

在本节中,我们将使用两种不同的机器学习模型计算单变量偏依赖: (i) 多层感知器和 (ii) 梯度提升模型。通过这两个模型,我们将演示如何计算和解释数值和类别特征的偏依赖图 (PDP) 和个体条件期望 (ICE)。

多层感知器#

让我们拟合一个MLPRegressor并计算单变量偏依赖图。

from time import time

from sklearn.neural_network import MLPRegressor
from sklearn.pipeline import make_pipeline

print("Training MLPRegressor...")
tic = time()
mlp_model = make_pipeline(
    mlp_preprocessor,
    MLPRegressor(
        hidden_layer_sizes=(30, 15),
        learning_rate_init=0.01,
        early_stopping=True,
        random_state=0,
    ),
)
mlp_model.fit(X_train, y_train)
print(f"done in {time() - tic:.3f}s")
print(f"Test R2 score: {mlp_model.score(X_test, y_test):.2f}")
Training MLPRegressor...
done in 0.679s
Test R2 score: 0.61

我们使用专门为神经网络创建的预处理器配置了一个管道,并调整了神经网络的大小和学习率,以在训练时间和测试集上的预测性能之间取得合理的平衡。

重要的是,这个表格数据集的特征具有非常不同的动态范围。神经网络往往对具有不同尺度的特征非常敏感,忘记预处理数值特征会导致模型性能非常差。

可以使用更大的神经网络获得更高的预测性能,但训练成本也会显著增加。

请注意,在绘制偏依赖图之前,务必检查模型在测试集上的准确性是否足够,因为解释给定特征对预测性能差的模型预测函数的影响几乎没有意义。在这方面,我们的MLP模型运行良好。

我们将绘制平均偏依赖。

import matplotlib.pyplot as plt

from sklearn.inspection import PartialDependenceDisplay

common_params = {
    "subsample": 50,
    "n_jobs": 2,
    "grid_resolution": 20,
    "random_state": 0,
}

print("Computing partial dependence plots...")
features_info = {
    # features of interest
    "features": ["temp", "humidity", "windspeed", "season", "weather", "hour"],
    # type of partial dependence plot
    "kind": "average",
    # information regarding categorical features
    "categorical_features": categorical_features,
}
tic = time()
_, ax = plt.subplots(ncols=3, nrows=2, figsize=(9, 8), constrained_layout=True)
display = PartialDependenceDisplay.from_estimator(
    mlp_model,
    X_train,
    **features_info,
    ax=ax,
    **common_params,
)
print(f"done in {time() - tic:.3f}s")
_ = display.figure_.suptitle(
    (
        "Partial dependence of the number of bike rentals\n"
        "for the bike rental dataset with an MLPRegressor"
    ),
    fontsize=16,
)
Partial dependence of the number of bike rentals for the bike rental dataset with an MLPRegressor
Computing partial dependence plots...
done in 0.649s

梯度提升#

现在让我们拟合一个HistGradientBoostingRegressor,并对相同的特征计算偏依赖。我们还使用为此模型创建的特定预处理器。

from sklearn.ensemble import HistGradientBoostingRegressor

print("Training HistGradientBoostingRegressor...")
tic = time()
hgbdt_model = make_pipeline(
    hgbdt_preprocessor,
    HistGradientBoostingRegressor(
        categorical_features=categorical_features,
        random_state=0,
        max_iter=50,
    ),
)
hgbdt_model.fit(X_train, y_train)
print(f"done in {time() - tic:.3f}s")
print(f"Test R2 score: {hgbdt_model.score(X_test, y_test):.2f}")
Training HistGradientBoostingRegressor...
done in 0.128s
Test R2 score: 0.62

在这里,我们使用了梯度提升模型的默认超参数,无需任何预处理,因为基于树的模型天生就对数值特征的单调变换具有鲁棒性。

请注意,在这个表格数据集中,梯度提升机比神经网络的训练速度快得多,而且准确性也更高。调整它们的超参数也便宜得多(默认值往往效果很好,而神经网络通常并非如此)。

我们将绘制一些数值和类别特征的偏依赖图。

print("Computing partial dependence plots...")
tic = time()
_, ax = plt.subplots(ncols=3, nrows=2, figsize=(9, 8), constrained_layout=True)
display = PartialDependenceDisplay.from_estimator(
    hgbdt_model,
    X_train,
    **features_info,
    ax=ax,
    **common_params,
)
print(f"done in {time() - tic:.3f}s")
_ = display.figure_.suptitle(
    (
        "Partial dependence of the number of bike rentals\n"
        "for the bike rental dataset with a gradient boosting"
    ),
    fontsize=16,
)
Partial dependence of the number of bike rentals for the bike rental dataset with a gradient boosting
Computing partial dependence plots...
done in 1.218s

图的分析#

我们将首先查看数值特征的PDP。对于这两个模型,温度的PDP总体趋势是自行车租赁数量随着温度的升高而增加。我们可以进行类似的分析,但湿度特征的趋势相反。湿度增加时,自行车租赁数量减少。最后,我们看到风速特征也存在同样的趋势。对于这两个模型,风速增加时,自行车租赁数量减少。我们还观察到MLPRegressor的预测比HistGradientBoostingRegressor平滑得多。

现在,我们将查看类别特征的偏依赖图。

我们观察到,对于季节特征,春季是最低的。对于天气特征,降雨类别是最低的。关于小时特征,我们在早上7点和下午6点左右看到两个峰值。这些发现与我们之前对数据集的观察结果一致。

但是,值得注意的是,如果特征之间存在相关性,我们可能会创建潜在的无意义的合成样本。

ICE 与 PDP#

PDP是特征边际效应的平均值。我们对提供的数据集的所有样本的响应进行平均。因此,一些影响可能会被隐藏。在这方面,可以绘制每个单独的响应。这种表示称为个体效应图 (ICE)。在下图中,我们绘制了温度和湿度特征的50个随机选择的ICE。

print("Computing partial dependence plots and individual conditional expectation...")
tic = time()
_, ax = plt.subplots(ncols=2, figsize=(6, 4), sharey=True, constrained_layout=True)

features_info = {
    "features": ["temp", "humidity"],
    "kind": "both",
    "centered": True,
}

display = PartialDependenceDisplay.from_estimator(
    hgbdt_model,
    X_train,
    **features_info,
    ax=ax,
    **common_params,
)
print(f"done in {time() - tic:.3f}s")
_ = display.figure_.suptitle("ICE and PDP representations", fontsize=16)
ICE and PDP representations
Computing partial dependence plots and individual conditional expectation...
done in 0.507s

我们看到,温度特征的ICE为我们提供了一些额外的信息:一些ICE线是平的,而另一些则显示出在温度高于35摄氏度时依赖性下降。我们观察到湿度特征也存在类似的模式:当湿度高于80%时,一些ICE线急剧下降。

并非所有ICE线都是平行的,这表明模型发现了特征之间的交互作用。我们可以通过使用参数interaction_cst约束梯度提升模型不使用特征之间的任何交互作用来重复实验。

from sklearn.base import clone

interaction_cst = [[i] for i in range(X_train.shape[1])]
hgbdt_model_without_interactions = (
    clone(hgbdt_model)
    .set_params(histgradientboostingregressor__interaction_cst=interaction_cst)
    .fit(X_train, y_train)
)
print(f"Test R2 score: {hgbdt_model_without_interactions.score(X_test, y_test):.2f}")
Test R2 score: 0.38
_, ax = plt.subplots(ncols=2, figsize=(6, 4), sharey=True, constrained_layout=True)

features_info["centered"] = False
display = PartialDependenceDisplay.from_estimator(
    hgbdt_model_without_interactions,
    X_train,
    **features_info,
    ax=ax,
    **common_params,
)
_ = display.figure_.suptitle("ICE and PDP representations", fontsize=16)
ICE and PDP representations

二维交互图#

具有两个感兴趣特征的PDP使我们能够可视化它们之间的交互作用。但是,ICE不能以简单的方式绘制,因此难以解释。我们将展示from_estimator中提供的表示,这是一个二维热图。

print("Computing partial dependence plots...")
features_info = {
    "features": ["temp", "humidity", ("temp", "humidity")],
    "kind": "average",
}
_, ax = plt.subplots(ncols=3, figsize=(10, 4), constrained_layout=True)
tic = time()
display = PartialDependenceDisplay.from_estimator(
    hgbdt_model,
    X_train,
    **features_info,
    ax=ax,
    **common_params,
)
print(f"done in {time() - tic:.3f}s")
_ = display.figure_.suptitle(
    "1-way vs 2-way of numerical PDP using gradient boosting", fontsize=16
)
1-way vs 2-way of numerical PDP using gradient boosting
Computing partial dependence plots...
done in 8.160s

双变量偏依赖图显示了自行车租赁数量对温度和湿度的联合值的依赖性。我们清楚地看到了这两个特征之间的相互作用。对于高于20摄氏度的温度,湿度对自行车租赁数量的影响似乎与温度无关。

另一方面,对于低于20摄氏度的温度,温度和湿度都会持续影响自行车租赁数量。

此外,20摄氏度阈值的影响脊的斜率很大程度上取决于湿度水平:在干燥条件下脊很陡峭,但在湿度高于70%的潮湿条件下则平缓得多。

现在,我们将这些结果与为约束模型计算的相同图进行对比,该模型被约束为学习一个不依赖于这种非线性特征交互的预测函数。

print("Computing partial dependence plots...")
features_info = {
    "features": ["temp", "humidity", ("temp", "humidity")],
    "kind": "average",
}
_, ax = plt.subplots(ncols=3, figsize=(10, 4), constrained_layout=True)
tic = time()
display = PartialDependenceDisplay.from_estimator(
    hgbdt_model_without_interactions,
    X_train,
    **features_info,
    ax=ax,
    **common_params,
)
print(f"done in {time() - tic:.3f}s")
_ = display.figure_.suptitle(
    "1-way vs 2-way of numerical PDP using gradient boosting", fontsize=16
)
1-way vs 2-way of numerical PDP using gradient boosting
Computing partial dependence plots...
done in 7.703s

对于被约束为不建模特征交互的模型,一维偏依赖图显示每个特征单独存在局部峰值,特别是对于“湿度”特征。这些峰值可能反映了模型的退化行为,该模型试图通过过度拟合特定训练点来补偿被禁止的交互作用。请注意,根据测试集测量的此模型的预测性能明显低于原始的、不受约束的模型。

另请注意,在这些图上可见的局部峰值的数量取决于PD图本身的网格分辨率参数。

局部峰值导致二维偏依赖 (PD) 图呈现出噪声较大的网格状。由于湿度特征存在高频振荡,很难判断这些特征之间是否存在相互作用。然而,可以清楚地看到,当温度超过20度边界时观察到的简单交互效应在这个模型中不再可见。

类别特征之间的偏依赖性将提供一个离散的表示,可以以热图的形式显示。例如,季节、天气和目标变量之间的交互作用如下所示:

print("Computing partial dependence plots...")
features_info = {
    "features": ["season", "weather", ("season", "weather")],
    "kind": "average",
    "categorical_features": categorical_features,
}
_, ax = plt.subplots(ncols=3, figsize=(14, 6), constrained_layout=True)
tic = time()
display = PartialDependenceDisplay.from_estimator(
    hgbdt_model,
    X_train,
    **features_info,
    ax=ax,
    **common_params,
)

print(f"done in {time() - tic:.3f}s")
_ = display.figure_.suptitle(
    "1-way vs 2-way PDP of categorical features using gradient boosting", fontsize=16
)
1-way vs 2-way PDP of categorical features using gradient boosting
Computing partial dependence plots...
done in 0.732s

三维表示#

让我们针对两个特征的交互作用绘制相同的偏依赖图,这次使用三维表示。

# unused but required import for doing 3d projections with matplotlib < 3.2
import mpl_toolkits.mplot3d  # noqa: F401
import numpy as np

from sklearn.inspection import partial_dependence

fig = plt.figure(figsize=(5.5, 5))

features = ("temp", "humidity")
pdp = partial_dependence(
    hgbdt_model, X_train, features=features, kind="average", grid_resolution=10
)
XX, YY = np.meshgrid(pdp["grid_values"][0], pdp["grid_values"][1])
Z = pdp.average[0].T
ax = fig.add_subplot(projection="3d")
fig.add_axes(ax)

surf = ax.plot_surface(XX, YY, Z, rstride=1, cstride=1, cmap=plt.cm.BuPu, edgecolor="k")
ax.set_xlabel(features[0])
ax.set_ylabel(features[1])
fig.suptitle(
    "PD of number of bike rentals on\nthe temperature and humidity GBDT model",
    fontsize=16,
)
# pretty init view
ax.view_init(elev=22, azim=122)
clb = plt.colorbar(surf, pad=0.08, shrink=0.6, aspect=10)
clb.ax.set_title("Partial\ndependence")
plt.show()
PD of number of bike rentals on the temperature and humidity GBDT model, Partial dependence

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

相关示例

使用部分依赖进行高级绘图

使用部分依赖进行高级绘图

时间相关的特征工程

时间相关的特征工程

scikit-learn 0.24 发行亮点

scikit-learn 0.24 发行亮点

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

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

由 Sphinx-Gallery 生成的图库