Recommend Products Never Bought at Full Price
Task type: RecommendationTask
Industry: Retail
Discount-sensitive customers represent a distinct behavioral segment. By identifying products a customer has never bought at full price, marketing teams can target them with strategic discounts on those specific products — turning occasional discount buyers into regular customers while protecting margins on products they already buy at full price.
What makes this advanced? groupBy min aggregation — uses
groupBy("product_id").min("discount_applied")to find products where the minimum discount > 0, meaning the customer never paid full price.
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):
purchases_eventswithproduct_idanddiscount_appliedcolumns
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 discount-only products found).
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 ===
TRANSACTION_DATA_SOURCE = "purchases_events"
def discount_products_recommendation_target_fn(
history: Events,
future: Events,
attributes: Attributes,
ctx: Dict,
) -> Sketch | None:
"""Recommend products only bought with discounts."""
# 1. Find minimum discount per product
min_discount, product_ids = (
history[TRANSACTION_DATA_SOURCE]
.groupBy("product_id")
.min("discount_applied")
)
# 2. Filter to products never bought at full price
target_ids = set(
pid for pid, md in zip(product_ids, min_discount) if md > 0
)
if not target_ids:
return None
# 3. Build recommendation from matching transactions
targets = history[TRANSACTION_DATA_SOURCE].filter(
"product_id", lambda pid: pid in target_ids
)
return sketch(targets["product_id"], sequential_decay(targets, gamma=0))
Step-by-Step Breakdown
① Find minimum discount per product
min_discount, product_ids = (
history[TRANSACTION_DATA_SOURCE]
.groupBy("product_id")
.min("discount_applied")
)
groupBy("product_id").min("discount_applied") groups all historical purchases by product and finds the minimum discount applied to each product. If the minimum discount for a product is 0, the customer bought it at full price at least once. If the minimum discount is > 0, every purchase of that product included a discount.
② Filter to discount-only products
A set comprehension filters product IDs to those where the minimum discount exceeds zero. These are products the customer has never paid full price for — prime candidates for targeted discount campaigns.
③ Build the recommendation sketch
targets = history[TRANSACTION_DATA_SOURCE].filter(
"product_id", lambda pid: pid in target_ids
)
return sketch(targets["product_id"], sequential_decay(targets, gamma=0))
Historical transactions matching the discount-only products are filtered, and a sketch is built with uniform weights. Products that were purchased more frequently at a discount will appear multiple times, implicitly receiving higher weight in the sketch.
④ Handle the empty case
Customers who have bought every product at full price at least once are excluded — there are no discount-only products to recommend.
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=discount_products_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
- Distinguish discount types. Not all discounts are equal — loyalty rewards, clearance sales, and promotional coupons have different implications. Consider filtering by discount type if your data supports it.
- Set a minimum purchase count. A single discounted purchase of a product is weak evidence. Require 2+ purchases to increase confidence that the customer genuinely prefers the product at a discount.
- Combine with margin data. Some products have healthy margins even with discounts. Prioritise recommendations for products where a discount still yields acceptable profit.
- A/B test discount levels. Use the model to identify discount-sensitive products per customer, then test different discount levels (5%, 10%, 15%) to find the minimum discount that drives conversion.