Recommend Products for Repurchase
Task type: RecommendationTask
Industry: Retail
Repurchase recommendations drive repeat revenue by surfacing products a customer has bought before and is likely to buy again — consumables, favorites, and regular staples. By excluding products bought very recently (last 14 days), this recipe avoids redundant suggestions and focuses on items whose replenishment cycle is approaching.
What makes this advanced? Set-based exclusion with forward and backward intervals — uses backward interval to identify recent purchases, forward interval for the target window, and set operations to exclude recent items.
Prerequisites
Before writing a target function you need:
- A trained foundation model built on event data that includes the relevant data sources.
- The monad library installed in your environment.
- Data source(s):
transactionswith aproduct_idcolumn
Target Function
The target function tells monad how to label each entity for training. It receives four arguments:
| Argument | Type | Description |
|---|---|---|
history |
Events |
All events before the temporal split. |
future |
Events |
All events after the temporal split. |
attributes |
Attributes |
Static entity attributes. |
ctx |
Dict |
Context dictionary containing SPLIT_TIMESTAMP, data mode, etc. |
For recommendation tasks, the function must return one of:
- A
Sketchobject — a weighted set of target items. None— exclude this entity (e.g., no eligible repurchase candidates).
Full Example
import numpy as np
from datetime import timedelta
from typing import Dict
from monad.ui.target_function import Events, Attributes, Sketch, sketch, sequential_decay
from monad.ui.target_function import SPLIT_TIMESTAMP
from monad.ui.target_function import has_incomplete_training_window
# === Configuration ===
TARGET_WINDOW_DAYS = 10
EXCLUSION_DAYS = 14
TRANSACTION_DATA_SOURCE = "transactions"
def repurchase_recommendation_target_fn(
history: Events,
future: Events,
attributes: Attributes,
ctx: Dict,
) -> Sketch | None:
"""Recommend products likely to be repurchased, excluding recent buys."""
split_ts = ctx[SPLIT_TIMESTAMP]
if has_incomplete_training_window(ctx, timedelta(days=TARGET_WINDOW_DAYS)):
return None
# 1. Get future products in the target window
future_window = future[TRANSACTION_DATA_SOURCE].interval_from(
split_ts, timedelta(days=TARGET_WINDOW_DAYS)
)
future_products = future_window["product_id"]
# 2. Get all historical transactions
all_history = history[TRANSACTION_DATA_SOURCE]
if all_history.count() == 0:
return None
# 3. Exclude products from the last 14 days
recent = all_history.interval_from(
split_ts, -timedelta(days=EXCLUSION_DAYS)
)
cutoff_ts = split_ts - EXCLUSION_DAYS * 86400
older = all_history.interval_from(0, timedelta(seconds=cutoff_ts))
if older.count() == 0:
return None
recent_set = set(recent["product_id"].events)
eligible = older.filter("product_id", lambda p: p not in recent_set)
# 4. Keep only repurchase candidates (appear in future)
future_set = set(future_products.events)
eligible = eligible.filter("product_id", lambda p: p in future_set)
if eligible.count() == 0:
return None
return sketch(eligible["product_id"], sequential_decay(eligible, gamma=0))
Step-by-Step Breakdown
① Get future products in the target window
future_window = future[TRANSACTION_DATA_SOURCE].interval_from(
split_ts, timedelta(days=TARGET_WINDOW_DAYS)
)
future_products = future_window["product_id"]
The 10-day forward window captures products the customer will actually buy. These form the ground truth for the recommendation target — only products that are actually repurchased should be recommended.
② Identify recent purchases for exclusion
recent = all_history.interval_from(
split_ts, -timedelta(days=EXCLUSION_DAYS)
)
cutoff_ts = split_ts - EXCLUSION_DAYS * 86400
older = all_history.interval_from(0, timedelta(seconds=cutoff_ts))
The backward interval (negative timedelta) captures the last 14 days of history. Products bought in this window are excluded because recommending something the customer just bought feels redundant. The older slice captures all history before the exclusion window.
③ Apply set-based exclusion
recent_set = set(recent["product_id"].events)
eligible = older.filter("product_id", lambda p: p not in recent_set)
Products from the exclusion window are collected into a set, and older transactions are filtered to remove any products in that set. This leaves only products the customer bought in the past but not recently — the prime repurchase candidates.
④ Intersect with future purchases and build sketch
future_set = set(future_products.events)
eligible = eligible.filter("product_id", lambda p: p in future_set)
return sketch(eligible["product_id"], sequential_decay(eligible, gamma=0))
A final filter keeps only products that the customer will actually buy in the target window. The sketch function wraps the product IDs with uniform weights (all ones) to create the recommendation target.
Training
Once the target function is defined, fine-tune a downstream model:
from pathlib import Path
from monad.ui.config import TrainingParams, MetricParams, MetricMonitoringMode
from monad.config.early_stopping import EarlyStopping
from monad.ui.module import load_from_foundation_model, RecommendationTask
module = load_from_foundation_model(
checkpoint_path=Path("./foundation_model"),
downstream_task=RecommendationTask(),
target_fn=repurchase_recommendation_target_fn,
)
training_params = TrainingParams(
checkpoint_dir=Path("./<this_model>"),
learning_rate=1e-4,
epochs=20,
devices=[0],
metrics=[
MetricParams(alias="ndcg", metric_name="NDCGAtK", kwargs={"k": 10}),
MetricParams(alias="hitrate", metric_name="HitRateAtK", kwargs={"k": 10}),
MetricParams(alias="recall", metric_name="MultipleTargetsRecall", kwargs={"k": 10}),
],
metric_to_monitor="val_ndcg_0",
metric_monitoring_mode=MetricMonitoringMode.MAX,
early_stopping=EarlyStopping(min_delta=1e-4, patience=5),
)
module.fit(training_params, seed=42)
Evaluation
from pathlib import Path
from datetime import datetime, timezone
from monad.ui.module import load_from_checkpoint
from monad.ui.config import TestingParams, MetricParams, OutputType
module = load_from_checkpoint(Path("./<this_model>"))
testing_params = TestingParams(
prediction_date=datetime(2024, 5, 1, tzinfo=timezone.utc),
output_type=OutputType.DECODED,
devices=[0],
top_k=10,
metrics=[
MetricParams(alias="ndcg", metric_name="NDCG"),
MetricParams(alias="hitrate", metric_name="HitRate"),
MetricParams(alias="recall", metric_name="Recall"),
],
)
results = module.test(testing_params)
Prediction
from pathlib import Path
from datetime import datetime, timezone
from monad.ui.module import load_from_checkpoint
from monad.ui.config import TestingParams, OutputType
module = load_from_checkpoint(Path("./<this_model>"))
testing_params = TestingParams(
local_save_location=Path("./predictions.tsv"),
output_type=OutputType.DECODED,
prediction_date=datetime(2024, 6, 1, tzinfo=timezone.utc),
devices=[0],
)
predictions = module.predict(testing_params)
Recommended Metrics
| Metric | Why it matters |
|---|---|
| Hit Rate | Proportion of users with at least one relevant recommendation. |
| NDCG | Measures ranking quality — rewards relevant items ranked higher. |
| MAP | Mean average precision across all users. |
| Coverage | Fraction of the item catalogue that appears in recommendations. |
Production Tips
- Tune the exclusion window to match replenishment cycles. 14 days works for fast-moving consumer goods. For durable goods or subscription items, extend the exclusion window to 30-60 days.
- Add purchase frequency weighting. Instead of uniform weights, weight the sketch by historical purchase frequency — products bought many times are stronger repurchase candidates than one-time purchases.
- Filter by product availability. Post-process recommendations to remove out-of-stock or discontinued products before serving them to customers.
- Consider category-level repurchase. For some use cases, recommending the same category (not exact product) is more useful — e.g., "you bought shampoo 3 weeks ago, here are shampoo options."
- Monitor the exclusion-to-target ratio. If the exclusion window is too long relative to the target window, most candidates will be filtered out. Ensure a reasonable number of eligible products remain after exclusion.