Validation & Best Practices
This page covers how to validate your target function before training, common mistakes to avoid, and performance tips.
Validating with verify_target()
Before launching a full training run, use verify_target() to check that your target function executes without errors and returns the values you expect. It runs the function against a sample of entities from your foundation model data and reports problems early:
- Executes
target_fnon a sampled subset of entities - Checks return type and shape match the task
- Tracks
Nonerate and fails if it exceeds the allowed threshold - Surfaces exceptions with a stack trace and sample entity context
Basic Usage
from monad.ui.module import BinaryClassificationTask
from monad.ui.target_function import verify_target
verify_target(
target_fn=my_target_fn,
fm_checkpoint_path="./foundation_model",
task=BinaryClassificationTask(),
)
Extra columns and verify_target()
verify_target() reuses the checkpoint's stored DataParams, including extra_columns. You do not need to construct DataParams or ExtraColumn just to reuse that configuration. Pass data_params_overrides only when validation must use different data parameters or a legacy checkpoint does not contain them. See verify_target() for the full parameter reference.
If validation passes, verify_target() returns a NumPy array containing all non-None targets produced by the checked sample. Its first dimension is the number of accepted entities, not one example. Inspect result.shape or aggregate counts before printing it when labels are sensitive. If something is wrong, it raises one of:
| Error | Meaning |
|---|---|
TypeError | Return type is incorrect or inconsistent across entities |
ValueError | Too many entities returned None (exceeds percentage_nones_allowed) |
RuntimeError | The target function itself failed during execution |
For the full parameter reference, see verify_target().
Full Example
from typing import Dict
from datetime import timedelta
import numpy as np
from monad.batch import SPLIT_TIMESTAMP
from monad.ui.module import RegressionTask
from monad.ui.target_function import (
Attributes, Events, verify_target,
has_incomplete_training_window,
)
def ltv_target(
history: Events, future: Events, entity: Attributes, ctx: Dict
) -> np.ndarray:
if history["transactions"].count() < 2:
return None
if has_incomplete_training_window(ctx, timedelta(days=30)):
return None
future_30d = future.interval_from(ctx[SPLIT_TIMESTAMP], timedelta(days=30))
return np.array([future_30d["transactions"].sum("price")], dtype=np.float32)
result = verify_target(
target_fn=ltv_target,
fm_checkpoint_path="./foundation_model",
task=RegressionTask(num_targets=1),
num_percentage_entities=5, # check 5% of entities
percentage_nones_allowed=70, # allow up to 70% None
log_every_n_steps=100, # progress logging
)
print("Accepted target array shape:", result.shape)
Debugging Failures
RuntimeError
Wrap risky sections in try/except, print counts, guard empty data sources withif txns.count() == 0TypeError
Printtype(result),result.shape, andresult.dtype— verify they match the taskValueError
Your function returns too manyNonevalues. Log your eligibility conditions, temporarily relax filters, uselog_every_n_stepsto see which entities are skipped
Validation Tips
-
Start small.
Leavenum_percentage_entitiesat1(the default) for initial debugging. Increase it once the function runs cleanly to get a better picture of yourNonerate. -
Watch your None rate.
A highNonerate isn't necessarily wrong — cold-start filtering and incomplete windows are legitimate reasons to exclude entities. But if the rate is unexpectedly high, it often signals a bug in your filtering logic. Adjustpercentage_nones_allowedto match your expectation. -
Reuse checkpoint data parameters when possible.
Validation loads the storedDataParams, includingextra_columns, from the foundation model checkpoint. Supplydata_params_overridesonly to change those settings or support a legacy checkpoint that lacks them. -
Enable progress logging.
Setlog_every_n_steps(e.g.,100) to print a status line every N entities — useful for spotting where failures cluster or confirming the run is progressing. -
Use
limitfor large datasets.
If your foundation model covers millions of entities, setlimitto cap the number of evaluations and keep iteration fast.
Common Mistakes
-
Forgetting to check for empty data
On an emptyDataSourceEvents,sum()returns0.0andmean()returnsNaN;min()andmax()raiseValueError. Guardmean(),min(),max()and any division withif txns.count() == 0. For churn, count or spend targets an emptyfutureis a valid label (churn = 1, spend = 0), so returningNonethere drops exactly those entities from training. ReturnNoneonly when there is no label without future events (for example, which brand or item comes next). -
Missing
.eventsor.attribute
txns["price"]returns aModalityEventsobject, not an array. Usetxns["price"].eventsto get thenp.ndarray. Similarly,attributes["customers"]["age"]returns aModalityAttribute; use.attributeto get the value. -
Leaking future information into eligibility checks
Usehistory(orattributes) to decide whether an entity qualifies. Usefutureonly to compute the label. If you filter entities based on future data, the model receives a signal it cannot reproduce at inference time.Wrong — filtering on futurefuture_30d = future.interval_from(ctx[SPLIT_TIMESTAMP], timedelta(days=30)) if future_30d["transactions"].count() == 0: return None # removes all churners from training!Correct — filtering on history, labeling from future# eligibility: entity must have history if history["transactions"].count() == 0: return None # ensure a complete target window if has_incomplete_training_window(ctx, timedelta(days=30)): return None # label: use future only to compute the target future_30d = future.interval_from(ctx[SPLIT_TIMESTAMP], timedelta(days=30)) return np.array([1 if future_30d["transactions"].count() == 0 else 0], dtype=np.float32) -
Skipping
has_incomplete_training_window
Without this check, split points near the end of your data produce windows shorter than intended, creating inconsistent labels. -
Forgetting to unpack
.groupBy
All'groupByoperations return(values, names). Always unpack:counts, names = txns.groupBy("category").count(). -
Wrong timedelta direction
Positive timedelta moves forward (into future), negative moves backward (into history). A common error is using a positive timedelta onhistory, which selects nothing.
Performance Tips
Rare target cohorts
For a rare eligible cohort that is fixed for the whole run and selected without future target-window outcomes, TrainingParams.entity_ids can restrict scenario training before target materialization. Monad 1.14 has no automatic, target-aware prefilter that can infer which target_fn calls will return None. Pass entity_ids=EntityIds(subquery=..., matching=True) to TrainingParams; see the EntityIds reference.
Do not apply a static ID list when eligibility changes at each sampled split point: that changes the training population and can leak later history. In that case, return None from target_fn after the cheapest eligibility checks.
Put the cheapest history-only eligibility checks first, before joined-column reads or future-window aggregations. Scenario training reuses data_loader_params.num_workers from the foundation checkpoint, so tune that setting for loading throughput. Use limit_train_batches and limit_val_batches to bound a trial run. These controls reduce per-entity work, improve throughput, or cap the run; they do not improve the eligible-entity ratio.
predictions_to_include_fn and predictions_to_exclude_fn restrict prediction classes or items for supported tasks. They do not filter training entities.
- Use built-in aggregations
txns.sum("price")is faster than manual loops over.events. - Exit early
ReturnNoneas soon as you know an entity is invalid. - Cache data source access
Assigntxns = future["transactions"]once and reuse. -
Avoid manual loops
Use the built-in aggregation and filtering methods rather than iterating over.eventsarrays yourself.
Putting It Together
Once your target function is validated, wire it into a training run:
# --- Imports ---
from pathlib import Path
from datetime import timedelta
import numpy as np
from monad.batch import SPLIT_TIMESTAMP
from monad.config import TrainingParams
from monad.ui.module import load_from_foundation_model, BinaryClassificationTask
from monad.ui.target_function import Events, Attributes, has_incomplete_training_window
# --- Task & target function ---
task = BinaryClassificationTask()
def churn_target(history: Events, future: Events, entity: Attributes, ctx: dict):
...
return np.array([1 if future["transactions"].count() == 0 else 0],
dtype=np.float32)
# --- Load from foundation model ---
module = load_from_foundation_model(
checkpoint_path="./foundation_model",
downstream_task=task,
target_fn=churn_target,
)
# --- Execute training ---
training_params = TrainingParams(
checkpoint_dir=Path("./churn_model"),
...
)
module.fit(training_params=training_params)
Once task and target are defined, move on to Model Configuration to set up and run training.