Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite or Binder.
Sessions in time-based data: Predicting user purchases with the SessionEncoder#
This example shows how to use SessionEncoder in a scikit-learn pipeline to
create session-level features (sessionization) for conversion prediction, that is
predicting whether a user session will eventually lead to a purchase.
We will:
Use
make_retail_events()to generate synthetic retail event dataBuild a baseline classifier on raw event-level features with the
tabular_pipeline()Add session-level and historical features with
SessionEncoderTrain the same model again and compare ROC-AUC
The data includes columns such as event type, device type, viewed price, and timestamp. The target is binary: whether the session eventually contains a purchase event or not.
Since this is temporal data, we use a time-aware CV strategy with
TimeSeriesSplit to avoid leakage. We reuse the same splitter for all evaluations.
The dataset is sorted by timestamp, so the training set will always contain only
past data relative to the test set.
from sklearn.model_selection import TimeSeriesSplit
splitter = TimeSeriesSplit(n_splits=5)
We begin by generating the data with make_retail_events() and defining our
features and target.
from skrub import TableReport
from skrub.datasets import make_retail_events
events = make_retail_events(n_users=20, n_events=5000, random_state=0)
X, y = events.X, events.y
TableReport(X)
| user_id | timestamp | device_type | page_category | event_type | time_on_page | price_viewed | |
|---|---|---|---|---|---|---|---|
| 0 | user_0019 | 2024-01-01 01:00:27.612852+00:00 | mobile | home | page_view | 19.8 | 90.1 |
| 1 | user_0019 | 2024-01-01 01:06:43.178374+00:00 | mobile | books | add_to_cart | 38.5 | 13.4 |
| 2 | user_0019 | 2024-01-01 01:07:54.816525+00:00 | mobile | electronics | search | 146. | 669. |
| 3 | user_0019 | 2024-01-01 01:08:50.901138+00:00 | desktop | books | page_view | 308. | 59.3 |
| 4 | user_0015 | 2024-01-01 01:48:01.240749+00:00 | tablet | fashion | page_view | 78.6 | 52.1 |
| 4,995 | user_0003 | 2024-04-29 01:40:14.690176+00:00 | mobile | fashion | wishlist | 18.9 | 6.64 |
| 4,996 | user_0003 | 2024-04-29 01:42:25.974779+00:00 | desktop | fashion | add_to_cart | 149. | 70.7 |
| 4,997 | user_0003 | 2024-04-29 01:43:31.440150+00:00 | mobile | sports | page_view | 159. | 11.9 |
| 4,998 | user_0003 | 2024-04-29 03:54:06.562733+00:00 | mobile | home | page_view | 11.0 | 258. |
| 4,999 | user_0003 | 2024-04-29 04:00:12.883112+00:00 | mobile | fashion | page_view | 103. | 3.72 |
user_id
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 20 (0.4%)
Most frequent values
user_0008
user_0013
user_0007
user_0019
user_0017
user_0018
user_0010
user_0001
user_0000
user_0005
['user_0008', 'user_0013', 'user_0007', 'user_0019', 'user_0017', 'user_0018', 'user_0010', 'user_0001', 'user_0000', 'user_0005']
timestamp
DatetimeTZDtype- Null values
- 0 (0.0%)
- Unique values
-
5,000 (100.0%)
This column has a high cardinality (> 40).
- Min | Max
- 2024-01-01T01:00:27.612852+00:00 | 2024-04-29T04:00:12.883112+00:00
device_type
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 3 (< 0.1%)
Most frequent values
mobile
desktop
tablet
['mobile', 'desktop', 'tablet']
page_category
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 5 (0.1%)
Most frequent values
home
books
sports
fashion
electronics
['home', 'books', 'sports', 'fashion', 'electronics']
event_type
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 4 (< 0.1%)
Most frequent values
page_view
search
add_to_cart
wishlist
['page_view', 'search', 'add_to_cart', 'wishlist']
time_on_page
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
2,427 (48.5%)
This column has a high cardinality (> 40).
- Mean ± Std
- 120. ± 118.
- Median ± IQR
- 84.1 ± 130.
- Min | Max
- 0.100 | 968.
price_viewed
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
3,955 (79.1%)
This column has a high cardinality (> 40).
- Mean ± Std
- 68.3 ± 117.
- Median ± IQR
- 31.8 ± 62.1
- Min | Max
- 0.300 | 1.86e+03
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 | user_id | StringDtype | False | 0 (0.0%) | 20 (0.4%) | |||||
| 1 | timestamp | DatetimeTZDtype | True | 0 (0.0%) | 5000 (100.0%) | 2024-01-01T01:00:27.612852+00:00 | 2024-04-29T04:00:12.883112+00:00 | |||
| 2 | device_type | StringDtype | False | 0 (0.0%) | 3 (< 0.1%) | |||||
| 3 | page_category | StringDtype | False | 0 (0.0%) | 5 (0.1%) | |||||
| 4 | event_type | StringDtype | False | 0 (0.0%) | 4 (< 0.1%) | |||||
| 5 | time_on_page | Float64DType | False | 0 (0.0%) | 2427 (48.5%) | 120. | 118. | 0.100 | 84.1 | 968. |
| 6 | price_viewed | Float64DType | False | 0 (0.0%) | 3955 (79.1%) | 68.3 | 117. | 0.300 | 31.8 | 1.86e+03 |
No columns match the selected filter: . You can change the column filter in the dropdown menu above.
user_id
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 20 (0.4%)
Most frequent values
user_0008
user_0013
user_0007
user_0019
user_0017
user_0018
user_0010
user_0001
user_0000
user_0005
['user_0008', 'user_0013', 'user_0007', 'user_0019', 'user_0017', 'user_0018', 'user_0010', 'user_0001', 'user_0000', 'user_0005']
timestamp
DatetimeTZDtype- Null values
- 0 (0.0%)
- Unique values
-
5,000 (100.0%)
This column has a high cardinality (> 40).
- Min | Max
- 2024-01-01T01:00:27.612852+00:00 | 2024-04-29T04:00:12.883112+00:00
device_type
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 3 (< 0.1%)
Most frequent values
mobile
desktop
tablet
['mobile', 'desktop', 'tablet']
page_category
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 5 (0.1%)
Most frequent values
home
books
sports
fashion
electronics
['home', 'books', 'sports', 'fashion', 'electronics']
event_type
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 4 (< 0.1%)
Most frequent values
page_view
search
add_to_cart
wishlist
['page_view', 'search', 'add_to_cart', 'wishlist']
time_on_page
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
2,427 (48.5%)
This column has a high cardinality (> 40).
- Mean ± Std
- 120. ± 118.
- Median ± IQR
- 84.1 ± 130.
- Min | Max
- 0.100 | 968.
price_viewed
Float64DType- Null values
- 0 (0.0%)
- Unique values
-
3,955 (79.1%)
This column has a high cardinality (> 40).
- Mean ± Std
- 68.3 ± 117.
- Median ± IQR
- 31.8 ± 62.1
- Min | Max
- 0.300 | 1.86e+03
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 |
|---|---|---|---|
| user_id | timestamp | 0.117 | |
| time_on_page | price_viewed | 0.0798 | 0.0205 |
| user_id | event_type | 0.0650 | |
| timestamp | event_type | 0.0615 | |
| user_id | price_viewed | 0.0600 | |
| user_id | page_category | 0.0594 | |
| user_id | time_on_page | 0.0580 | |
| user_id | device_type | 0.0544 | |
| timestamp | page_category | 0.0543 | |
| device_type | time_on_page | 0.0504 | |
| page_category | time_on_page | 0.0503 | |
| device_type | price_viewed | 0.0496 | |
| event_type | time_on_page | 0.0494 | |
| timestamp | device_type | 0.0479 | |
| timestamp | time_on_page | 0.0476 | |
| page_category | price_viewed | 0.0467 | |
| event_type | price_viewed | 0.0441 | |
| timestamp | price_viewed | 0.0410 | |
| device_type | event_type | 0.0377 | |
| page_category | event_type | 0.0372 | |
| device_type | page_category | 0.0307 |
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 data contains 5000 events from 20 users, where each event is timestamped. Other columns include the event type, device used by the user, page category, time spent on page and price of the item. The target variable indicates whether a user session eventually contains a purchase event: all events in that session will have a target value of 1 if a purchase happens, and 0 otherwise.
Sanity check: evaluate a DummyClassifier on raw event data#
We begin by evaluating a DummyClassifier on the original event data
(without session features). Since it’s a DummyClassifier, we expect
chance-level performance (ROC-AUC of 0.5).
from sklearn.dummy import DummyClassifier
from sklearn.model_selection import cross_val_score
dummy = DummyClassifier(strategy="most_frequent")
scores = cross_val_score(dummy, X, y, cv=splitter, scoring="roc_auc")
print(f"ROC-AUC with DummyClassifier: {scores.mean():.3f}")
ROC-AUC with DummyClassifier: 0.500
First attempt: training a model without using session-level features#
We first use the tabular_pipeline() on raw event-level data, without any session
encoding or aggregation. This serves as a baseline to compare against the enriched
model later.
Remember that the tabular_pipeline() will automatically add a TableVectorizer
to perform feature engineering, so the model can still learn from the raw event
features. However, it won’t be able to directly capture session-level patterns.
from skrub import tabular_pipeline
model = tabular_pipeline("classification")
scores = cross_val_score(model, X, y, cv=splitter, scoring="roc_auc")
print(f"ROC-AUC without session encoding: {scores.mean():.3f}")
ROC-AUC without session encoding: 0.551
The model is not performing much better than the DummyClassifier, which suggests that raw event-level features are not sufficient for good conversion prediction. This baseline is limited because it cannot directly use session-level behavior (for example, whether “add_to_cart” happened in the same session).
A better approach: session encoding and aggregation#
Next, we use the SessionEncoder to create session-level features that we can
aggregate over. We define a session boundary as “a user has been inactive for
more than 30 minutes”. The SessionEncoder will create a new column
timestamp_session_id that assigns a unique session ID to each session detected.
The parameter session_gap=30 * 60 specifies the inactivity threshold in
seconds (30 minutes).
Note that session-based features involve aggregations, which must be performed
only on the training data within each fold to avoid leakage. In a scikit-learn
pipeline, we can achieve this by using SessionEncoder followed by a custom
transformer that computes session aggregates, and ensures that the pipeline is
properly fitted within each fold of cross-validation.
from skrub import SessionEncoder, tabular_pipeline
se = SessionEncoder("timestamp", split_by="user_id", session_gap=30 * 60)
# Here we fit the SessionEncoder on the entire dataset for demonstration purposes
X_sessions = se.fit_transform(X)
X_sessions.head()
| user_id | timestamp | device_type | page_category | event_type | time_on_page | price_viewed | timestamp_session_id | |
|---|---|---|---|---|---|---|---|---|
| 0 | user_0019 | 2024-01-01 01:00:27.612852+00:00 | mobile | home | page_view | 19.8 | 90.1 | 1,161 |
| 1 | user_0019 | 2024-01-01 01:06:43.178374+00:00 | mobile | books | add_to_cart | 38.5 | 13.4 | 1,161 |
| 2 | user_0019 | 2024-01-01 01:07:54.816525+00:00 | mobile | electronics | search | 146. | 669. | 1,161 |
| 3 | user_0019 | 2024-01-01 01:08:50.901138+00:00 | desktop | books | page_view | 308. | 59.3 | 1,161 |
| 4 | user_0015 | 2024-01-01 01:48:01.240749+00:00 | tablet | fashion | page_view | 78.6 | 52.1 | 955 |
user_id
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 2 (40.0%)
Most frequent values
user_0019
user_0015
['user_0019', 'user_0015']
timestamp
DatetimeTZDtype- Null values
- 0 (0.0%)
- Unique values
- 5 (100.0%)
- Min | Max
- 2024-01-01T01:00:27.612852+00:00 | 2024-01-01T01:48:01.240749+00:00
device_type
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 3 (60.0%)
Most frequent values
mobile
desktop
tablet
['mobile', 'desktop', 'tablet']
page_category
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 4 (80.0%)
Most frequent values
books
home
electronics
fashion
['books', 'home', 'electronics', 'fashion']
event_type
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 3 (60.0%)
Most frequent values
page_view
add_to_cart
search
['page_view', 'add_to_cart', 'search']
time_on_page
Float64DType- Null values
- 0 (0.0%)
- Unique values
- 5 (100.0%)
- Mean ± Std
- 118. ± 117.
- Median ± IQR
- 78.6 ± 107.
- Min | Max
- 19.8 | 308.
price_viewed
Float64DType- Null values
- 0 (0.0%)
- Unique values
- 5 (100.0%)
- Mean ± Std
- 177. ± 276.
- Median ± IQR
- 59.3 ± 38.0
- Min | Max
- 13.4 | 669.
timestamp_session_id
Int64DType- Null values
- 0 (0.0%)
- Unique values
- 2 (40.0%)
- Mean ± Std
- 1.12e+03 ± 92.1
- Median ± IQR
- 1,161 ± 0
- Min | Max
- 955 | 1,161
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 | user_id | StringDtype | True | 0 (0.0%) | 2 (40.0%) | |||||
| 1 | timestamp | DatetimeTZDtype | True | 0 (0.0%) | 5 (100.0%) | 2024-01-01T01:00:27.612852+00:00 | 2024-01-01T01:48:01.240749+00:00 | |||
| 2 | device_type | StringDtype | False | 0 (0.0%) | 3 (60.0%) | |||||
| 3 | page_category | StringDtype | False | 0 (0.0%) | 4 (80.0%) | |||||
| 4 | event_type | StringDtype | False | 0 (0.0%) | 3 (60.0%) | |||||
| 5 | time_on_page | Float64DType | False | 0 (0.0%) | 5 (100.0%) | 118. | 117. | 19.8 | 78.6 | 308. |
| 6 | price_viewed | Float64DType | False | 0 (0.0%) | 5 (100.0%) | 177. | 276. | 13.4 | 59.3 | 669. |
| 7 | timestamp_session_id | Int64DType | True | 0 (0.0%) | 2 (40.0%) | 1.12e+03 | 92.1 | 955 | 1,161 | 1,161 |
No columns match the selected filter: . You can change the column filter in the dropdown menu above.
user_id
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 2 (40.0%)
Most frequent values
user_0019
user_0015
['user_0019', 'user_0015']
timestamp
DatetimeTZDtype- Null values
- 0 (0.0%)
- Unique values
- 5 (100.0%)
- Min | Max
- 2024-01-01T01:00:27.612852+00:00 | 2024-01-01T01:48:01.240749+00:00
device_type
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 3 (60.0%)
Most frequent values
mobile
desktop
tablet
['mobile', 'desktop', 'tablet']
page_category
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 4 (80.0%)
Most frequent values
books
home
electronics
fashion
['books', 'home', 'electronics', 'fashion']
event_type
StringDtype- Null values
- 0 (0.0%)
- Unique values
- 3 (60.0%)
Most frequent values
page_view
add_to_cart
search
['page_view', 'add_to_cart', 'search']
time_on_page
Float64DType- Null values
- 0 (0.0%)
- Unique values
- 5 (100.0%)
- Mean ± Std
- 118. ± 117.
- Median ± IQR
- 78.6 ± 107.
- Min | Max
- 19.8 | 308.
price_viewed
Float64DType- Null values
- 0 (0.0%)
- Unique values
- 5 (100.0%)
- Mean ± Std
- 177. ± 276.
- Median ± IQR
- 59.3 ± 38.0
- Min | Max
- 13.4 | 669.
timestamp_session_id
Int64DType- Null values
- 0 (0.0%)
- Unique values
- 2 (40.0%)
- Mean ± Std
- 1.12e+03 ± 92.1
- Median ± IQR
- 1,161 ± 0
- Min | Max
- 955 | 1,161
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 |
|---|---|---|---|
| price_viewed | timestamp_session_id | 1.00 | 0.252 |
| time_on_page | timestamp_session_id | 1.00 | 0.189 |
| time_on_page | price_viewed | 1.00 | 0.137 |
| event_type | price_viewed | 1.00 | |
| page_category | time_on_page | 1.00 | |
| event_type | time_on_page | 1.00 | |
| page_category | timestamp_session_id | 1.00 | |
| page_category | price_viewed | 1.00 | |
| device_type | price_viewed | 1.00 | |
| device_type | timestamp_session_id | 1.00 | |
| timestamp | timestamp_session_id | 1.00 | |
| device_type | time_on_page | 1.00 | |
| user_id | page_category | 1.00 | |
| user_id | device_type | 1.00 | |
| timestamp | time_on_page | 1.00 | |
| timestamp | price_viewed | 1.00 | |
| user_id | time_on_page | 1.00 | |
| user_id | price_viewed | 1.00 | |
| user_id | timestamp_session_id | 1.00 | |
| timestamp | device_type | 1.00 | |
| user_id | timestamp | 1.00 | |
| timestamp | page_category | 0.866 | |
| device_type | page_category | 0.816 | |
| page_category | event_type | 0.816 | |
| timestamp | event_type | 0.707 | |
| device_type | event_type | 0.471 | |
| event_type | timestamp_session_id | 0.408 | |
| user_id | event_type | 0.408 |
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").
Defining a custom transformer for session-level aggregation#
To avoid data leakage and maintain a clean pipeline, we can create a custom
transformer that inherits from BaseEstimator and TransformerMixin and
computes session-level aggregates within a scikit-learn pipeline.
This transformer will be fitted and applied separately within each fold of
cross-validation, ensuring that session features are computed only on the training
data of each fold.
from sklearn.base import BaseEstimator, TransformerMixin
class SessionAggregator(BaseEstimator, TransformerMixin):
def fit(self, X, y=None):
return self
def transform(self, X):
# Compute session-level aggregates
session_agg = X.groupby("timestamp_session_id").agg(
session_has_add_to_cart=("event_type", lambda x: "add_to_cart" in x.values),
session_n_events=("event_type", "count"),
session_mean_price=("price_viewed", "mean"),
session_dominant_device=("device_type", lambda x: x.mode()[0]),
)
# Join back to the original data
return X.merge(
session_agg,
how="left",
on="timestamp_session_id",
)
Then, we create a pipeline that includes the SessionEncoder, our custom
SessionAggregator, and the tabular_pipeline() for classification. This
pipeline will be used in cross-validation to evaluate the model
with session features.
from sklearn.pipeline import make_pipeline
model = make_pipeline(se, SessionAggregator(), tabular_pipeline("classification"))
scores = cross_val_score(model, X, y, cv=splitter, scoring="roc_auc")
print("ROC-AUC with session encoding:", scores.mean())
ROC-AUC with session encoding: 0.6872503615719967
As expected the model with session encoding performs much better than the baseline without session features, demonstrating the value of sessionization for conversion prediction.
The fact that we are working with aggregation means that it was necessary to create a custom transformer to compute session-level features. However, this situation can be avoided entirely by using the skrub DataOps workflow, which allows for more flexible data transformations without needing to fit everything within a scikit-learn pipeline.
Total running time of the script: (0 minutes 7.788 seconds)
Estimated memory usage: 610 MB