Purchase Propensity + RFM

Will a website visitor place an order? Two small models score one-day e-commerce sessions from 23 binary flags (added to basket, saw the checkout, signed in, device, returning visitor, ...): a class-balanced logistic regression (24 parameters, the deployed default) and a PyTorch MLP (3,649 parameters). The repo also ships an RFM segmentation (recency / frequency / monetary) of the 4,338 customers of the UCI Online Retail shop.

Model

Input 23 binary session flags (config.json["feature_names"]), as a dict {name: 0/1} or a list of the active names
Preprocessing StandardScaler fit on the train split (scaler.joblib), shared by both models
logistic_regression (default) scikit-learn LogisticRegression(class_weight="balanced", max_iter=2000), 23 coefficients + 1 intercept = 24 parameters (logreg.joblib)
pytorch_mlp model.PropensityMLP: Linear(23, 64) → ReLU → Dropout(0.2) → Linear(64, 32) → ReLU → Dropout(0.2) → Linear(32, 1), 3,649 parameters (model.safetensors, PyTorchModelHubMixin)
Output {"buy": p, "no_buy": 1 - p}, with p = P(the session ends in an order)
Decision threshold tuned for max F1 on validation: 0.949 (logistic regression), 0.965 (MLP), in config.json["models"]
RFM rfm_segment_stats.json: segment table, the R x F segment grid and the quintile edges used by Predictor.rfm_segment
Device the MLP runs on CUDA when available; the logistic regression runs on CPU

Both models were trained with class weights (balanced weights / pos_weight = 22.85), so their probabilities are not calibrated: they are pushed up for every session, and the tuned thresholds sit near 0.95. Use the threshold (or the ranking) rather than reading p as a literal purchase rate.

Usage

from huggingface_hub import snapshot_download
import sys
path = snapshot_download("shalev396/purchase-propensity")
sys.path.insert(0, path)
import model
predictor = model.load(path, device="cpu")   # or "cuda" (used by the MLP)

session = ["basket_add_detail", "checked_delivery_detail", "sign_in", "saw_checkout", "device_computer", "loc_uk"]
print(predictor.predict(session))                         # {'buy': 0.991, 'no_buy': 0.009}
print(predictor.predict({"saw_homepage": 1, "device_mobile": 1}, model="pytorch_mlp"))
print(predictor.threshold())                              # 0.949: decision threshold of the default model
print(predictor.explain(session))                         # P(buy) drop when each active flag is switched off
print(predictor.predict_batch("sessions.csv"))            # CSV / DataFrame with the 23 flag columns
print(predictor.rfm_segment(recency_days=12, frequency=6, monetary=2500))   # -> 'Champions' (R5 F4 M5)

Requirements: torch, scikit-learn, joblib, numpy, pandas, huggingface_hub, safetensors (requirements.txt).

  • Space / free API: shalev396/purchase-propensity, POST /gradio_api/call/predict with {"data": [["saw_checkout", "sign_in"], "best"]}.
  • Inference Endpoint: deploy this repo (Deploy → Inference Endpoints). handler.py accepts {"inputs": ["saw_checkout", "sign_in"], "parameters": {"model": "best"}} (or a dict of flags, or a list of dicts).

Training

  • Sessions: Kaggle benpowis/customer-propensity-to-purchase-data (training_sample.csv): 455,401 one-day sessions, 19,093 of which ordered (4.19 %). The 23 flags only take 9,086 distinct combinations. Stratified split: 309,672 train / 54,648 validation / 91,081 test (68 / 12 / 20, seed 42).
  • Logistic regression: lbfgs on the standardized train split, balanced class weights (1.6 s).
  • MLP: BCEWithLogitsLoss(pos_weight = 22.85), AdamW (lr 1e-3, weight decay 1e-4), batch 4,096, up to 30 epochs with early stopping on validation ROC-AUC (patience 5). It stopped after 18 epochs and kept epoch 13 (validation ROC-AUC 0.99725, 141 s).
  • Selection: each model's threshold maximizes F1 on validation; the default model is the one with the higher validation PR-AUC (0.8999 vs 0.8968); the test split is scored once, afterwards.
  • RFM: UCI Online Retail (id 352), cleaned to 397,884 invoice lines (no missing customer, no cancellations, positive quantity and price). Per customer: days since the last order (counted from 2011-12-10, the day after the last invoice on 2011-12-09), number of invoices and total spend; rank-based quintile scores 1-5; segment from the standard (R, F) grid.
  • This checkpoint: retrained on 2026-09-25 with training/ on a desktop CPU (147 s for both models). The logistic regression reproduces the original project's run exactly (same split and seed: same threshold, confusion matrix and metrics).

Full code: training/ · Colab.

Experiments

experiment params val PR-AUC threshold test ROC-AUC test PR-AUC test F1 test precision test recall
logistic_regression (deployed default) 24 0.8999 0.949 0.9974 0.8995 0.9245 0.8689 0.9877
pytorch_mlp 3,649 0.8968 0.965 0.9973 0.8997 0.9237 0.8703 0.9840

Both models are essentially tied on the test split: ROC-AUC 0.9974 vs 0.9973 and F1 0.925 vs 0.924. The 24-parameter linear model matches a 3,649-parameter MLP, which says the signal in these flags is almost additive. The logistic regression is the default because it won on validation PR-AUC; it is also cheaper, exact to reproduce and directly interpretable.

Both experiments on the test split MLP training curves

Evaluation

Deployed model (logistic_regression, threshold 0.949), test split of 91,081 sessions:

metric (test) value
roc_auc 0.9974
pr_auc 0.8995
f1 0.9245
precision 0.8689
recall 0.9877
accuracy 0.9932

ROC and precision-recall curves Confusion matrix

At the tuned threshold it finds 3,772 of the 3,819 buyers (recall 98.8 %), and 86.9 % of the sessions it flags do order (569 false alarms among 87,262 non-buyers). Two late-funnel flags carry most of the signal. On the train split, 52 % of sessions with saw_checkout order and not a single session without it does (every order saw the checkout); 66 % of sessions with checked_delivery_detail order versus 0.05 % without it.

Feature importance

RFM segments (4,338 customers, GBP 8.91M revenue): Champions are 14.6 % of customers and 48.7 % of revenue; Hibernating is the largest segment (24.8 % of customers, 5.9 % of revenue).

RFM segments

Limitations

  • Late-funnel leakage risk. saw_checkout and checked_delivery_detail are recorded during the same session as the order, so the near-perfect scores answer "is this session about to buy?". They are not an early prediction. A model for targeting visitors earlier would have to drop these flags and would likely score much lower.
  • Repeated patterns. 455k sessions share only 9,086 flag combinations, so most test sessions have an identical twin in the training data (different users, same flags). The metrics are honest for this population but say little about new kinds of sessions.
  • Uncalibrated probabilities (class weighting), see Model. Use the tuned thresholds.
  • One shop, one period. The session data is an anonymous sample from one online retailer (93 % of sessions from the UK); the RFM data is a UK online gift shop in 2010-2011. Neither transfers to another business without retraining.
  • The RFM explorer's quintile edges are approximate for tied values (scores are rank-based, and many customers ordered exactly once).
Downloads last month
-
Safetensors
Model size
3.65k params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Space using shalev396/purchase-propensity 1

Evaluation results