Примечание
Перейти к концу для скачивания полного примера кода. Или запустить этот пример в вашем браузере через JupyterLite или Binder
Графики частичной зависимости и индивидуальных условных ожиданий
Графики частичной зависимости показывают зависимость целевой функции [2] и набора интересных признаков, учитывая значения всех других признаков (дополнительные признаки). Из-за ограничений восприятия человека размер набора интересных признаков должен быть небольшим (обычно один или два), поэтому они обычно выбираются среди самых важных признаков.
Аналогично, график индивидуального условного ожидания (ICE) [3] показывает зависимость целевой функции от интересующего признака. Однако, в отличие от графиков частичной зависимости, которые показывают среднее влияние интересующих признаков, графики ICE визуализируют зависимость предсказания от признака для каждого образца отдельно, с одной линией на каждый образец. Для графиков ICE поддерживается только один интересующий признак.
В этом примере показано, как получить графики частичной зависимости и ICE из MLPRegressor и HistGradientBoostingRegressor, обученных на наборе данных по прокату велосипедов. Пример вдохновлён [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)

Первое заметное различие между набором обучающих и тестовых данных заключается в том, что количество проката велосипедов выше в тестовом наборе. Поэтому неудивительно, что модель машинного обучения недооценивает количество проката велосипедов. Также мы наблюдаем, что количество проката велосипедов меньше весной. Кроме того, мы видим, что в рабочие дни есть определённый паттерн примерно с 6-7 утра и с 17-18 часов с некоторыми пиками проката велосипедов. Мы можем помнить эти различия и использовать их для понимания графика частичной зависимости.
Препроцессор для моделей машинного обучения
Так как позже мы используем две разные модели, MLPRegressor и HistGradientBoostingRegressor, мы создаём два разных препроцессора, специфичных для каждой модели.
Препроцессор для модели нейронной сети
Мы будем использовать 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
Препроцессор для модели градиентного бустинга
Для модели градиентного бустинга мы оставляем числовые признаки без изменений и кодируем только категориальные признаки с помощью 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
Однофакторная частичная зависимость с различными моделями
В этом разделе мы вычислим однофакторную частичную зависимость с использованием двух различных моделей машинного обучения: (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.861s 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,
)

Computing partial dependence plots... done in 0.705s
Градиентный бустинг
Теперь давайте обучим 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.139s 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,
)

Computing partial dependence plots... done in 1.294s
Анализ графиков
Сначала рассмотрим графики частичной зависимости для числовых признаков. Для обеих моделей общая тенденция PDP температуры заключается в том, что количество велосипедных поездок увеличивается с повышением температуры. Мы можем провести аналогичный анализ, но с обратной тенденцией для признака влажности. Количество велосипедных поездок уменьшается по мере увеличения влажности. Наконец, мы наблюдаем ту же тенденцию для признака скорости ветра. Количество велосипедных поездок уменьшается по мере увеличения скорости ветра для обеих моделей. Мы также наблюдаем, что MLPRegressor имеет гораздо более плавные прогнозы, чем HistGradientBoostingRegressor.
Теперь рассмотрим графики частичной зависимости для категориальных признаков.
Мы наблюдаем, что весна имеет самый низкий показатель для признака сезона. Что касается признака погоды, категория дождь имеет самый низкий показатель. Относительно признака часа мы видим два пика около 7 утра и 18 часов. Эти выводы согласуются с ранее сделанными наблюдениями над данными.
Однако стоит отметить, что мы создаем потенциально бессмысленные синтетические образцы, если признаки коррелированы.
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)

Computing partial dependence plots and individual conditional expectation... done in 0.594s
Мы видим, что ICE для признака температуры дает нам дополнительную информацию: некоторые линии ICE являются плоскими, а другие показывают уменьшение зависимости для температуры выше 35 градусов Цельсия. Мы наблюдаем аналогичный паттерн для признака влажности: некоторые линии ICE показывают резкое уменьшение, когда влажность превышает 80%.
Не все линии 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)

2D графики взаимодействия
Диаграммы частичной зависимости (PDP) с двумя интересующими признаками позволяют визуализировать взаимодействия между ними. Однако графики индивидуальных условных ожиданий (ICE) нельзя легко построить и интерпретировать. Мы покажем представление, доступное в from_estimator, которое представляет собой 2D тепловую карту.
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
)

Computing partial dependence plots... done in 9.495s
Диаграмма частичной зависимости по двум параметрам показывает зависимость количества прокат велосипедов от совместных значений температуры и влажности. Мы отчетливо видим взаимодействие между двумя признаками. При температуре выше 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
)

Computing partial dependence plots... done in 8.072s
Графики частичной зависимости по одному параметру для модели, ограниченной не моделированием взаимодействий признаков, показывают локальные пики для каждого признака по отдельности, особенно для признака «влажность». Эти пики могут отражать ухудшение работы модели, которая пытается как-то компенсировать запрещенные взаимодействия, переобучая определённые тренировочные точки. Обратите внимание, что предсказательная производительность этой модели, измеренная на тестовом наборе, значительно хуже, чем у исходной, не ограниченной модели.
Также обратите внимание, что количество локальных пиков, видимых на этих графиках, зависит от параметра разрешения сетки диаграммы частичной зависимости.
Эти локальные пики приводят к шумной сетчатой 2D диаграмме частичной зависимости. Довольно сложно определить, существуют ли взаимодействия между этими признаками из-за высоких частотных колебаний в признаке влажности. Однако можно чётко увидеть, что простое взаимодействие, наблюдаемое при пересечении температуры порога 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
)

Computing partial dependence plots... done in 0.444s
3D представление
Давайте построим ту же диаграмму частичной зависимости для взаимодействия 2 признаков, на этот раз в 3 измерениях.
# 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()

Общее время выполнения сценария: (0 минут 27.396 секунд)
Связанные примеры
© 2007–2025 The scikit-learn developers
Licensed under the 3-clause BSD License.
https://scikit-learn.org/1.6/auto_examples/inspection/plot_partial_dependence.html