Skip to content

Commit d7f33a8

Browse files
committed
Merge remote-tracking branch 'upstream/HEAD' into doc-clarity-improvements
2 parents 2130334 + c50109f commit d7f33a8

25 files changed

Lines changed: 3389 additions & 649 deletions

CHANGES.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ New Features
3131
:meth:`DataOp.skb.eval`, :meth:`SkrubLearner.predict`, etc., or in
3232
:meth:`DataOp.skb.find` or :meth:`SkrubLearner.truncated_after`. :pr:`2062` by
3333
:user:`Jérôme Dockès <jeromedockes>`.
34+
- The :class:`SessionEncoder` is now available. This encoder adds a `session_id`
35+
column, which groups together events that occur within the given session gap.
36+
Additionally, it is possible to provide a ``split_by`` column or list of columns
37+
(e.g., user ID or (user ID, user device)) to compute sessions for each grouping
38+
value.
39+
:pr:`1930` by :user:`Riccardo Cappuzzo <rcap107>`.
40+
- A new synthetic dataset generator for timestamped data and session-based
41+
operations has been added: :meth:`~skrub.datasets.make_retail_events`.
42+
:pr:`1930` by :user:`Riccardo Cappuzzo <rcap107>`.
3443
- The :class:`DropSimilar` transformer has been added, for removing columns in a
3544
dataframe that present high correlation with other columns. :pr:`2023` by
3645
:user:`Eloi Massoulié <emassoulie>`.
@@ -65,6 +74,19 @@ Changes
6574
:pr:`2048` by :user:`Riccardo Cappuzzo <rcap107>`.
6675
- The minimum required version of matplotlib has been increased from 3.4.3 to 3.6.1.
6776
:pr:`2159` by :user:`Riccardo Cappuzzo <rcap107>`.
77+
- :meth:`SkrubLearner.score` has been enhanced when the DataOp used
78+
:meth:`DataOp.skb.with_scoring`. During scoring, predict(), predict_proba()
79+
etc. are cached to avoid recomputation when multiple scorers are used (or one
80+
scorer calls them several times). Moreover it is possible to pass
81+
``return_predictions=True`` to also retrieve any predictions that have been
82+
computed during scoring, in addition to the scores. Finally, in cases where we
83+
already have the predictions but want the result of score() without
84+
recomputing them, it is possible to provide them in the environment passed to
85+
``score({..., "_skrub_predictions": {"predict_proba": ...}})``.
86+
:pr:`2195` by :user:`Jérôme Dockès <jeromedockes>`.
87+
- :meth:`SkrubLearner.find_fitted_estimator` now supports searching for the
88+
apply node by ID or callable predicate as alternatives to the node name.
89+
:pr:`2194` by :user:`Jérôme Dockès <jeromedockes>`.
6890

6991
Bugfixes
7092
--------

doc/_templates/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ <h5 class="feature-title">Inspect it, apply it to new data</h5>
152152
</div>
153153
<p class="feature-text-see-also">
154154
<!-- name of the userguide file on DataOps -->
155-
<a href="{{ pathto('auto_tutorials/1110_data_ops_intro') }}">Discover the skrub DataOps →</a>
155+
<a href="{{ pathto('data_ops') }}">Discover the skrub DataOps →</a>
156156
</p>
157157
</div>
158158
<div class="col-md-7">

doc/api_reference.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"SimilarityEncoder",
9191
"ToCategorical",
9292
"DatetimeEncoder",
93+
"SessionEncoder",
9394
"ToDatetime",
9495
"ToFloat",
9596
],
@@ -341,6 +342,9 @@
341342
"datasets.get_data_dir",
342343
"datasets.make_deduplication_data",
343344
"datasets.toy_orders",
345+
"datasets.toy_products",
346+
"datasets.toy_cities",
347+
"datasets.make_retail_events",
344348
],
345349
}
346350
],

doc/data_ops.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,18 @@ they can be saved in a file, loaded, applied to new data as easily as a single
5656
etc. . Skrub DataOps remove those limitations and add several useful features
5757
such as interactive previews and integration with Optuna.
5858

59+
A quick overview of DataOps
60+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
61+
62+
This tutorial walks through the main components of the DataOps on a simple
63+
example:
64+
65+
.. toctree::
66+
:maxdepth: 1
67+
68+
auto_tutorials/1111_data_ops_quick_tour
69+
70+
5971
Data Ops basic concepts
6072
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6173

@@ -64,7 +76,6 @@ Data Ops basic concepts
6476

6577
modules/data_ops/basics/what_are_data_ops
6678
modules/data_ops/basics/building_data_ops_plan
67-
auto_tutorials/1110_data_ops_intro
6879
modules/data_ops/basics/using_previews
6980
modules/data_ops/basics/direct_access_methods
7081
modules/data_ops/basics/control_flow

doc/data_ops_report.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,16 @@ def create_employee_salaries_report():
7474
if (output_dir / "index.html").exists():
7575
return output_dir
7676

77-
dataset = skrub.datasets.fetch_employee_salaries(split="train").employee_salaries
78-
data_var = skrub.var("data", dataset)
79-
X = data_var.drop("current_annual_salary", axis=1).skb.mark_as_X()
80-
y = data_var["current_annual_salary"].skb.mark_as_y()
81-
82-
vectorizer = TableVectorizer()
83-
X_vec = X.skb.apply(vectorizer)
77+
pred = (
78+
skrub.var("employee_data")
79+
.skb.apply(TableVectorizer())
80+
.skb.apply(HistGradientBoostingRegressor(), y=skrub.var("salary"))
81+
)
8482

85-
hgb = HistGradientBoostingRegressor()
86-
predictor = X_vec.skb.apply(hgb, y=y)
83+
dataset = skrub.datasets.fetch_employee_salaries(split="train")
8784

88-
predictor.skb.full_report(
85+
pred.skb.full_report(
86+
{"employee_data": dataset.X, "salary": dataset.y},
8987
output_dir=output_dir,
9088
overwrite=True,
9189
open=False,

doc/modules/data_ops/basics/building_data_ops_plan.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,5 @@ By working only on Data Ops we ensure that all the operations done on the data
9090
are added correctly to the computational graph, which then allows the resulting
9191
learner to execute all steps as intended.
9292

93-
See :ref:`sphx_glr_auto_tutorials_1110_data_ops_intro.py` for an introductory
93+
See :ref:`sphx_glr_auto_tutorials_1111_data_ops_quick_tour.py` for an introductory
9494
example on how to use skrub DataOps on a single dataframe.

doc/modules/data_ops/basics/control_flow.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ Finally, there are other situations where using :func:`deferred` can be helpful:
168168

169169
.. rubric:: Examples
170170

171-
- See :ref:`sphx_glr_auto_tutorials_1110_data_ops_intro.py` for an introductory
171+
- See :ref:`sphx_glr_auto_examples_data_ops_1111_data_ops_quick_tour.py` for an introductory
172172
example on how to use skrub DataOps on a single dataframe.
173173
- See :ref:`sphx_glr_auto_examples_02_data_ops_1120_multiple_tables.py` for an example
174174
of how skrub DataOps can be used to process multiple tables using dataframe APIs.

doc/modules/data_ops/validation/tuning_validating_data_ops.rst

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,75 @@ SkrubLearner(data_op=<Scoring <Apply DummyClassifier> (1 scorers)>
256256
Note that the score above is negative: it is the negative log loss we passed to
257257
``with_scoring``, and not the default score (accuracy, which would be positive).
258258

259+
If we also want to recover the default score that would be returned by the
260+
applied estimator's ``score()`` method (what we would get if we did not use
261+
:meth:`DataOp.skb.with_scoring`), we can pass ``None`` as the scorer, and the
262+
default corresponding key in the result is ``"score"`` (exactly like in
263+
:func:`~sklearn.model_selection.cross_validate`):
264+
265+
>>> (
266+
... pred.skb.with_scoring(None)
267+
... .skb.with_scoring('accuracy')
268+
... .skb.with_scoring('roc_auc')
269+
... .skb.make_learner()
270+
... .fit(split["train"])
271+
... .score(split["test"])
272+
... )
273+
{'score': 0.6666666666666666, 'accuracy': 0.6666666666666666, 'roc_auc': 0.5}
274+
259275
:meth:`DataOp.skb.with_scoring` only changes how scoring is performed
260276
(the outputs of :meth:`DataOp.skb.cross_validate`,
261277
:meth:`DataOp.skb.make_randomized_search`, :class:`SkrubLearner.score <SkrubLearner>` etc.),
262-
**not** the actual outputs of the learner (it does _not_ affect the outputs of
278+
**not** the actual outputs of the learner (it does *not* affect the outputs of
263279
:meth:`DataOp.skb.eval`, :class:`SkrubLearner.predict <SkrubLearner>`, etc.)
264280

265281
This method can be called several times to add scorers that take different
266282
kwargs. See the reference documentation for details.
283+
284+
Avoiding computing predictions multiple times
285+
=============================================
286+
287+
This section gives a few tips to avoid recomputing predictions, which is
288+
particularly important for pipelines for which inference is expensive, such as
289+
those using the :class:`TextEncoder` or Tabular Foundation Models such as
290+
`TabICL <https://tabicl.readthedocs.io/en/latest/>`_.
291+
292+
When :meth:`DataOp.skb.with_scoring` is used, predictions are cached during
293+
scoring so that if multiple scorers call the same function (e.g. ``predict()``
294+
or ``predict_proba()``) the computation runs only once. Moreover, if we also
295+
need the predictions in addition to the scores (for example to create plots,
296+
study calibration, etc.) we can pass ``return_predictions=True`` to the
297+
learner's ``score()`` method.
298+
299+
Instead of doing the following, which computes the predictions twice:
300+
301+
>>> scores = learner.score(split['test'])
302+
>>> predictions = learner.predict(split['test']) # used for plotting etc.
303+
304+
We can write:
305+
306+
>>> scores, predictions = learner.score(split['test'], return_predictions=True)
307+
>>> scores # doctest: +SKIP
308+
{'neg_log_loss': -0.6365141682948128}
309+
>>> predictions
310+
{'predict_proba': array([[0.66666667, 0.33333333],
311+
[0.66666667, 0.33333333],
312+
[0.66666667, 0.33333333]])}
313+
314+
The returned dictionary contains any predictions of the pipeline that have been
315+
computed as part of scoring. When :meth:`DataOp.skb.with_scoring` has not been
316+
used, it will always be empty.
317+
318+
If we happen to already have the predictions for the data we are scoring on, but
319+
would like to use the learner's ``score`` method to get the scores, we can
320+
inject the precomputed predictions in the input environment, under the special
321+
key ``"_skrub_predictions"``:
322+
323+
>>> predict_proba = learner.predict_proba(split['test'])
324+
>>> learner.score(split['test'] | {'_skrub_predictions': {'predict_proba': predict_proba}})
325+
{'neg_log_loss': -0.63...}
326+
327+
With the above, ``predict()`` is not called during scoring. As for
328+
``return_predictions``, this only applies to scorers added with
329+
:meth:`DataOp.skb.with_scoring`. If no scorer has been configured
330+
``'_skrub_predictions'`` will be ignored.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
.. _sessionization:
2+
3+
.. |SessionEncoder| replace:: :class:`~skrub.SessionEncoder`
4+
.. |BaseEstimator| replace:: :class:`~sklearn.base.BaseEstimator`
5+
.. |TransformerMixin| replace:: :class:`~sklearn.base.TransformerMixin`
6+
7+
8+
Detecting sessions in timestamped data with the SessionEncoder
9+
----------------------------------------------------------------
10+
11+
When dealing with timestamped data (data that includes at least a timestamp column),
12+
it may be beneficial to try and identify groups of events as
13+
`"sessions" <https://en.wikipedia.org/wiki/Session_(web_analytics)>`__,
14+
through **sessionization**.
15+
16+
Sessionization is the process of grouping a sequence of events (like user
17+
interactions) into meaningful sessions.
18+
For example, in an online retail context you might define a new session whenever
19+
more than 30 minutes pass with no activity from the user. On a website, a session may
20+
define a sequence of requests made by a single end-user within a certain time duration.
21+
22+
While definitions may vary depending on the specific use case, being able to detect
23+
such "bursts" of activity by a user can often help with building features that have
24+
greater predictive power than raw individual events, such as number of sessions or
25+
average session duration.
26+
27+
The |SessionEncoder| addresses this problem by detecting sessions based on
28+
a timestamp column, other session-related columns (e.g., user and device) that should be
29+
used to distinguish between sessions, and a ``session_gap``. Session-related columns
30+
-- identified by the ``split_by`` parameter -- allow to split sessions based on
31+
the provided parameters, for example to group user actions only if they were conducted
32+
on the same device.
33+
34+
A session is then defined as a sequence of events that share the same value in the
35+
``split_by`` columns, and whose events are closer to each other than the
36+
``session_gap``.
37+
38+
>>> from skrub import SessionEncoder
39+
>>> from skrub.datasets import make_retail_events
40+
>>> events = make_retail_events(n_events=100, random_state=0)
41+
>>> X, y = events.X, events.y
42+
43+
Once the necessary features are provided, the |SessionEncoder|
44+
returns a dataframe that includes a ``timestamp_session_id`` column, which is
45+
composed of a distinct integer ID for each session:
46+
47+
>>> se = SessionEncoder(timestamp_col="timestamp", split_by="user_id", session_gap=30 * 60)
48+
>>> res = se.fit_transform(X)
49+
>>> res.head(5) # doctest: +SKIP
50+
user_id timestamp device_type page_category event_type time_on_page price_viewed timestamp_session_id
51+
0 user_0164 2024-01-01 03:29:07.708922+00:00 mobile fashion page_view 134.1 309.80 59
52+
1 user_0164 2024-01-01 03:29:42.185048+00:00 tablet books search 103.4 11.00 59
53+
2 user_0164 2024-01-01 03:32:38.352703+00:00 desktop home wishlist 180.3 4.80 59
54+
3 user_0008 2024-01-02 10:49:56.974375+00:00 mobile books page_view 7.0 33.94 2
55+
4 user_0149 2024-01-04 10:00:15.882835+00:00 desktop electronics page_view 108.5 4.44 49
56+
57+
With the session ID, it becomes possible to compute aggregations on
58+
each session, for example to find the duration or number of sessions
59+
by a user.
60+
61+
.. warning::
62+
63+
Caution! Aggregation can introduce data leakage. Records should only be aggregated from
64+
within the training set at training time, and the test set at predict time. To
65+
ensure this is the case, any code that performs aggregation can be wrapped in a
66+
scikit-learn |BaseEstimator| (as shown in the
67+
:ref:`SessionEncoder example <sphx_glr_auto_examples_0110_session_encoder.py>`),
68+
otherwise the pipeline should use the skrub :ref:`Data Ops framework<user_guide_data_ops_plan>`.
69+
70+
The |SessionEncoder| includes the ``suffix`` parameter (by default
71+
``suffix="session_id"``) to specify what the name of the new column should be.
72+
This can help with creating multiple session IDs based on the same timestamp.
73+
For example, we might want to create sessions based on users, and based on users
74+
and their device:
75+
76+
>>> se = SessionEncoder(timestamp_col="timestamp",
77+
... split_by="user_id",
78+
... session_gap=30 * 60,
79+
... suffix="user"
80+
... )
81+
>>> res = se.fit_transform(X)
82+
>>> res.head(5) # doctest: +SKIP
83+
user_id timestamp ... price_viewed timestamp_user
84+
0 user_0164 2024-01-01 03:29:07.708922+00:00 ... 309.80 59
85+
1 user_0164 2024-01-01 03:29:42.185048+00:00 ... 11.00 59
86+
2 user_0164 2024-01-01 03:32:38.352703+00:00 ... 4.80 59
87+
3 user_0008 2024-01-02 10:49:56.974375+00:00 ... 33.94 2
88+
4 user_0149 2024-01-04 10:00:15.882835+00:00 ... 4.44 49
89+
90+
>>> se = SessionEncoder(timestamp_col="timestamp",
91+
... split_by=["user_id", "device_type"],
92+
... session_gap=30 * 60,
93+
... suffix="user_device"
94+
... )
95+
>>> res = se.fit_transform(X)
96+
>>> res.head(5) # doctest: +SKIP
97+
user_id timestamp ... price_viewed timestamp_user_device
98+
0 user_0164 2024-01-01 03:29:07.708922+00:00 ... 309.80 75
99+
1 user_0164 2024-01-01 03:29:42.185048+00:00 ... 11.00 76
100+
2 user_0164 2024-01-01 03:32:38.352703+00:00 ... 4.80 74
101+
3 user_0008 2024-01-02 10:49:56.974375+00:00 ... 33.94 2
102+
4 user_0149 2024-01-04 10:00:15.882835+00:00 ... 4.44 59
103+
104+
The |SessionEncoder| has additional features that are detailed in the relevant docstring.
105+
You can also find a working :ref:`example <sphx_glr_auto_examples_0110_session_encoder.py>`
106+
in the gallery.

doc/multi_column_operations.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ multiple columns.
1515
modules/multi_column_operations/selectors
1616
modules/multi_column_operations/type_of_selectors
1717
modules/multi_column_operations/advanced_selectors
18+
modules/multi_column_operations/sessionization

0 commit comments

Comments
 (0)