|
10 | 10 | different encoding strategies for categorical features. In |
11 | 11 | particular, we will evaluate: |
12 | 12 |
|
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 |
19 | 19 | :class:`~ensemble.HistGradientBoostingRegressor` estimator. |
20 | 20 |
|
21 | 21 | We will work with the Ames Iowa Housing dataset which consists of numerical |
|
92 | 92 | ("drop", make_column_selector(dtype_include="category")), remainder="passthrough" |
93 | 93 | ) |
94 | 94 | hist_dropped = make_pipeline(dropper, HistGradientBoostingRegressor(random_state=42)) |
| 95 | +hist_dropped |
95 | 96 |
|
96 | 97 | # %% |
97 | 98 | # Gradient boosting estimator with one-hot encoding |
|
112 | 113 | hist_one_hot = make_pipeline( |
113 | 114 | one_hot_encoder, HistGradientBoostingRegressor(random_state=42) |
114 | 115 | ) |
| 116 | +hist_one_hot |
115 | 117 |
|
116 | 118 | # %% |
117 | 119 | # Gradient boosting estimator with ordinal encoding |
|
139 | 141 | hist_ordinal = make_pipeline( |
140 | 142 | ordinal_encoder, HistGradientBoostingRegressor(random_state=42) |
141 | 143 | ) |
| 144 | +hist_ordinal |
142 | 145 |
|
143 | 146 | # %% |
144 | 147 | # Gradient boosting estimator with native categorical support |
|
156 | 159 | hist_native = HistGradientBoostingRegressor( |
157 | 160 | random_state=42, categorical_features="from_dtype" |
158 | 161 | ) |
| 162 | +hist_native |
159 | 163 |
|
160 | 164 | # %% |
161 | 165 | # Model comparison |
162 | 166 | # ---------------- |
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. |
166 | 171 |
|
| 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 | +# %% |
167 | 188 | import matplotlib.pyplot as plt |
| 189 | +import matplotlib.ticker as ticker |
168 | 190 |
|
169 | | -from sklearn.model_selection import cross_validate |
170 | 191 |
|
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], |
206 | 214 | ) |
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, |
213 | 221 | ) |
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() |
215 | 238 |
|
| 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() |
216 | 252 |
|
217 | | -plot_results("Gradient Boosting on Ames Housing") |
| 253 | + |
| 254 | +plot_performance_tradeoff(results, "Gradient Boosting on Ames Housing") |
218 | 255 |
|
219 | 256 | # %% |
| 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 | +# |
220 | 261 | # We see that the model with one-hot-encoded data is by far the slowest. This |
221 | 262 | # is to be expected, since one-hot-encoding creates one additional feature per |
222 | 263 | # category value (for each categorical feature), and thus more split points |
@@ -264,14 +305,21 @@ def plot_results(figure_title): |
264 | 305 | histgradientboostingregressor__max_iter=15, |
265 | 306 | ) |
266 | 307 |
|
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 | +] |
273 | 318 |
|
274 | | -plt.show() |
| 319 | +# %% |
| 320 | +plot_performance_tradeoff( |
| 321 | + results_underfit, "Gradient Boosting on Ames Housing (few and shallow trees)" |
| 322 | +) |
275 | 323 |
|
276 | 324 | # %% |
277 | 325 | # The results for these under-fitting models confirm our previous intuition: |
|
0 commit comments