Примечание
Перейти в конец, чтобы скачать полный код примера или запустить этот пример в браузере с помощью 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.DataFrame'> RangeIndex: 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: 101.5 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.745s 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.956s
Градиентный бустинг
Теперь обучим 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.118s 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.421s
Анализ графиков
Сначала рассмотрим графики PDP для числовых признаков. Для обеих моделей общий тренд PDP для температуры показывает, что число аренд велосипедов растет с повышением температуры. Аналогичный анализ для влажности показывает противоположный тренд: число аренд велосипедов уменьшается с повышением влажности. Наконец, мы видим такую же тенденцию для скорости ветра: в обеих моделях число аренд велосипедов снижается при увеличении скорости ветра. Мы также видим, что предсказания MLPRegressor значительно более плавные, чем предсказания HistGradientBoostingRegressor.
Теперь рассмотрим графики частичной зависимости для категориальных признаков.
Для признака «сезон» мы видим, что самый низкий столбец соответствует весне. Для признака «погода» самый низкий столбец соответствует дождю. Для признака «час» видны два пика — около 7 утра и 6 вечера. Эти результаты согласуются с выводами, сделанными нами ранее при анализе набора данных.
Однако стоит отметить, что при наличии корреляции между признаками мы можем создавать потенциально бессмысленные синтетические наблюдения.
ICE и PDP
PDP представляет собой усреднение маргинальных эффектов признаков. Мы усредняем отклики всех наблюдений из предоставленного набора. В результате некоторые эффекты могут оставаться незаметными. Для этого можно построить график отклика каждого отдельного наблюдения. Такое представление называется графиком индивидуальных эффектов (ICE). На графике ниже показаны ICE для 50 случайно выбранных наблюдений по признакам температуры и влажности.
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.607s
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)

Графики взаимодействия двух признаков
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
)

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

Пользовательские точки для анализа
Ни в одном из предыдущих примеров не указывалось, _какие именно_ точки используются для построения графиков частичной зависимости. По умолчанию мы используем процентили, определенные по входному набору данных. В некоторых случаях полезно задать точные точки, для которых нужно получить предсказания модели. Например, если пользователь хочет проверить поведение модели на данных, отличающихся от обучающих, или сравнить две модели, обученные на немного разных данных. Параметр custom_values позволяет передать значения, для которых нужно получить предсказания модели. Он переопределяет параметры grid_resolution и percentiles. Вернемся к рассмотренному выше примеру с градиентным бустингом, но зададим пользовательские значения.
print("Computing partial dependence plots with custom evaluation values...")
tic = time()
_, ax = plt.subplots(ncols=2, figsize=(6, 4), sharey=True, constrained_layout=True)
features_info = {
"features": ["temp", "humidity"],
"kind": "both",
}
display = PartialDependenceDisplay.from_estimator(
hgbdt_model,
X_train,
**features_info,
ax=ax,
**common_params,
# we set custom values for temp feature -
# all other features are evaluated based on the data
custom_values={"temp": np.linspace(0, 40, 10)},
)
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 with custom evaluation values... done in 0.728s
Общее время выполнения скрипта: (0 минут 30.470 секунд)
Похожие примеры
© 2007–2026 The scikit-learn developers
Licensed under the 3-clause BSD License.
https://scikit-learn.org/1.9/auto_examples/inspection/plot_partial_dependence.html