Skip to content

Retraining on New Data

Keep a scenario current as new events arrive, new entities appear, and behaviour drifts — without retraining the foundation model. The foundation model is trained once and reused; the scenario is the cheap part you refresh against a newer date range.

To bring a scenario up to date you:

  • reuse your existing foundation model checkpoint,
  • point scenario training at a newer date range via the split argument, and
  • call fit() to write a refreshed scenario checkpoint.

Two-stage design

The foundation model learns general-purpose entity representations and stays fixed. A scenario fine-tunes it for one prediction task. To pick up new data you re-run scenario training against a newer date range while pointing at the same foundation model checkpoint.

How often should I retrain?

Typically every ~4 weeks, to account for new entities, seasonal patterns, and behavioural shifts. The right cadence depends on your data refresh rate and domain dynamics — see the FAQ.

Which Approach to Use

There are two ways to bring a scenario up to date. Both reuse the existing foundation model.

Approach Function When to use
Refresh from the foundation model load_from_foundation_model() Recommended for a genuinely new time window. Re-initialises the scenario head and trains from scratch on recent data.
Continue an existing scenario load_from_checkpoint() with scoring=False Resume from an already-trained scenario's weights — e.g. train a few more epochs, or nudge the window forward while keeping learned weights.

Prerequisites

  • A trained foundation model checkpoint (its fm/ subdirectory).
  • Your data source already contains the new rows for the date range you ask for. BaseModel reads events from the live data source at the date ranges you pass — it does not store a frozen copy. If the new events are not in the table yet, they will not be used.

Passing New Dates

Dates are controlled through the split, which you override at load time with the split argument — for either function. The split is a mapping from data mode (TRAIN / VALIDATION / TEST / PREDICT) to a TimeRange:

Python
from datetime import datetime, timezone
from monad.ui.config import DataMode, TimeRange

new_split = {
    DataMode.TRAIN:      TimeRange(start_date=datetime(2026, 1, 1, tzinfo=timezone.utc)),
    DataMode.VALIDATION: TimeRange(
        start_date=datetime(2026, 5, 1, tzinfo=timezone.utc),
        end_date=datetime(2026, 5, 31, tzinfo=timezone.utc),
    ),
    DataMode.TEST:       TimeRange(
        start_date=datetime(2026, 6, 1, tzinfo=timezone.utc),
        end_date=datetime(2026, 6, 30, tzinfo=timezone.utc),
    ),
}

TimeRange takes a required start_date and an optional end_date (open-ended when omitted). Timezone-aware UTC datetimes are recommended to avoid ambiguity. This dict is the concrete form of the TimeSplitOverride type in the Scenario Model API.

Move the data stream start with the window

The split says which events become inputs, validation, and targets. data_start_date says how far back the event stream begins. When you move the window forward you will usually want to move data_start_date with it — pass it as a keyword argument:

Python
trainer = load_from_foundation_model(
    checkpoint_path=foundation_model_path,
    downstream_task=task,
    target_fn=target_fn,
    split=new_split,
    # must not be later than TRAIN.start_date, or loading raises a ValueError
    data_start_date=datetime(2025, 12, 1, tzinfo=timezone.utc),
)

Which keyword overrides are honoured

Extra keyword arguments are applied only if they match a field of DataParams (for example data_start_date, minimum_splitpoint_date, maximum_splitpoints_per_entity). Any keyword that does not match a DataParams field is silently ignored — double-check spelling. See Loading Overrides for the full list.

Example: Refresh on a Newer Window

The recommended path for periodic retraining. It reuses the foundation model and trains a fresh scenario head on recent data. Adapt the paths, dates, task type, and target function to your scenario.

Python
from datetime import datetime, timezone
from pathlib import Path

from monad.ui.module import load_from_foundation_model, BinaryClassificationTask
from monad.ui.config import DataMode, TimeRange, TrainingParams

foundation_model_path = Path("./output/fm")          # unchanged, reused
scenario_model_path   = Path("./churn_2026-07")      # new output dir per refresh

def target_fn(history, future, entity, ctx):
    ...                                              # same labelling logic as before
    return label

new_split = {
    DataMode.TRAIN:      TimeRange(start_date=datetime(2026, 1, 1, tzinfo=timezone.utc)),
    DataMode.VALIDATION: TimeRange(
        start_date=datetime(2026, 5, 1, tzinfo=timezone.utc),
        end_date=datetime(2026, 5, 31, tzinfo=timezone.utc),
    ),
    DataMode.TEST:       TimeRange(
        start_date=datetime(2026, 6, 1, tzinfo=timezone.utc),
        end_date=datetime(2026, 6, 30, tzinfo=timezone.utc),
    ),
}

trainer = load_from_foundation_model(
    checkpoint_path=foundation_model_path,
    downstream_task=BinaryClassificationTask(),
    target_fn=target_fn,
    split=new_split,
    data_start_date=datetime(2025, 12, 1, tzinfo=timezone.utc),
)

trainer.fit(
    training_params=TrainingParams(checkpoint_dir=scenario_model_path, epochs=5),
    seed=42,
)

Keep old checkpoints

Write each refresh to its own checkpoint_dir (e.g. dated) rather than overwriting the previous scenario. This lets you compare metrics across refreshes and roll back if a new window regresses.

Example: Continue an Existing Scenario

Use this when you want to keep the weights an existing scenario already learned and carry on from them. Point load_from_checkpoint() at the scenario checkpoint (not the foundation model) and keep scoring=False (the default) so it loads for training rather than inference.

Python
from datetime import datetime, timezone
from pathlib import Path

from monad.ui.module import load_from_checkpoint
from monad.ui.config import DataMode, TimeRange, TrainingParams

trainer = load_from_checkpoint(
    checkpoint_path=Path("./churn_2026-06"),   # a trained scenario checkpoint
    scoring=False,                             # load for (resumed) training
    split={
        DataMode.TRAIN:      TimeRange(start_date=datetime(2026, 2, 1, tzinfo=timezone.utc)),
        DataMode.VALIDATION: TimeRange(
            start_date=datetime(2026, 6, 1, tzinfo=timezone.utc),
            end_date=datetime(2026, 6, 30, tzinfo=timezone.utc),
        ),
    },
)

trainer.fit(
    training_params=TrainingParams(checkpoint_dir=Path("./churn_2026-07"), epochs=2),
    seed=42,
)

The target function is loaded for you

load_from_checkpoint() restores the target function saved with the checkpoint (target_fn.bin), so you do not re-pass target_fn. This differs from load_from_foundation_model(), where target_fn is required.

Expect date-coherence warnings

When the new ranges differ from the ones stored in the checkpoint, or if any period overlaps another, BaseModel emits warnings (it does not stop). This is expected when you deliberately shift the window — read them to confirm the dates are what you intended.

Do I Also Need to Retrain the Foundation Model?

Usually no. The foundation model generalises to unseen behaviour, which is the whole point of the two-stage design. Retrain the foundation model as well when the change is structural rather than incremental — for example a large influx of brand-new entities, a new data source or event type, or a pronounced distribution shift that a scenario refresh alone does not recover from. See Foundation Model training.

Resource Description
Loading Overrides Every override you can apply at load time, including entity-based splits (EntitySplitOverride) and prediction filtering
Reference: Scenario Model Full signatures for load_from_foundation_model() and load_from_checkpoint()
Inference Score new data with an existing model without retraining, using prediction_date
Testing Against Ground Truth Measure a refreshed scenario against held-out data