Skip to content

Predict Evening Brand Purchases

Task type: MultilabelClassificationTask Industry: Retail

Evening shopping behavior differs markedly from daytime patterns — customers tend to browse more, make impulse purchases, and favor certain brands. By predicting which brands a customer will repeatedly buy during evening hours, marketing teams can schedule push notifications, flash sales, and personalized emails to coincide with peak evening engagement windows.

What makes this advanced? Time-of-day filtering — extracts hours from unix timestamps using modular arithmetic, applies evening mask to isolate purchases between 18:00 and 23:00.


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 with a brand 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 multilabel classification, the function must return one of:

  • A 1-D float32 array of size num_labels — binary indicators (0 or 1) per brand.
  • Noneexclude this entity from training.

Full Example

Python
import numpy as np
from datetime import timedelta
from typing import Dict

from monad.ui.target_function import Events, Attributes
from monad.ui.target_function import SPLIT_TIMESTAMP
from monad.ui.target_function import has_incomplete_training_window


# === Configuration ===
TARGET_WINDOW_DAYS = 90
PURCHASE_DATA_SOURCE = "purchases"
MIN_EVENING_PURCHASES = 2
EVENING_START_HOUR = 18
EVENING_END_HOUR = 23
TARGET_BRANDS = ["BrandA", "BrandB", "BrandC"]

def evening_brand_purchases_target_fn(
    history: Events,
    future: Events,
    attributes: Attributes,
    ctx: Dict,
) -> np.ndarray | None:
    """Predict which brands the customer buys 2+ times in the evening."""

    if has_incomplete_training_window(ctx, timedelta(days=TARGET_WINDOW_DAYS)):
        return None

    split_ts = ctx[SPLIT_TIMESTAMP]
    future = future.interval_from(split_ts, timedelta(days=TARGET_WINDOW_DAYS))
    purchases = future[PURCHASE_DATA_SOURCE]

    if purchases.count() == 0:
        return None

    # 1. Extract hour from timestamps and apply evening mask
    hours = ((purchases.timestamps % 86400) // 3600).astype(int)
    evening_mask = (hours >= EVENING_START_HOUR) & (hours < EVENING_END_HOUR)

    brands = purchases["brand"]
    evening_brands = brands[evening_mask]

    if len(evening_brands) == 0:
        return None

    # 2. Count purchases per brand
    unique, counts = np.unique(evening_brands, return_counts=True)
    frequent_brands = unique[counts >= MIN_EVENING_PURCHASES]

    if len(frequent_brands) == 0:
        return None

    # 3. Build target vector
    target = np.isin(np.array(TARGET_BRANDS), frequent_brands).astype(np.float32)
    return target

Step-by-Step Breakdown

① Extract hour from unix timestamps

Python
hours = ((purchases.timestamps % 86400) // 3600).astype(int)
evening_mask = (hours >= EVENING_START_HOUR) & (hours < EVENING_END_HOUR)

Unix timestamps are converted to hour-of-day using modular arithmetic: % 86400 extracts seconds within the day, // 3600 converts to hours. This avoids the overhead of full datetime parsing and works efficiently on numpy arrays. The boolean mask isolates transactions between 18:00 and 23:00.

② Filter to evening purchases and count per brand

Python
brands = purchases["brand"]
evening_brands = brands[evening_mask]

unique, counts = np.unique(evening_brands, return_counts=True)
frequent_brands = unique[counts >= MIN_EVENING_PURCHASES]

The evening mask is applied to the brand column. np.unique with return_counts=True efficiently tallies purchases per brand. Only brands with 2 or more evening purchases qualify — a single purchase may be coincidental rather than a genuine preference.

③ Build the binary target vector

Python
target = np.isin(np.array(TARGET_BRANDS), frequent_brands).astype(np.float32)
return target

np.isin checks which target brands appear in the frequent set, producing a binary vector aligned with TARGET_BRANDS. This is the format MultilabelClassificationTask expects.

④ Handle edge cases

Python
if purchases.count() == 0:
    return None
if len(evening_brands) == 0:
    return None
if len(frequent_brands) == 0:
    return None

Three levels of filtering (no purchases, no evening purchases, no frequent evening brands) progressively exclude entities that cannot provide meaningful labels.


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, MultilabelClassificationTask

module = load_from_foundation_model(
    checkpoint_path=Path("./foundation_model"),
    downstream_task=MultilabelClassificationTask(class_names=TARGET_BRANDS),
    target_fn=evening_brand_purchases_target_fn,
)

training_params = TrainingParams(
    checkpoint_dir=Path("./<this_model>"),
    learning_rate=1e-4,
    epochs=20,
    devices=[0],
    metrics=[
        MetricParams(alias="auroc", metric_name="AUROC", kwargs={"task": "multilabel", "num_labels": <num_labels>}),
        MetricParams(alias="auprc", metric_name="AveragePrecision", kwargs={"task": "multilabel", "num_labels": <num_labels>}),
        MetricParams(alias="f1", metric_name="F1Score", kwargs={"task": "multilabel", "num_labels": <num_labels>}),
    ],
    metric_to_monitor="val_auroc_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],
    metrics=[
        MetricParams(alias="auroc", metric_name="AUROC"),
        MetricParams(alias="auprc", metric_name="AveragePrecision"),
        MetricParams(alias="f1", metric_name="F1Score"),
    ],
)

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
Hamming Loss Fraction of labels that are incorrectly predicted.
Subset Accuracy Proportion of samples with all labels correct.
Macro AUROC Average ranking quality across all labels.
Per-label F1 Identifies which labels the model struggles with.

Production Tips

  1. Adjust evening hours for your time zone. The 18:00-23:00 window assumes UTC-aligned timestamps. If your customer base spans multiple time zones, normalise timestamps to local time before filtering.
  2. Tune the minimum purchase threshold. Requiring 2+ purchases filters noise but may be too strict for low-frequency brands. Consider lowering to 1 for premium or niche brands.
  3. Expand the brand list dynamically. Rather than hardcoding TARGET_BRANDS, consider deriving the list from the top N brands by evening transaction volume in your training data.
  4. Combine with time-of-day features. Evening purchase propensity can vary by day of week. Consider adding a weekday-vs-weekend split for more granular targeting.