Skip to content

Predict Dominant Weekend Card Channel

Task type: MulticlassClassificationTask Industry: Banking

Weekend spending patterns reveal channel preferences that weekday transactions often obscure. By predicting which channel dominates a customer's weekend card activity, product teams can tailor weekend-specific promotions, optimize channel capacity, and personalize the user experience for the channel each customer gravitates toward on their days off.

What makes this advanced? Softmax probability output + weekday filtering — filters transactions to weekends only using pandas weekday extraction, returns softmax probability distribution instead of hard class.


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): card_trans with a channel_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 multiclass classification, the function must return one of:

  • A 1-D float32 array of size num_classes — probability distribution across the target channels.
  • 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

import pandas as pd


def softmax(x: np.ndarray) -> np.ndarray:
    e = np.exp(x - np.max(x))
    return e / e.sum()

# === Configuration ===
TARGET_WINDOW_DAYS = 60
CARD_TRANS_DATA_SOURCE = "card_trans"
TARGET_CHANNELS = ["online", "store", "mobile"]

def dominant_weekend_channel_target_fn(
    history: Events,
    future: Events,
    attributes: Attributes,
    ctx: Dict,
) -> np.ndarray | None:
    """Predict dominant weekend card transaction channel as probabilities."""

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

    # 1. Filter to weekend transactions only
    card_transactions = future[CARD_TRANS_DATA_SOURCE].filter(
        by="timestamps",
        condition=lambda v: pd.to_datetime(v, unit="s").weekday() >= 5,
    )

    # Skip customers with no weekend card activity instead of emitting
    # a degenerate uniform softmax over zero counts.
    if len(card_transactions) == 0:
        return None

    # 2. Count transactions per channel
    grouped, _ = card_transactions.groupBy(by="channel_id").count(
        groups=TARGET_CHANNELS
    )

    # 3. Return softmax probabilities
    return softmax(grouped).astype(np.float32)

Step-by-Step Breakdown

① Filter to weekend transactions only

Python
card_transactions = future[CARD_TRANS_DATA_SOURCE].filter(
    by="timestamps",
    condition=lambda v: pd.to_datetime(v, unit="s").weekday() >= 5,
)

Pandas weekday() returns 5 for Saturday and 6 for Sunday. By filtering on >= 5, only weekend card transactions are retained. This ensures the model learns weekend-specific channel preferences rather than overall patterns dominated by weekday activity.

② Count transactions per channel

Python
grouped, _ = card_transactions.groupBy(by="channel_id").count(
    groups=TARGET_CHANNELS
)

groupBy("channel_id").count() tallies transactions per channel. The groups parameter restricts output to the three target channels in the correct order, returning a fixed-size array even if some channels have zero transactions.

③ Return softmax probabilities

Python
return softmax(grouped).astype(np.float32)

Instead of a hard one-hot label, softmax converts raw counts into a smooth probability distribution. This gives the model a richer training signal — a customer with 10 online and 8 store transactions is labeled differently from one with 10 online and 1 store. The explicit .astype(np.float32) keeps the dtype aligned with the contract task heads expect — the inline softmax helper returns float64 by default.

④ Validate the training window

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

Ensures 60 days of future data are available. Samples near the end of the dataset with truncated windows are excluded to avoid biased 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, MulticlassClassificationTask

module = load_from_foundation_model(
    checkpoint_path=Path("./foundation_model"),
    downstream_task=MulticlassClassificationTask(class_names=TARGET_CHANNELS),
    target_fn=dominant_weekend_channel_target_fn,
)

training_params = TrainingParams(
    checkpoint_dir=Path("./<this_model>"),
    learning_rate=1e-4,
    epochs=20,
    devices=[0],
    metrics=[
        MetricParams(alias="accuracy", metric_name="Accuracy", kwargs={"task": "multiclass", "num_classes": <num_classes>}),
        MetricParams(alias="f1_macro", metric_name="F1Score", kwargs={"task": "multiclass", "num_classes": <num_classes>, "average": "macro"}),
    ],
    metric_to_monitor="val_accuracy_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="accuracy", metric_name="Accuracy"),
        MetricParams(alias="f1_macro", 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
Accuracy Overall proportion of correct predictions.
Macro F1 Balanced F1 across all classes.
Top-K Accuracy Whether the true class is in the top K predictions.
Confusion Matrix Reveals which classes are most often confused.

Production Tips

  1. Adjust the weekend definition for your market. In some regions the weekend falls on Friday-Saturday rather than Saturday-Sunday. Modify the weekday threshold accordingly.
  2. Monitor channel mix drift over time. Mobile payment adoption is growing rapidly. Retrain periodically to capture shifting channel preferences.
  3. Use softmax temperature to control label smoothness. If you want sharper labels, divide the counts by a temperature parameter before applying softmax. Lower temperature produces more peaked distributions.
  4. Combine with spend amount. Channel dominance by transaction count may differ from dominance by spend volume. Consider building a second model weighted by transaction amount.