How-To: Write a Validation Script¶
This guide walks through writing a validation script for a Synapse challenge
using the cnb_tools.validation_toolkit module (imported here as vtk).
Validation scripts are designed to catch malformed or incomplete prediction files before they reach the scoring step, returning a machine-readable result that the evaluation workflow can act on.
Overview¶
A typical validation script does four things:
- Reads the prediction file submitted by a participant
- Reads the groundtruth file to determine what a valid prediction looks like
- Runs a series of checks and collects any error messages
- Writes a JSON result file containing a
submission_statusandsubmission_errors
The last step is optional, but is standard practice in the evaluation workflows developed at Sage.
The validation_toolkit module provides ready-made check functions for the
most common validation patterns, so you don't have to write them from scratch.
Available Check Functions¶
| Function | What it checks |
|---|---|
check_missing_keys(gold_col, pred_col) |
Every groundtruth ID has a matching prediction |
check_unknown_keys(gold_col, pred_col) |
No prediction IDs are absent from the groundtruth |
check_duplicate_keys(pred_col) |
No ID appears more than once in the prediction file |
check_nan_values(pred_col, include_inf) |
No NaN or None values; set include_inf=True to also flag inf/-inf |
check_binary_values(pred_col) |
All values are 0 or 1 (configurable via label1/label2); NaN values are ignored |
check_values_range(pred_col, min_val, max_val) |
All values fall within [min_val, max_val]; NaN values are ignored |
check_valid_values(pred_col, valid_values) |
All values belong to an allowed set (e.g. category labels); NaN values are ignored |
check_not_constant(pred_col) |
Column has more than one unique non-null value |
All functions return an empty string when the check passes, or a descriptive error message when it fails. This makes it easy to collect errors in a list and filter out the empty strings at the end.
Worked Example¶
Below is a minimal example of a validate() function for a challenge where
participants submit a CSV with one row per sample and a single numeric
prediction column with values in the range [0, 1].
import pandas as pd
from cnb_tools import validation_toolkit as vtk
# Groundtruth: only the ID column is needed for key checks.
ID_COL = "id"
GROUNDTRUTH_COLS = {ID_COL: str}
# Required columns in the prediction file and their expected dtypes.
PREDICTION_COLS = {ID_COL: str, "score": float}
def validate(gt_file: str, pred_file: str) -> list[str]:
errors = []
truth = pd.read_csv(gt_file, usecols=GROUNDTRUTH_COLS, dtype=GROUNDTRUTH_COLS)
try:
pred = pd.read_csv(
pred_file,
usecols=list(PREDICTION_COLS),
dtype=PREDICTION_COLS,
float_precision="round_trip",
)
assert set(PREDICTION_COLS).issubset(pred.columns)
except (ValueError, AssertionError):
return [f"Missing required columns. Expecting: {list(PREDICTION_COLS)}."]
errors.append(vtk.check_duplicate_keys(pred[ID_COL]))
errors.append(vtk.check_missing_keys(truth[ID_COL], pred[ID_COL]))
errors.append(vtk.check_unknown_keys(truth[ID_COL], pred[ID_COL]))
errors.append(vtk.check_nan_values(pred["score"]))
errors.append(vtk.check_values_range(pred["score"], min_val=0, max_val=1))
return [e for e in errors if e]
Useful options
- Pass
verbose=Truetocheck_missing_keys,check_unknown_keys, andcheck_duplicate_keysto include the actual offending IDs in the error message — useful during development. - Pass
include_inf=Truetocheck_nan_valuesif infinite values should also be treated as invalid.
Integrating with a Synapse workflow
The validate() function returns a plain list of error strings and has no
Synapse-specific dependencies — you can call it from any orchestration
layer. If you are building a Synapse-based evaluation pipeline, refer to
our starter templates:
Multi-task challenges¶
For challenges with more than one task, write a validate_task2() (etc.) and
route by evaluation queue ID:
def validate(task_number: int, gt_file: str, pred_file: str) -> list[str]:
validation_func = {
12345678: validate_task1,
12345679: validate_task2,
}.get(task_number)
if validation_func:
return validation_func(gt_file=gt_file, pred_file=pred_file)
return [f"Invalid task number: `{task_number}`"]
Adding Custom Checks¶
The built-in functions cover the most common cases, but you can write your own
in the same style — return "" on pass, or a descriptive message string on
failure:
def check_no_future_dates(col: pd.Series, cutoff: str) -> str:
"""Check that no dates are after the cutoff."""
invalid = col[pd.to_datetime(col) > pd.Timestamp(cutoff)]
if not invalid.empty:
return f"'{col.name}' contains {len(invalid)} date(s) after {cutoff}."
return ""
Then use it like any built-in check:
errors.append(check_no_future_dates(pred["date"], cutoff="2025-01-01"))