Skip to content

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): transactions with a product_id column

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 Sketch object — a weighted set of target items.
  • Noneexclude this entity (e.g., no eligible repurchase candidates).

Full Example

Python
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

Python
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

Python
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

Python
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

Python
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:

Python
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

Python
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

Python
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)

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

  1. 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.
  2. 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.
  3. Filter by product availability. Post-process recommendations to remove out-of-stock or discontinued products before serving them to customers.
  4. 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."
  5. 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.