Note
Go to the end to download the full example code.
SKD001 & SKD002 - Overfitting and underfitting#
SKD002 and SKD001 describe opposite ends of the same problem: model expressiveness.
Too little expressiveness: train and test scores are close to those of a model that guesses randomly (often called a dummy model) (SKD002).
Too much expressiveness: train scores pull far ahead of test scores (SKD001).
A performant model learns from the training data without memorizing its specificities, so that learned knowledge can be generalized to new data. This notebook walks the underfitting to overfitting path while showing different mitigations techniques:
tweaking model capacity (model family / complexity)
feature engineering
tuning regularization
using early stopping
using more data
We use California housing (median house value). Half of the rows feed the main walkthrough; the other half is reserved to show the effect of adding training data.
Load the California housing dataset#
Each row is a census block group in a geographical region whose coordinates
we have (Latitude, Longitude). The features mix house characteristics
(age, rooms, occupancy) with regional aggregates (median income, population).
The target MedHouseVal is the median house value for the block group.
The feature set is small but rich: later we combine columns such as
AveRooms / AveOccup into a rooms-per-person marker.
import pandas as pd
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
housing = fetch_california_housing(as_frame=True)
X, X_heldout, y, y_heldout = train_test_split(
housing.data,
housing.target,
train_size=0.5,
random_state=42,
)
Let us glance at the table. Pay attention to AveRooms and AveOccup:
we will combine them into a rooms-per-person feature below.
from skrub import TableReport
TableReport(X)
| MedInc | HouseAge | AveRooms | AveBedrms | Population | AveOccup | Latitude | Longitude | |
|---|---|---|---|---|---|---|---|---|
| 5,967 | 3.89 | 19.0 | 4.97 | 1.03 | 1.13e+03 | 2.89 | 34.1 | -118. |
| 17,744 | 7.13 | 15.0 | 6.93 | 1.06 | 1.15e+03 | 3.52 | 37.3 | -122. |
| 952 | 5.01 | 15.0 | 6.67 | 1.06 | 2.48e+03 | 2.56 | 37.7 | -122. |
| 9,361 | 8.39 | 36.0 | 7.00 | 0.990 | 1.55e+03 | 2.47 | 38.0 | -123. |
| 11,024 | 4.85 | 29.0 | 5.55 | 0.901 | 724. | 2.76 | 33.8 | -118. |
| 11,284 | 6.37 | 35.0 | 6.13 | 0.926 | 658. | 3.03 | 33.8 | -118. |
| 11,964 | 3.05 | 33.0 | 6.87 | 1.27 | 1.75e+03 | 3.90 | 34.0 | -117. |
| 5,390 | 2.93 | 36.0 | 3.99 | 1.08 | 1.76e+03 | 3.33 | 34.0 | -118. |
| 860 | 5.72 | 15.0 | 6.40 | 1.07 | 1.78e+03 | 3.18 | 37.6 | -122. |
| 15,795 | 2.58 | 52.0 | 3.40 | 1.06 | 2.62e+03 | 2.11 | 37.8 | -122. |
MedInc
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
7,566 (73.3%)
This column has a high cardinality (> 40).
- Mean ± Std
- 3.87 ± 1.91
- Median ± IQR
- 3.52 ± 2.21
- Min | Max
- 0.500 | 15.0
HouseAge
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
52 (0.5%)
This column has a high cardinality (> 40).
- Mean ± Std
- 28.5 ± 12.6
- Median ± IQR
- 29.0 ± 19.0
- Min | Max
- 1.00 | 52.0
AveRooms
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
9,957 (96.5%)
This column has a high cardinality (> 40).
- Mean ± Std
- 5.44 ± 2.58
- Median ± IQR
- 5.24 ± 1.61
- Min | Max
- 0.889 | 142.
AveBedrms
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
8,141 (78.9%)
This column has a high cardinality (> 40).
- Mean ± Std
- 1.10 ± 0.469
- Median ± IQR
- 1.05 ± 0.0937
- Min | Max
- 0.375 | 25.6
Population
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
3,125 (30.3%)
This column has a high cardinality (> 40).
- Mean ± Std
- 1.43e+03 ± 1.14e+03
- Median ± IQR
- 1.17e+03 ± 945.
- Min | Max
- 3.00 | 3.57e+04
AveOccup
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
9,755 (94.5%)
This column has a high cardinality (> 40).
- Mean ± Std
- 3.17 ± 14.5
- Median ± IQR
- 2.82 ± 0.841
- Min | Max
- 0.692 | 1.24e+03
Latitude
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
789 (7.6%)
This column has a high cardinality (> 40).
- Mean ± Std
- 35.7 ± 2.14
- Median ± IQR
- 34.3 ± 3.78
- Min | Max
- 32.5 | 42.0
Longitude
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
783 (7.6%)
This column has a high cardinality (> 40).
- Mean ± Std
- -120. ± 2.01
- Median ± IQR
- -119. ± 3.79
- Min | Max
- -124. | -114.
No columns match the selected filter: . You can change the column filter in the dropdown menu above.
|
Column
|
Column name
|
dtype
|
Is sorted
|
Null values
|
Unique values
|
Mean
|
Std
|
Min
|
Median
|
Max
|
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | MedInc | Float64DType | False | 0 (0.0%) | 7566 (73.3%) | 3.87 | 1.91 | 0.500 | 3.52 | 15.0 |
| 1 | HouseAge | Float64DType | False | 0 (0.0%) | 52 (0.5%) | 28.5 | 12.6 | 1.00 | 29.0 | 52.0 |
| 2 | AveRooms | Float64DType | False | 0 (0.0%) | 9957 (96.5%) | 5.44 | 2.58 | 0.889 | 5.24 | 142. |
| 3 | AveBedrms | Float64DType | False | 0 (0.0%) | 8141 (78.9%) | 1.10 | 0.469 | 0.375 | 1.05 | 25.6 |
| 4 | Population | Float64DType | False | 0 (0.0%) | 3125 (30.3%) | 1.43e+03 | 1.14e+03 | 3.00 | 1.17e+03 | 3.57e+04 |
| 5 | AveOccup | Float64DType | False | 0 (0.0%) | 9755 (94.5%) | 3.17 | 14.5 | 0.692 | 2.82 | 1.24e+03 |
| 6 | Latitude | Float64DType | False | 0 (0.0%) | 789 (7.6%) | 35.7 | 2.14 | 32.5 | 34.3 | 42.0 |
| 7 | Longitude | Float64DType | False | 0 (0.0%) | 783 (7.6%) | -120. | 2.01 | -124. | -119. | -114. |
No columns match the selected filter: . You can change the column filter in the dropdown menu above.
MedInc
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
7,566 (73.3%)
This column has a high cardinality (> 40).
- Mean ± Std
- 3.87 ± 1.91
- Median ± IQR
- 3.52 ± 2.21
- Min | Max
- 0.500 | 15.0
HouseAge
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
52 (0.5%)
This column has a high cardinality (> 40).
- Mean ± Std
- 28.5 ± 12.6
- Median ± IQR
- 29.0 ± 19.0
- Min | Max
- 1.00 | 52.0
AveRooms
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
9,957 (96.5%)
This column has a high cardinality (> 40).
- Mean ± Std
- 5.44 ± 2.58
- Median ± IQR
- 5.24 ± 1.61
- Min | Max
- 0.889 | 142.
AveBedrms
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
8,141 (78.9%)
This column has a high cardinality (> 40).
- Mean ± Std
- 1.10 ± 0.469
- Median ± IQR
- 1.05 ± 0.0937
- Min | Max
- 0.375 | 25.6
Population
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
3,125 (30.3%)
This column has a high cardinality (> 40).
- Mean ± Std
- 1.43e+03 ± 1.14e+03
- Median ± IQR
- 1.17e+03 ± 945.
- Min | Max
- 3.00 | 3.57e+04
AveOccup
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
9,755 (94.5%)
This column has a high cardinality (> 40).
- Mean ± Std
- 3.17 ± 14.5
- Median ± IQR
- 2.82 ± 0.841
- Min | Max
- 0.692 | 1.24e+03
Latitude
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
789 (7.6%)
This column has a high cardinality (> 40).
- Mean ± Std
- 35.7 ± 2.14
- Median ± IQR
- 34.3 ± 3.78
- Min | Max
- 32.5 | 42.0
Longitude
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
783 (7.6%)
This column has a high cardinality (> 40).
- Mean ± Std
- -120. ± 2.01
- Median ± IQR
- -119. ± 3.79
- Min | Max
- -124. | -114.
No columns match the selected filter: . You can change the column filter in the dropdown menu above.
| Column 1 | Column 2 | Cramér's V | Pearson's Correlation |
|---|---|---|---|
| AveRooms | AveBedrms | 0.743 | 0.805 |
| Latitude | Longitude | 0.504 | -0.924 |
| HouseAge | Longitude | 0.158 | -0.0834 |
| HouseAge | Latitude | 0.137 | -0.0168 |
| MedInc | AveRooms | 0.132 | 0.360 |
| HouseAge | Population | 0.125 | -0.258 |
| AveRooms | AveOccup | 0.119 | -0.0181 |
| Population | AveOccup | 0.110 | 0.165 |
| AveBedrms | Longitude | 0.105 | -0.00510 |
| AveBedrms | Latitude | 0.0984 | 0.103 |
| AveBedrms | AveOccup | 0.0942 | -0.0248 |
| MedInc | Latitude | 0.0940 | -0.0780 |
| AveRooms | Latitude | 0.0912 | 0.133 |
| MedInc | HouseAge | 0.0845 | -0.123 |
| MedInc | AveOccup | 0.0824 | -0.0203 |
| MedInc | Longitude | 0.0811 | -0.0169 |
| AveRooms | Longitude | 0.0780 | -0.0467 |
| HouseAge | AveRooms | 0.0755 | -0.183 |
| HouseAge | AveBedrms | 0.0659 | -0.101 |
| Population | Longitude | 0.0634 | 0.116 |
| HouseAge | AveOccup | 0.0582 | 0.0112 |
| AveOccup | Latitude | 0.0564 | -0.113 |
| Population | Latitude | 0.0562 | -0.122 |
| AveOccup | Longitude | 0.0500 | 0.111 |
| MedInc | AveBedrms | 0.0392 | -0.0779 |
| MedInc | Population | 0.0350 | -0.00452 |
| AveRooms | Population | 0.0128 | -0.0798 |
| AveBedrms | Population | 0.0122 | -0.0798 |
Please enable javascript
The skrub table reports need javascript to display correctly. If you are displaying a report in a Jupyter notebook and you see this message, you may need to re-execute the cell or to trust the notebook (button on the top right or "File > Trust notebook").
MedHouseVal is capped at 5.0 (500k USD). Click the column in the report to
see the histogram and the pile-up at the ceiling.
| MedHouseVal | |
|---|---|
| 5,967 | 1.45 |
| 17,744 | 2.78 |
| 952 | 2.71 |
| 9,361 | 4.71 |
| 11,024 | 2.18 |
| 11,284 | 2.29 |
| 11,964 | 0.978 |
| 5,390 | 2.22 |
| 860 | 2.83 |
| 15,795 | 3.25 |
MedHouseVal
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
3,231 (31.3%)
This column has a high cardinality (> 40).
- Mean ± Std
- 2.07 ± 1.16
- Median ± IQR
- 1.79 ± 1.44
- Min | Max
- 0.150 | 5.00
No columns match the selected filter: . You can change the column filter in the dropdown menu above.
|
Column
|
Column name
|
dtype
|
Is sorted
|
Null values
|
Unique values
|
Mean
|
Std
|
Min
|
Median
|
Max
|
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | MedHouseVal | Float64DType | False | 0 (0.0%) | 3231 (31.3%) | 2.07 | 1.16 | 0.150 | 1.79 | 5.00 |
No columns match the selected filter: . You can change the column filter in the dropdown menu above.
MedHouseVal
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
3,231 (31.3%)
This column has a high cardinality (> 40).
- Mean ± Std
- 2.07 ± 1.16
- Median ± IQR
- 1.79 ± 1.44
- Min | Max
- 0.150 | 5.00
No columns match the selected filter: . You can change the column filter in the dropdown menu above.
Please enable javascript
The skrub table reports need javascript to display correctly. If you are displaying a report in a Jupyter notebook and you see this message, you may need to re-execute the cell or to trust the notebook (button on the top right or "File > Trust notebook").
We use the same split for every model comparison, with a fixed seed, so that we are sure that we are comparing models on the same test data.
from skore import TrainTestSplit
splitter = TrainTestSplit(test_size=0.2, random_state=42)
Underfitting: SKD002 fires#
We start with a linear model that is overly regularized.
Ridge with a very large alpha shrinks
coefficients toward zero, so predictions stay close to a dummy baseline.
import skore
from sklearn.linear_model import Ridge
report_underfit = skore.evaluate(
Ridge(alpha=1e6),
X=X,
y=y,
splitter=splitter,
)
report_underfit.metrics.summarize(data_source="both").frame()
| Ridge (train) | Ridge (test) | |
|---|---|---|
| metric | ||
| r2 | 0.041240 | 0.040223 |
| rmse | 1.127863 | 1.150918 |
| mae | 0.889223 | 0.907935 |
| mape | 0.607538 | 0.614893 |
| fit_time | 0.002285 | 0.002285 |
| predict_time | 0.000733 | 0.000739 |
For a model that learns so little, SKD002 fires.
report_underfit.checks.summarize(fast_mode=True)
- [SKD002] Potential underfitting. Train/test scores are on par and not significantly better than the dummy baseline for 3/4 comparable metrics.
- [SKD006] Coefficient interpretation. Features are not on the same scale: coefficient magnitudes are not directly comparable as feature importance.
- [SKD003] Inconsistent performance across splits. Not applicable to estimator reports.
- [SKD004] High class imbalance. ML task is not binary classification. Got regression.
- [SKD005] Underrepresented classes. ML task is not multiclass classification. Got regression.
- [SKD007] MDI biased for high-cardinality features. Estimator is not a tree-based model: it does not have a `feature_importances_` attribute.
- [SKD013] Train-test overlap in time series. No datetime column found.
- [SKD014] Hyperparameters at search edge. Estimator is not a BaseSearchCV instance. Got Ridge.
- [SKD015] Hyperparameters worth tuning. Estimator is not a BaseSearchCV instance. Got Ridge.
No checks were muted.
Fast mode is on: expensive checks are skipped unless already cached.
Mute a check by passing its code to ignore, e.g. .checks.summarize(ignore=['SKD001']).
Increase model expressiveness#
Moving away from underfitting means giving the model enough expressiveness to
use the inputs. Let us drop to a Ridge with a
much smaller alpha: it already learns useful weights on each feature and
is enough to clear SKD002 on this table.
report_ridge = skore.evaluate(Ridge(alpha=1.0), X=X, y=y, splitter=splitter)
report_ridge.metrics.summarize(data_source="both").frame()
| Ridge (train) | Ridge (test) | |
|---|---|---|
| metric | ||
| r2 | 0.610830 | 0.609422 |
| rmse | 0.718573 | 0.734197 |
| mae | 0.525393 | 0.539913 |
| mape | 0.313230 | 0.320523 |
| fit_time | 0.003204 | 0.003204 |
| predict_time | 0.000873 | 0.000880 |
report_ridge.checks.summarize(fast_mode=True)
No issues were detected in your report.
- [SKD003] Inconsistent performance across splits. Not applicable to estimator reports.
- [SKD004] High class imbalance. ML task is not binary classification. Got regression.
- [SKD005] Underrepresented classes. ML task is not multiclass classification. Got regression.
- [SKD007] MDI biased for high-cardinality features. Estimator is not a tree-based model: it does not have a `feature_importances_` attribute.
- [SKD013] Train-test overlap in time series. No datetime column found.
- [SKD014] Hyperparameters at search edge. Estimator is not a BaseSearchCV instance. Got Ridge.
- [SKD015] Hyperparameters worth tuning. Estimator is not a BaseSearchCV instance. Got Ridge.
No checks were muted.
Fast mode is on: expensive checks are skipped unless already cached.
Mute a check by passing its code to ignore, e.g. .checks.summarize(ignore=['SKD001']).
Feature engineering#
Better features help when the model family is fine but the representation
is weak (still an underfitting problem). Here, AveRooms / AveOccup is a
rooms-per-person signal that a linear model can use more easily than the raw
counts.
The same step has an overfitting side effect: every new feature also increases expressiveness. Mild engineering can close an underfit gap; aggressive expansions (very high-degree interactions, huge one-hot spaces) can reopen an overfit gap. Think of features as capacity you add to the inputs, not only to the estimator.
X_fe = X.assign(RoomsPerPerson=X["AveRooms"] / X["AveOccup"].clip(lower=0.1))
report_ridge_fe = skore.evaluate(Ridge(alpha=1.0), X=X_fe, y=y, splitter=splitter)
report_ridge_fe.metrics.summarize(data_source="both").frame()
| Ridge (train) | Ridge (test) | |
|---|---|---|
| metric | ||
| r2 | 0.640718 | 0.639733 |
| rmse | 0.690429 | 0.705134 |
| mae | 0.494856 | 0.508096 |
| mape | 0.295383 | 0.301925 |
| fit_time | 0.002935 | 0.002935 |
| predict_time | 0.001219 | 0.001245 |
The test score rises while the train score stays close: we reduced underfitting without creating overfitting by adding a signal-rich feature.
report_ridge_fe.checks.summarize(fast_mode=True)
No issues were detected in your report.
- [SKD003] Inconsistent performance across splits. Not applicable to estimator reports.
- [SKD004] High class imbalance. ML task is not binary classification. Got regression.
- [SKD005] Underrepresented classes. ML task is not multiclass classification. Got regression.
- [SKD007] MDI biased for high-cardinality features. Estimator is not a tree-based model: it does not have a `feature_importances_` attribute.
- [SKD013] Train-test overlap in time series. No datetime column found.
- [SKD014] Hyperparameters at search edge. Estimator is not a BaseSearchCV instance. Got Ridge.
- [SKD015] Hyperparameters worth tuning. Estimator is not a BaseSearchCV instance. Got Ridge.
No checks were muted.
Fast mode is on: expensive checks are skipped unless already cached.
Mute a check by passing its code to ignore, e.g. .checks.summarize(ignore=['SKD001']).
Overfitting: SKD001 fires#
Hand-crafted features are one way to capture nonlinearity for a linear model.
Another is to switch to a model family that learns nonlinear structure on its
own. A default RandomForestRegressor does that,
but unrestricted leaves can also memorize the training data: train metrics
look excellent, test metrics improve but not as much as train ones, and
SKD001 flags the gap.
from sklearn.ensemble import RandomForestRegressor
report_rf = skore.evaluate(
RandomForestRegressor(random_state=42),
X=X,
y=y,
splitter=splitter,
)
report_rf.metrics.summarize(data_source="both").frame()
| RandomForestRegressor (train) | RandomForestRegressor (test) | |
|---|---|---|
| metric | ||
| r2 | 0.969057 | 0.784944 |
| rmse | 0.202621 | 0.544797 |
| mae | 0.134065 | 0.361575 |
| mape | 0.077009 | 0.206075 |
| fit_time | 4.870742 | 4.870742 |
| predict_time | 0.176020 | 0.066465 |
report_rf.checks.summarize(fast_mode=True)
- [SKD001] Potential overfitting. Significant train/test gaps were found for 4/4 default predictive metrics.
- [SKD007] MDI biased for high-cardinality features. High-cardinality features detected: MedInc, AveRooms, AveBedrms (and 1 more). Mean Decrease in Impurity (MDI) importance is biased toward such features. Consider using permutation importance for a more robust alternative.
- [SKD016] Estimator not tuned. Estimator(s) left at default settings; consider tuning: ['max_features', 'min_samples_leaf'] for RandomForestRegressor.
- [SKD003] Inconsistent performance across splits. Not applicable to estimator reports.
- [SKD004] High class imbalance. ML task is not binary classification. Got regression.
- [SKD005] Underrepresented classes. ML task is not multiclass classification. Got regression.
- [SKD006] Coefficient interpretation. Estimator is not a linear model: it does not have a `coef_` attribute.
- [SKD013] Train-test overlap in time series. No datetime column found.
- [SKD014] Hyperparameters at search edge. Estimator is not a BaseSearchCV instance. Got RandomForestRegressor.
- [SKD015] Hyperparameters worth tuning. Estimator is not a BaseSearchCV instance. Got RandomForestRegressor.
No checks were muted.
Fast mode is on: expensive checks are skipped unless already cached.
Mute a check by passing its code to ignore, e.g. .checks.summarize(ignore=['SKD001']).
Regularization#
Once SKD001 appears, we pull expressiveness back with capacity limits on the estimator (leaf size, feature fraction, depth, learning rate, …).
Here, we set the hyperparameters of the model by hand. In practice, it is rather hard to know in advance which combination of hyperparameters leads to generalization. One should lean towards tuning hyperparameters. We would advocate for randomized search, with successive halving when the amount of samples is large. See tuning the hyper-parameters of an estimator. In this particular example, we are not implementing the search in order to keep the execution time of the example short.
model_rf_reg = RandomForestRegressor(
min_samples_leaf=20,
max_features=0.5,
random_state=42,
)
report_rf_reg = skore.evaluate(
model_rf_reg,
X=X,
y=y,
splitter=splitter,
)
report_rf_reg.metrics.summarize(data_source="both").frame()
| RandomForestRegressor (train) | RandomForestRegressor (test) | |
|---|---|---|
| metric | ||
| r2 | 0.809771 | 0.762550 |
| rmse | 0.502389 | 0.572461 |
| mae | 0.344942 | 0.394609 |
| mape | 0.200611 | 0.226745 |
| fit_time | 1.707768 | 1.707768 |
| predict_time | 0.080103 | 0.031034 |
report_rf_reg.checks.summarize(fast_mode=True)
No issues were detected in your report.
- [SKD007] MDI biased for high-cardinality features. High-cardinality features detected: MedInc, AveRooms, AveBedrms (and 1 more). Mean Decrease in Impurity (MDI) importance is biased toward such features. Consider using permutation importance for a more robust alternative.
- [SKD003] Inconsistent performance across splits. Not applicable to estimator reports.
- [SKD004] High class imbalance. ML task is not binary classification. Got regression.
- [SKD005] Underrepresented classes. ML task is not multiclass classification. Got regression.
- [SKD006] Coefficient interpretation. Estimator is not a linear model: it does not have a `coef_` attribute.
- [SKD013] Train-test overlap in time series. No datetime column found.
- [SKD014] Hyperparameters at search edge. Estimator is not a BaseSearchCV instance. Got RandomForestRegressor.
- [SKD015] Hyperparameters worth tuning. Estimator is not a BaseSearchCV instance. Got RandomForestRegressor.
No checks were muted.
Fast mode is on: expensive checks are skipped unless already cached.
Mute a check by passing its code to ignore, e.g. .checks.summarize(ignore=['SKD001']).
Early stopping#
For iterative learners, early stopping is another way to limit expressiveness
that does not require a hyperparameter search. We hold out a validation set,
monitor a metric, and stop when that metric stops improving. Further
iterations are assumed to overfit. We can either pass a validation set to
.fit or use the validation_fraction parameter. See scikit-learn’s
example on gradient boosting with early stopping.
Let us compare a HistGradientBoostingRegressor
with and without early stopping on the same split to see the effect. We bump
max_iter so that early stopping has room to act. With
validation_fraction, the early-stopped model trains on only 90% of each
train fold; a small test drop relative to the unstopped run can come from
that, rather than from stopping itself. Passing an external validation set
(for example X_heldout / y_heldout) would avoid that, but wiring fit
parameters through skore currently needs a skrub DataOp, so we keep
validation_fraction here.
from sklearn.ensemble import HistGradientBoostingRegressor
report_hgbr = skore.evaluate(
HistGradientBoostingRegressor(max_iter=1000, random_state=42),
X=X,
y=y,
splitter=splitter,
)
model_hgbr_es = HistGradientBoostingRegressor(
early_stopping=True,
validation_fraction=0.1,
n_iter_no_change=10,
max_iter=1000,
random_state=42,
)
report_hgbr_es = skore.evaluate(
model_hgbr_es,
X=X,
y=y,
splitter=splitter,
)
skore.compare(
{
"hgbr": report_hgbr,
"hgbr_early_stopping": report_hgbr_es,
}
).metrics.summarize(data_source="both").frame()
| hgbr (train) | hgbr (test) | hgbr_early_stopping (train) | hgbr_early_stopping (test) | |
|---|---|---|---|---|
| metric | ||||
| r2 | 0.990605 | 0.831857 | 0.935434 | 0.826642 |
| rmse | 0.111650 | 0.481724 | 0.292687 | 0.489138 |
| mae | 0.078763 | 0.313363 | 0.199150 | 0.316511 |
| mape | 0.046624 | 0.175758 | 0.114104 | 0.177941 |
| fit_time | 2.604526 | 2.604526 | 0.808667 | 0.808667 |
| predict_time | 0.364234 | 0.121463 | 0.110828 | 0.027179 |
report_hgbr.checks.summarize(fast_mode=True)
- [SKD001] Potential overfitting. Significant train/test gaps were found for 4/4 default predictive metrics.
- [SKD016] Estimator not tuned. Estimator(s) left at default settings; consider tuning: ['learning_rate', 'max_leaf_nodes'] for HistGradientBoostingRegressor.
- [SKD003] Inconsistent performance across splits. Not applicable to estimator reports.
- [SKD004] High class imbalance. ML task is not binary classification. Got regression.
- [SKD005] Underrepresented classes. ML task is not multiclass classification. Got regression.
- [SKD006] Coefficient interpretation. Estimator is not a linear model: it does not have a `coef_` attribute.
- [SKD007] MDI biased for high-cardinality features. Estimator is not a tree-based model: it does not have a `feature_importances_` attribute.
- [SKD013] Train-test overlap in time series. No datetime column found.
- [SKD014] Hyperparameters at search edge. Estimator is not a BaseSearchCV instance. Got HistGradientBoostingRegressor.
- [SKD015] Hyperparameters worth tuning. Estimator is not a BaseSearchCV instance. Got HistGradientBoostingRegressor.
No checks were muted.
Fast mode is on: expensive checks are skipped unless already cached.
Mute a check by passing its code to ignore, e.g. .checks.summarize(ignore=['SKD001']).
report_hgbr_es.checks.summarize(fast_mode=True)
- [SKD001] Potential overfitting. Significant train/test gaps were found for 4/4 default predictive metrics.
- [SKD016] Estimator not tuned. Estimator(s) left at default settings; consider tuning: ['learning_rate', 'max_leaf_nodes'] for HistGradientBoostingRegressor.
- [SKD003] Inconsistent performance across splits. Not applicable to estimator reports.
- [SKD004] High class imbalance. ML task is not binary classification. Got regression.
- [SKD005] Underrepresented classes. ML task is not multiclass classification. Got regression.
- [SKD006] Coefficient interpretation. Estimator is not a linear model: it does not have a `coef_` attribute.
- [SKD007] MDI biased for high-cardinality features. Estimator is not a tree-based model: it does not have a `feature_importances_` attribute.
- [SKD013] Train-test overlap in time series. No datetime column found.
- [SKD014] Hyperparameters at search edge. Estimator is not a BaseSearchCV instance. Got HistGradientBoostingRegressor.
- [SKD015] Hyperparameters worth tuning. Estimator is not a BaseSearchCV instance. Got HistGradientBoostingRegressor.
No checks were muted.
Fast mode is on: expensive checks are skipped unless already cached.
Mute a check by passing its code to ignore, e.g. .checks.summarize(ignore=['SKD001']).
More training data#
Extra labeled rows help both regimes, but they are especially useful against
overfitting: the model has fewer opportunities to memorize a small sample.
Let us keep one fixed test fold, fit on the original train fold, then refit
after concatenating the held-out half of the dataset. We use
evaluate() with splitter="prefit" once the estimator is
fitted. A side-effect is that fit time is unavailable (skore did not time
.fit), so that metric appears as NaN in the reported tables.
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
model_less = HistGradientBoostingRegressor(
early_stopping=True,
validation_fraction=0.1,
n_iter_no_change=10,
random_state=42,
).fit(X_train, y_train)
report_less = skore.evaluate(model_less, X_test, y_test, splitter="prefit")
model_more = HistGradientBoostingRegressor(
early_stopping=True,
validation_fraction=0.1,
n_iter_no_change=10,
random_state=42,
).fit(pd.concat([X_train, X_heldout]), pd.concat([y_train, y_heldout]))
report_more = skore.evaluate(model_more, X_test, y_test, splitter="prefit")
skore.compare(
{"less_training_data": report_less, "more_training_data": report_more}
).metrics.summarize(data_source="test").frame()
| estimator | less_training_data | more_training_data |
|---|---|---|
| metric | ||
| r2 | 0.820257 | 0.833391 |
| rmse | 0.498065 | 0.479521 |
| mae | 0.327594 | 0.313229 |
| mape | 0.185737 | 0.176323 |
| predict_time | 0.016623 | 0.013071 |
In production, a learning curve (many refits on growing subsets) shows whether more labels are still worth the cost. skore does not wrap that API yet; we can treat it as a scikit-learn-side diagnostic next to these checks.
Summary comparison#
Here is a side-by-side comparison of the impact of the different techniques we applied, measured on the same test set.
skore.compare(
{
"ridge_large_alpha": report_underfit,
"ridge": report_ridge,
"ridge_feature_engineering": report_ridge_fe,
"default_rf": report_rf,
"regularized_rf": report_rf_reg,
"hgbr": report_hgbr,
"hgbr_early_stopping": report_hgbr_es,
"more_data": report_more,
}
).metrics.summarize(data_source="test").frame()
| estimator | ridge_large_alpha | ridge | ridge_feature_engineering | default_rf | regularized_rf | hgbr | hgbr_early_stopping | more_data |
|---|---|---|---|---|---|---|---|---|
| metric | ||||||||
| r2 | 0.040223 | 0.609422 | 0.639733 | 0.784944 | 0.762550 | 0.831857 | 0.826642 | 0.833391 |
| rmse | 1.150918 | 0.734197 | 0.705134 | 0.544797 | 0.572461 | 0.481724 | 0.489138 | 0.479521 |
| mae | 0.907935 | 0.539913 | 0.508096 | 0.361575 | 0.394609 | 0.313363 | 0.316511 | 0.313229 |
| mape | 0.614893 | 0.320523 | 0.301925 | 0.206075 | 0.226745 | 0.175758 | 0.177941 | 0.176323 |
| fit_time | 0.002285 | 0.003204 | 0.002935 | 4.870742 | 1.707768 | 2.604526 | 0.808667 | NaN |
| predict_time | 0.000739 | 0.000880 | 0.001245 | 0.066465 | 0.031034 | 0.121463 | 0.027179 | 0.013071 |
Conclusion#
SKD002 and SKD001 are two different consequences of a unsuited expressiveness. We relax regularization / add capacity and use informative features until the model beats a weak baseline; then we regularize, stop early, and add data if train scores have a noticeable gap compared to the test scores.
Feature engineering helps the underfit side of the path; capacity control and more data help the overfit side. Hyperparameter choices play a dual role. In practice we combine several of these levers and re-run the checks until the results are satisfying enough.
Total running time of the script: (0 minutes 15.663 seconds)