Simplified and structured experiment reporting#

This example shows how to leverage skore for structuring useful experiment information allowing to get insights from machine learning experiments.

Loading a non-trivial dataset#

We use a skrub dataset that contains information about employees and their salaries. We will see that this dataset is non-trivial.

Downloading 'employee_salaries' from https://github.com/skrub-data/skrub-data-files/raw/refs/heads/main/employee_salaries.zip (attempt 1/3)

Let’s first have a condensed summary of the input data using a skrub.TableReport.

from skrub import TableReport

TableReport(df)

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").



From the table report, we can make the following observations:

  • Looking at the Table tab, we observe that the year related to the date_first_hired column is also present in the date column. Hence, we should beware of not creating twice the same feature during the feature engineering.

  • Looking at the Stats tab:

    • The type of data is heterogeneous: we mainly have categorical and date-related features.

    • The division and employee_position_title features contain a large number of categories. It is something that we should consider in our feature engineering.

  • Looking at the Associations tab, we observe that two features are holding the exact same information: department and department_name. Hence, during our feature engineering, we could potentially drop one of them if the final predictive model is sensitive to the collinearity.

In terms of target and thus the task that we want to solve, we are interested in predicting the salary of an employee given the previous features. We therefore have a regression task at end.

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").



Later in this example, we will show that skore stores similar information when a model is trained on a dataset, thus enabling us to get quick insights on the dataset used to train and test the model.

Tree-based model#

Let’s start by creating a tree-based model using some out-of-the-box tools.

For feature engineering, we use skrub’s TableVectorizer. To deal with the high cardinality of the categorical features, we use a StringEncoder to encode the categorical features.

Finally, we use a HistGradientBoostingRegressor as a base estimator, it is a rather robust model.

Modelling#

from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.pipeline import make_pipeline
from skrub import StringEncoder, TableVectorizer

hgbt_model = make_pipeline(
    TableVectorizer(high_cardinality=StringEncoder()),
    HistGradientBoostingRegressor(),
)
hgbt_model
Pipeline(steps=[('tablevectorizer', TableVectorizer()),
                ('histgradientboostingregressor',
                 HistGradientBoostingRegressor())])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.


Evaluation#

Let us compute the 5-fold cross-validation report for this model using evaluate() with splitter=5. This will return a CrossValidationReport object.

from skore import evaluate

hgbt_model_report = evaluate(hgbt_model, df, y, splitter=5, n_jobs=4)
hgbt_model_report.help()


A report provides a collection of useful information. For instance, it allows to compute on demand the predictions of the model and some performance metrics.

Side-note: performance metrics rely on the model predictions, so the report saves the predictions once at the beginning to speed up metric computations.

We can now have a look at the performance of the model with some standard metrics.

hgbt_model_report.metrics.summarize().frame()
histgradientboostingregressor_mean histgradientboostingregressor_std
metric
r2 0.911260 0.019803
rmse 8654.444928 1264.812178
mae 4630.480320 191.880224
mape 0.065230 0.002356
fit_time 7.087549 2.621123
predict_time 0.481940 0.197623


Similarly to what we saw in the previous section, the skore.CrossValidationReport also stores some information about the dataset used.

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").



The display obtained allows for a quick overview with the same HTML-based view as the skrub.TableReport we have seen earlier. In addition, you can access a skore.TableReportDisplay.plot() method to have a particular focus on one potential analysis. For instance, we can get a figure representing the correlation matrix of the dataset.

Cramer's V Correlation
<Figure size 1000x1000 with 2 Axes>

We get the results from some statistical metrics aggregated over the cross-validation splits as well as some performance metrics related to the time it took to train and test the model.

The skore.CrossValidationReport also provides a way to inspect similar information at the level of each cross-validation split by accessing an skore.EstimatorReport for each split.

hgbt_split_1 = hgbt_model_report.reports_[0]
hgbt_split_1.metrics.summarize().frame(favorability=True)
HistGradientBoostingRegressor favorability
metric
r2 0.912106 (↗︎)
rmse 8536.929670 (↘︎)
mae 4537.691211 (↘︎)
mape 0.065474 (↘︎)
fit_time 8.115261 (↘︎)
predict_time 0.623894 (↘︎)


The favorability of each metric indicates whether the metric is better when higher or lower.

Linear model#

Now that we have established a first model that serves as a baseline, we shall proceed to define a quite complex linear model: a pipeline with a complex feature engineering that uses a linear model as the base estimator.

Modelling#

import numpy as np
from sklearn.compose import make_column_transformer
from sklearn.linear_model import RidgeCV
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder, SplineTransformer
from skrub import DatetimeEncoder, DropCols, GapEncoder, ToDatetime


def periodic_spline_transformer(period, n_splines=None, degree=3):
    if n_splines is None:
        n_splines = period
    n_knots = n_splines + 1  # periodic and include_bias is True
    return SplineTransformer(
        degree=degree,
        n_knots=n_knots,
        knots=np.linspace(0, period, n_knots).reshape(n_knots, 1),
        extrapolation="periodic",
        include_bias=True,
    )


one_hot_features = ["gender", "department_name", "assignment_category"]
datetime_features = "date_first_hired"

date_encoder = make_pipeline(
    ToDatetime(),
    DatetimeEncoder(resolution="day", add_weekday=True, add_total_seconds=False),
    DropCols("date_first_hired_year"),
)

date_engineering = make_column_transformer(
    (periodic_spline_transformer(12, n_splines=6), ["date_first_hired_month"]),
    (periodic_spline_transformer(31, n_splines=15), ["date_first_hired_day"]),
    (periodic_spline_transformer(7, n_splines=3), ["date_first_hired_weekday"]),
)

feature_engineering_date = make_pipeline(date_encoder, date_engineering)

preprocessing = make_column_transformer(
    (feature_engineering_date, datetime_features),
    (OneHotEncoder(drop="if_binary", handle_unknown="ignore"), one_hot_features),
    (GapEncoder(n_components=100), "division"),
    (GapEncoder(n_components=100), "employee_position_title"),
)

linear_model = make_pipeline(preprocessing, RidgeCV(alphas=np.logspace(-3, 3, 100)))
linear_model
Pipeline(steps=[('columntransformer',
                 ColumnTransformer(transformers=[('pipeline',
                                                  Pipeline(steps=[('pipeline',
                                                                   Pipeline(steps=[('todatetime',
                                                                                    ToDatetime()),
                                                                                   ('datetimeencoder',
                                                                                    DatetimeEncoder(add_total_seconds=False,
                                                                                                    add_weekday=True,
                                                                                                    resolution='day')),
                                                                                   ('dropcols',
                                                                                    DropCols(cols='date_first_hired_year'))])),
                                                                  ('columntransformer',
                                                                   ColumnTransformer(transfor...
       4.03701726e+01, 4.64158883e+01, 5.33669923e+01, 6.13590727e+01,
       7.05480231e+01, 8.11130831e+01, 9.32603347e+01, 1.07226722e+02,
       1.23284674e+02, 1.41747416e+02, 1.62975083e+02, 1.87381742e+02,
       2.15443469e+02, 2.47707636e+02, 2.84803587e+02, 3.27454916e+02,
       3.76493581e+02, 4.32876128e+02, 4.97702356e+02, 5.72236766e+02,
       6.57933225e+02, 7.56463328e+02, 8.69749003e+02, 1.00000000e+03])))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.


In the diagram above, we can see what how we performed our feature engineering:

  • For categorical features, we use two approaches. If the number of categories is relatively small, we use a OneHotEncoder. If the number of categories is large, we use a GapEncoder that is designed to deal with high cardinality categorical features.

  • Then, we have another transformation to encode the date features. We first split the date into multiple features (day, month, and year). Then, we apply a periodic spline transformation to each of the date features in order to capture the periodicity of the data.

  • Finally, we fit a RidgeCV model.

Evaluation#

Now, we want to evaluate this linear model via cross-validation (with 5 folds). For that, we use again evaluate() with splitter=5.



We observe that the cross-validation report has detected that we have a regression task at hand and thus provides us with some metrics and plots that make sense with regards to our specific problem at hand.

We can now have a look at the performance of the model with some standard metrics.

linear_model_report.metrics.summarize().frame(favorability=True)
ridgecv_mean ridgecv_std favorability
metric
r2 0.766776 0.020667 (↗︎)
rmse 14051.772133 1037.715358 (↘︎)
mae 9914.749267 440.087987 (↘︎)
mape 0.149006 0.004719 (↘︎)
fit_time 35.898886 12.462659 (↘︎)
predict_time 2.461384 1.481144 (↘︎)


Comparing the models#

Now that we cross-validated our models, we can make some further comparison using the compare() function that returns a ComparisonReport:

from skore import compare

comparator = compare([hgbt_model_report, linear_model_report])
comparator.metrics.summarize().frame(favorability=True)
mean_histgradientboostingregressor mean_ridgecv std_histgradientboostingregressor std_ridgecv favorability
metric
r2 0.911260 0.766776 0.019803 0.020667 (↗︎)
rmse 8654.444928 14051.772133 1264.812178 1037.715358 (↘︎)
mae 4630.480320 9914.749267 191.880224 440.087987 (↘︎)
mape 0.065230 0.149006 0.002356 0.004719 (↘︎)
fit_time 7.087549 35.898886 2.621123 12.462659 (↘︎)
predict_time 0.481940 2.461384 0.197623 1.481144 (↘︎)


In addition, if we forgot to compute a specific metric (e.g. mean_absolute_error()), we can easily add it to the report, without re-training the model and even without re-computing the predictions since they are cached internally in the report. This allows us to save some potentially huge computation time.

comparator.metrics.add(metric="neg_mean_absolute_error", name="MAE")

comparator.metrics.summarize().frame()
mean_histgradientboostingregressor mean_ridgecv std_histgradientboostingregressor std_ridgecv
metric
MAE 4630.480320 9914.749267 191.880224 440.087987
r2 0.911260 0.766776 0.019803 0.020667
rmse 8654.444928 14051.772133 1264.812178 1037.715358
mae 4630.480320 9914.749267 191.880224 440.087987
mape 0.065230 0.149006 0.002356 0.004719
fit_time 7.087549 35.898886 2.621123 12.462659
predict_time 0.481940 2.461384 0.197623 1.481144


Finally, we can even get a deeper understanding by analyzing each split in the CrossValidationReport. Here, we plot the actual-vs-predicted values for each split.

_ = linear_model_report.metrics.prediction_error().plot(kind="actual_vs_predicted")
Prediction Error for RidgeCV Data source: Test set

Conclusion#

This example showcased skore’s integrated approach to machine learning workflow, from initial data exploration with TableReport through model development and evaluation with CrossValidationReport. We demonstrated how skore automatically captures dataset information and provides efficient caching, enabling quick insights and flexible model comparison. The workflow highlights skore’s ability to streamline the entire ML process while maintaining computational efficiency.

Total running time of the script: (1 minutes 13.487 seconds)

Gallery generated by Sphinx-Gallery