Skip to content

Commit 588f396

Browse files
ArturoAmorQArturoAmorQlucyleeow
authored
DOC Update plots in Categorical Feature Support in GBDT example (scikit-learn#31062)
Co-authored-by: ArturoAmorQ <arturo.amor-quiroz@polytechnique.edu> Co-authored-by: Lucy Liu <jliu176@gmail.com>
1 parent f1229ff commit 588f396

1 file changed

Lines changed: 108 additions & 60 deletions

File tree

examples/ensemble/plot_gradient_boosting_categorical.py

Lines changed: 108 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
different encoding strategies for categorical features. In
1111
particular, we will evaluate:
1212
13-
- dropping the categorical features
14-
- using a :class:`~preprocessing.OneHotEncoder`
15-
- using an :class:`~preprocessing.OrdinalEncoder` and treat categories as
16-
ordered, equidistant quantities
17-
- using an :class:`~preprocessing.OrdinalEncoder` and rely on the :ref:`native
18-
category support <categorical_support_gbdt>` of the
13+
- "Dropped": dropping the categorical features;
14+
- "One Hot": using a :class:`~preprocessing.OneHotEncoder`;
15+
- "Ordinal": using an :class:`~preprocessing.OrdinalEncoder` and treat
16+
categories as ordered, equidistant quantities;
17+
- "Native": using an :class:`~preprocessing.OrdinalEncoder` and rely on the
18+
:ref:`native category support <categorical_support_gbdt>` of the
1919
:class:`~ensemble.HistGradientBoostingRegressor` estimator.
2020
2121
We will work with the Ames Iowa Housing dataset which consists of numerical
@@ -92,6 +92,7 @@
9292
("drop", make_column_selector(dtype_include="category")), remainder="passthrough"
9393
)
9494
hist_dropped = make_pipeline(dropper, HistGradientBoostingRegressor(random_state=42))
95+
hist_dropped
9596

9697
# %%
9798
# Gradient boosting estimator with one-hot encoding
@@ -112,6 +113,7 @@
112113
hist_one_hot = make_pipeline(
113114
one_hot_encoder, HistGradientBoostingRegressor(random_state=42)
114115
)
116+
hist_one_hot
115117

116118
# %%
117119
# Gradient boosting estimator with ordinal encoding
@@ -139,6 +141,7 @@
139141
hist_ordinal = make_pipeline(
140142
ordinal_encoder, HistGradientBoostingRegressor(random_state=42)
141143
)
144+
hist_ordinal
142145

143146
# %%
144147
# Gradient boosting estimator with native categorical support
@@ -156,67 +159,105 @@
156159
hist_native = HistGradientBoostingRegressor(
157160
random_state=42, categorical_features="from_dtype"
158161
)
162+
hist_native
159163

160164
# %%
161165
# Model comparison
162166
# ----------------
163-
# Finally, we evaluate the models using cross validation. Here we compare the
164-
# models performance in terms of
165-
# :func:`~metrics.mean_absolute_percentage_error` and fit times.
167+
# Here we use :term:`cross validation` to compare the models performance in
168+
# terms of :func:`~metrics.mean_absolute_percentage_error` and fit times. In the
169+
# upcoming plots, error bars represent 1 standard deviation as computed across
170+
# folds.
166171

172+
from sklearn.model_selection import cross_validate
173+
174+
common_params = {"cv": 5, "scoring": "neg_mean_absolute_percentage_error", "n_jobs": -1}
175+
176+
dropped_result = cross_validate(hist_dropped, X, y, **common_params)
177+
one_hot_result = cross_validate(hist_one_hot, X, y, **common_params)
178+
ordinal_result = cross_validate(hist_ordinal, X, y, **common_params)
179+
native_result = cross_validate(hist_native, X, y, **common_params)
180+
results = [
181+
("Dropped", dropped_result),
182+
("One Hot", one_hot_result),
183+
("Ordinal", ordinal_result),
184+
("Native", native_result),
185+
]
186+
187+
# %%
167188
import matplotlib.pyplot as plt
189+
import matplotlib.ticker as ticker
168190

169-
from sklearn.model_selection import cross_validate
170191

171-
scoring = "neg_mean_absolute_percentage_error"
172-
n_cv_folds = 3
173-
174-
dropped_result = cross_validate(hist_dropped, X, y, cv=n_cv_folds, scoring=scoring)
175-
one_hot_result = cross_validate(hist_one_hot, X, y, cv=n_cv_folds, scoring=scoring)
176-
ordinal_result = cross_validate(hist_ordinal, X, y, cv=n_cv_folds, scoring=scoring)
177-
native_result = cross_validate(hist_native, X, y, cv=n_cv_folds, scoring=scoring)
178-
179-
180-
def plot_results(figure_title):
181-
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 8))
182-
183-
plot_info = [
184-
("fit_time", "Fit times (s)", ax1, None),
185-
("test_score", "Mean Absolute Percentage Error", ax2, None),
186-
]
187-
188-
x, width = np.arange(4), 0.9
189-
for key, title, ax, y_limit in plot_info:
190-
items = [
191-
dropped_result[key],
192-
one_hot_result[key],
193-
ordinal_result[key],
194-
native_result[key],
195-
]
196-
197-
mape_cv_mean = [np.mean(np.abs(item)) for item in items]
198-
mape_cv_std = [np.std(item) for item in items]
199-
200-
ax.bar(
201-
x=x,
202-
height=mape_cv_mean,
203-
width=width,
204-
yerr=mape_cv_std,
205-
color=["C0", "C1", "C2", "C3"],
192+
def plot_performance_tradeoff(results, title):
193+
fig, ax = plt.subplots()
194+
markers = ["s", "o", "^", "x"]
195+
196+
for idx, (name, result) in enumerate(results):
197+
test_error = -result["test_score"]
198+
mean_fit_time = np.mean(result["fit_time"])
199+
mean_score = np.mean(test_error)
200+
std_fit_time = np.std(result["fit_time"])
201+
std_score = np.std(test_error)
202+
203+
ax.scatter(
204+
result["fit_time"],
205+
test_error,
206+
label=name,
207+
marker=markers[idx],
208+
)
209+
ax.scatter(
210+
mean_fit_time,
211+
mean_score,
212+
color="k",
213+
marker=markers[idx],
206214
)
207-
ax.set(
208-
xlabel="Model",
209-
title=title,
210-
xticks=x,
211-
xticklabels=["Dropped", "One Hot", "Ordinal", "Native"],
212-
ylim=y_limit,
215+
ax.errorbar(
216+
x=mean_fit_time,
217+
y=mean_score,
218+
yerr=std_score,
219+
c="k",
220+
capsize=2,
213221
)
214-
fig.suptitle(figure_title)
222+
ax.errorbar(
223+
x=mean_fit_time,
224+
y=mean_score,
225+
xerr=std_fit_time,
226+
c="k",
227+
capsize=2,
228+
)
229+
230+
ax.set_xscale("log")
231+
232+
nticks = 7
233+
x0, x1 = np.log10(ax.get_xlim())
234+
ticks = np.logspace(x0, x1, nticks)
235+
ax.set_xticks(ticks)
236+
ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%1.1e"))
237+
ax.minorticks_off()
215238

239+
ax.annotate(
240+
" best\nmodels",
241+
xy=(0.05, 0.05),
242+
xycoords="axes fraction",
243+
xytext=(0.1, 0.15),
244+
textcoords="axes fraction",
245+
arrowprops=dict(arrowstyle="->", lw=1.5),
246+
)
247+
ax.set_xlabel("Time to fit (seconds)")
248+
ax.set_ylabel("Mean Absolute Percentage Error")
249+
ax.set_title(title)
250+
ax.legend()
251+
plt.show()
216252

217-
plot_results("Gradient Boosting on Ames Housing")
253+
254+
plot_performance_tradeoff(results, "Gradient Boosting on Ames Housing")
218255

219256
# %%
257+
# In the plot above, the "best models" are those that are closer to the
258+
# down-left corner, as indicated by the arrow. Those models would indeed
259+
# correspond to faster fitting and lower error.
260+
#
220261
# We see that the model with one-hot-encoded data is by far the slowest. This
221262
# is to be expected, since one-hot-encoding creates one additional feature per
222263
# category value (for each categorical feature), and thus more split points
@@ -264,14 +305,21 @@ def plot_results(figure_title):
264305
histgradientboostingregressor__max_iter=15,
265306
)
266307

267-
dropped_result = cross_validate(hist_dropped, X, y, cv=n_cv_folds, scoring=scoring)
268-
one_hot_result = cross_validate(hist_one_hot, X, y, cv=n_cv_folds, scoring=scoring)
269-
ordinal_result = cross_validate(hist_ordinal, X, y, cv=n_cv_folds, scoring=scoring)
270-
native_result = cross_validate(hist_native, X, y, cv=n_cv_folds, scoring=scoring)
271-
272-
plot_results("Gradient Boosting on Ames Housing (few and small trees)")
308+
dropped_result = cross_validate(hist_dropped, X, y, **common_params)
309+
one_hot_result = cross_validate(hist_one_hot, X, y, **common_params)
310+
ordinal_result = cross_validate(hist_ordinal, X, y, **common_params)
311+
native_result = cross_validate(hist_native, X, y, **common_params)
312+
results_underfit = [
313+
("Dropped", dropped_result),
314+
("One Hot", one_hot_result),
315+
("Ordinal", ordinal_result),
316+
("Native", native_result),
317+
]
273318

274-
plt.show()
319+
# %%
320+
plot_performance_tradeoff(
321+
results_underfit, "Gradient Boosting on Ames Housing (few and shallow trees)"
322+
)
275323

276324
# %%
277325
# The results for these under-fitting models confirm our previous intuition:

0 commit comments

Comments
 (0)