Note
Go to the end to download the full example code or to run this example in your browser via 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)
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}")
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}")
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()
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())
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.
Estimated memory usage: 0 MB