Skip to content

Commit 0ceace6

Browse files
committed
Pushing the docs to dev/ for branch: main, commit c50109f1e80977aac749fc4e7e13ca0fdb3ab00b
1 parent 09f0f70 commit 0ceace6

326 files changed

Lines changed: 122556 additions & 70190 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dev/CHANGES.html

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,19 @@ <h3>New Features<a class="headerlink" href="#new-features" title="Link to this h
614614
<a class="reference internal" href="reference/generated/skrub.DataOp.skb.eval.html#skrub.DataOp.skb.eval" title="skrub.DataOp.skb.eval"><code class="xref py py-meth docutils literal notranslate"><span class="pre">DataOp.skb.eval()</span></code></a>, <code class="xref py py-meth docutils literal notranslate"><span class="pre">SkrubLearner.predict()</span></code>, etc., or in
615615
<a class="reference internal" href="reference/generated/skrub.DataOp.skb.find.html#skrub.DataOp.skb.find" title="skrub.DataOp.skb.find"><code class="xref py py-meth docutils literal notranslate"><span class="pre">DataOp.skb.find()</span></code></a> or <a class="reference internal" href="reference/generated/skrub.SkrubLearner.html#skrub.SkrubLearner.truncated_after" title="skrub.SkrubLearner.truncated_after"><code class="xref py py-meth docutils literal notranslate"><span class="pre">SkrubLearner.truncated_after()</span></code></a>. <a class="reference external" href="https://github.com/skrub-data/skrub/pull/2062">#2062</a> by
616616
<a class="reference external" href="https://github.com/jeromedockes">Jérôme Dockès</a>.</p></li>
617+
<li><p>The <a class="reference internal" href="reference/generated/skrub.SessionEncoder.html#skrub.SessionEncoder" title="skrub.SessionEncoder"><code class="xref py py-class docutils literal notranslate"><span class="pre">SessionEncoder</span></code></a> is now available. This encoder adds a <code class="docutils literal notranslate"><span class="pre">session_id</span></code>
618+
column, which groups together events that occur within the given session gap.
619+
Additionally, it is possible to provide a <code class="docutils literal notranslate"><span class="pre">split_by</span></code> column or list of columns
620+
(e.g., user ID or (user ID, user device)) to compute sessions for each grouping
621+
value.
622+
<a class="reference external" href="https://github.com/skrub-data/skrub/pull/1930">#1930</a> by <a class="reference external" href="https://github.com/rcap107">Riccardo Cappuzzo</a>.</p></li>
623+
<li><p>A new synthetic dataset generator for timestamped data and session-based</p></li>
624+
</ul>
625+
<blockquote>
626+
<div><p>operations has been added: <a class="reference internal" href="reference/generated/skrub.datasets.make_retail_events.html#skrub.datasets.make_retail_events" title="skrub.datasets.make_retail_events"><code class="xref py py-meth docutils literal notranslate"><span class="pre">make_retail_events()</span></code></a>.
627+
<a class="reference external" href="https://github.com/skrub-data/skrub/pull/1930">#1930</a> by <a class="reference external" href="https://github.com/rcap107">Riccardo Cappuzzo</a>.</p>
628+
</div></blockquote>
629+
<ul class="simple">
617630
<li><p>The <code class="xref py py-class docutils literal notranslate"><span class="pre">DropSimilar</span></code> transformer has been added, for removing columns in a
618631
dataframe that present high correlation with other columns. <a class="reference external" href="https://github.com/skrub-data/skrub/pull/2023">#2023</a> by
619632
<a class="reference external" href="https://github.com/emassoulie">Eloi Massoulié</a>.</p></li>
@@ -718,7 +731,7 @@ <h3>New Features<a class="headerlink" href="#id2" title="Link to this heading">#
718731
<li><p><a class="reference internal" href="reference/generated/skrub.selectors.has_dtype.html#skrub.selectors.has_dtype" title="skrub.selectors.has_dtype"><code class="xref py py-func docutils literal notranslate"><span class="pre">selectors.has_dtype()</span></code></a> has been added, allowing users to select columns
719732
by passing the dtype objects they want to match. <a class="reference external" href="https://github.com/skrub-data/skrub/pull/2027">#2027</a> by
720733
<a class="reference external" href="https://github.com/kudos07">kudos07</a>.</p></li>
721-
<li><p>A new dataframe generator, <code class="xref py py-func docutils literal notranslate"><span class="pre">datasets.toy_cities()</span></code>, has been added for
734+
<li><p>A new dataframe generator, <a class="reference internal" href="reference/generated/skrub.datasets.toy_cities.html#skrub.datasets.toy_cities" title="skrub.datasets.toy_cities"><code class="xref py py-func docutils literal notranslate"><span class="pre">datasets.toy_cities()</span></code></a>, has been added for
722735
use cases on dataframes with variable sizes and variable correlation between
723736
columns. <a class="reference external" href="https://github.com/skrub-data/skrub/pull/2042">#2042</a> by <a class="reference external" href="https://github.com/emassoulie">Eloi Massoulié</a>.</p></li>
724737
<li><p>A new selector function, <code class="xref py py-func docutils literal notranslate"><span class="pre">selectors.drop()</span></code>, has been added to drop columns
Binary file not shown.
Binary file not shown.
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"\n# Sessions in time-based data: Predicting user purchases with the SessionEncoder\n\n.. |SessionEncoder| replace:: :class:`~skrub.SessionEncoder`\n.. |make_retail_events| replace:: :func:`~skrub.datasets.make_retail_events`\n.. |tabular_pipeline| replace:: :func:`~skrub.tabular_pipeline`\n.. |TableVectorizer| replace:: :class:`~skrub.TableVectorizer`\n.. |DummyClassifier| replace:: :class:`~sklearn.dummy.DummyClassifier`\n.. |TimeSeriesSplit| replace:: :class:`~sklearn.model_selection.TimeSeriesSplit`\n.. |BaseEstimator| replace:: :class:`~sklearn.base.BaseEstimator`\n.. |TransformerMixin| replace:: :class:`~sklearn.base.TransformerMixin`\n\nThis example shows how to use |SessionEncoder| in a scikit-learn pipeline to\ncreate session-level features (sessionization) for conversion prediction, that is\npredicting whether a user session will eventually lead to a purchase.\n\n.. topic:: What is sessionization?\n\n Sessionization is the process of grouping a sequence of events (like user\n interactions) into meaningful sessions. A session typically starts fresh or\n after a period of inactivity. For example, in an online retail context, you\n might define a new session whenever more than 30 minutes pass with no activity\n from a user. This allows you to extract session-level features (like the total\n number of events in a session or the dominant device type used) which often have\n greater predictive power than raw individual events.\n\nWe will:\n\n1. Use |make_retail_events| to generate synthetic retail event data\n2. Build a baseline classifier on raw event-level features with the |tabular_pipeline|\n3. Add session-level and historical features with |SessionEncoder|\n4. Train the same model again and compare ROC-AUC\n\nThe data includes columns such as event type, device type, viewed price, and\ntimestamp. The target is binary: whether the session eventually contains a\npurchase event or not.\n"
8+
]
9+
},
10+
{
11+
"cell_type": "markdown",
12+
"metadata": {},
13+
"source": [
14+
"Since this is temporal data, we use a time-aware CV strategy with\n|TimeSeriesSplit| to avoid leakage. We reuse the same splitter for all evaluations.\nThe dataset is sorted by timestamp, so the training set will always contain only\npast data relative to the test set.\n\n"
15+
]
16+
},
17+
{
18+
"cell_type": "code",
19+
"execution_count": null,
20+
"metadata": {
21+
"collapsed": false
22+
},
23+
"outputs": [],
24+
"source": [
25+
"from sklearn.model_selection import TimeSeriesSplit\n\nsplitter = TimeSeriesSplit(n_splits=5)"
26+
]
27+
},
28+
{
29+
"cell_type": "markdown",
30+
"metadata": {},
31+
"source": [
32+
"We begin by generating the data with |make_retail_events| and defining our\nfeatures and target.\n\n"
33+
]
34+
},
35+
{
36+
"cell_type": "code",
37+
"execution_count": null,
38+
"metadata": {
39+
"collapsed": false
40+
},
41+
"outputs": [],
42+
"source": [
43+
"from skrub import TableReport\nfrom skrub.datasets import make_retail_events\n\nevents = make_retail_events(n_users=20, n_events=5000, random_state=0)\nX, y = events.X, events.y\nTableReport(X)"
44+
]
45+
},
46+
{
47+
"cell_type": "markdown",
48+
"metadata": {},
49+
"source": [
50+
"The data contains 5000 events from 20 users, where each event is timestamped.\nOther columns include the event type, device used by the user, page category,\ntime spent on page and price of the item. The target variable indicates whether\na user session eventually contains a purchase event: all events in that session\nwill have a target value of 1 if a purchase happens, and 0 otherwise.\n\n"
51+
]
52+
},
53+
{
54+
"cell_type": "markdown",
55+
"metadata": {},
56+
"source": [
57+
"## Sanity check: evaluate a DummyClassifier on raw event data\nWe begin by evaluating a |DummyClassifier| on the original event data\n(without session features). Since it's a |DummyClassifier|, we expect\nchance-level performance (ROC-AUC of 0.5).\n\n"
58+
]
59+
},
60+
{
61+
"cell_type": "code",
62+
"execution_count": null,
63+
"metadata": {
64+
"collapsed": false
65+
},
66+
"outputs": [],
67+
"source": [
68+
"from sklearn.dummy import DummyClassifier\nfrom sklearn.model_selection import cross_val_score\n\ndummy = DummyClassifier(strategy=\"most_frequent\")\n\nscores = cross_val_score(dummy, X, y, cv=splitter, scoring=\"roc_auc\")\nprint(f\"ROC-AUC with DummyClassifier: {scores.mean():.3f}\")"
69+
]
70+
},
71+
{
72+
"cell_type": "markdown",
73+
"metadata": {},
74+
"source": [
75+
"## First attempt: training a model without using session-level features\nWe first use the |tabular_pipeline| on raw event-level data, without any session\nencoding or aggregation. This serves as a baseline to compare against the enriched\nmodel later.\nRemember that the |tabular_pipeline| will automatically add a |TableVectorizer|\nto perform feature engineering, so the model can still learn from the raw event\nfeatures. However, it won't be able to directly capture session-level patterns.\n\n"
76+
]
77+
},
78+
{
79+
"cell_type": "code",
80+
"execution_count": null,
81+
"metadata": {
82+
"collapsed": false
83+
},
84+
"outputs": [],
85+
"source": [
86+
"from skrub import tabular_pipeline\n\nmodel = tabular_pipeline(\"classification\")\n\nscores = cross_val_score(model, X, y, cv=splitter, scoring=\"roc_auc\")\nprint(f\"ROC-AUC without session encoding: {scores.mean():.3f}\")"
87+
]
88+
},
89+
{
90+
"cell_type": "markdown",
91+
"metadata": {},
92+
"source": [
93+
"The model is not performing much better than the DummyClassifier, which suggests\nthat raw event-level features are not sufficient for good conversion prediction.\nThis baseline is limited because it cannot directly use session-level behavior\n(for example, whether \"add_to_cart\" happened in the same session).\n\n"
94+
]
95+
},
96+
{
97+
"cell_type": "markdown",
98+
"metadata": {},
99+
"source": [
100+
"## A better approach: session encoding and aggregation\nNext, we use the |SessionEncoder| to create session-level features that we can\naggregate over. We define a session boundary as \"a user has been inactive for\nmore than 30 minutes\". The |SessionEncoder| will create a new column\n``timestamp_session_id`` that assigns a unique session ID to each session detected.\nThe parameter ``session_gap=30 * 60`` specifies the inactivity threshold in\nseconds (30 minutes).\n\nNote that session-based features involve aggregations, which must be performed\nonly on the training data within each fold to avoid leakage. In a scikit-learn\npipeline, we can achieve this by using |SessionEncoder| followed by a custom\ntransformer that computes session aggregates, and ensures that the pipeline is\nproperly fitted within each fold of cross-validation.\n\n"
101+
]
102+
},
103+
{
104+
"cell_type": "code",
105+
"execution_count": null,
106+
"metadata": {
107+
"collapsed": false
108+
},
109+
"outputs": [],
110+
"source": [
111+
"from skrub import SessionEncoder, tabular_pipeline\n\nse = SessionEncoder(\"timestamp\", split_by=\"user_id\", session_gap=30 * 60)\n# Here we fit the SessionEncoder on the entire dataset for demonstration purposes\nX_sessions = se.fit_transform(X)\nX_sessions.head()"
112+
]
113+
},
114+
{
115+
"cell_type": "markdown",
116+
"metadata": {},
117+
"source": [
118+
"## Defining a custom transformer for session-level aggregation\nTo avoid data leakage and maintain a clean pipeline, we can create a custom\ntransformer that inherits from |BaseEstimator| and |TransformerMixin| and\ncomputes session-level aggregates within a scikit-learn pipeline.\nThis transformer will be fitted and applied separately within each fold of\ncross-validation, ensuring that session features are computed only on the training\ndata of each fold.\n\n"
119+
]
120+
},
121+
{
122+
"cell_type": "code",
123+
"execution_count": null,
124+
"metadata": {
125+
"collapsed": false
126+
},
127+
"outputs": [],
128+
"source": [
129+
"from sklearn.base import BaseEstimator, TransformerMixin\n\n\nclass SessionAggregator(BaseEstimator, TransformerMixin):\n def fit(self, X, y=None):\n return self\n\n def transform(self, X):\n # Compute session-level aggregates\n session_agg = X.groupby(\"timestamp_session_id\").agg(\n session_has_add_to_cart=(\"event_type\", lambda x: \"add_to_cart\" in x.values),\n session_n_events=(\"event_type\", \"count\"),\n session_mean_price=(\"price_viewed\", \"mean\"),\n session_dominant_device=(\"device_type\", lambda x: x.mode()[0]),\n )\n # Join back to the original data\n return X.merge(\n session_agg,\n how=\"left\",\n on=\"timestamp_session_id\",\n )"
130+
]
131+
},
132+
{
133+
"cell_type": "markdown",
134+
"metadata": {},
135+
"source": [
136+
"Then, we create a pipeline that includes the |SessionEncoder|, our custom\n``SessionAggregator``, and the |tabular_pipeline| for classification. This\npipeline will be used in cross-validation to evaluate the model\nwith session features.\n\n"
137+
]
138+
},
139+
{
140+
"cell_type": "code",
141+
"execution_count": null,
142+
"metadata": {
143+
"collapsed": false
144+
},
145+
"outputs": [],
146+
"source": [
147+
"from sklearn.pipeline import make_pipeline\n\nmodel = make_pipeline(se, SessionAggregator(), tabular_pipeline(\"classification\"))\nscores = cross_val_score(model, X, y, cv=splitter, scoring=\"roc_auc\")\nprint(\"ROC-AUC with session encoding:\", scores.mean())"
148+
]
149+
},
150+
{
151+
"cell_type": "markdown",
152+
"metadata": {},
153+
"source": [
154+
"As expected the model with session encoding performs much better than the baseline\nwithout session features, demonstrating the value of sessionization for conversion\nprediction.\n\nThe fact that we are working with aggregation means that it was necessary to\ncreate a custom transformer to compute session-level features. However, this situation\ncan be avoided entirely by using the skrub DataOps workflow, which allows for more\nflexible data transformations without needing to fit everything within a\nscikit-learn pipeline.\n\n"
155+
]
156+
}
157+
],
158+
"metadata": {
159+
"kernelspec": {
160+
"display_name": "Python 3",
161+
"language": "python",
162+
"name": "python3"
163+
},
164+
"language_info": {
165+
"codemirror_mode": {
166+
"name": "ipython",
167+
"version": 3
168+
},
169+
"file_extension": ".py",
170+
"mimetype": "text/x-python",
171+
"name": "python",
172+
"nbconvert_exporter": "python",
173+
"pygments_lexer": "ipython3",
174+
"version": "3.12.13"
175+
}
176+
},
177+
"nbformat": 4,
178+
"nbformat_minor": 0
179+
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)