Guides

Task-oriented workflow documentation for configuration, validation, demos, lifecycle, and troubleshooting.

Pages

  • Configuration guidemeridian-tools is driven by one YAML configuration file. This guide explains every section, its purpose, and its constraints. For a field-level schema reference, see yaml-schema.md.
  • Validation guide — This guide explains how to choose and configure validation strategies in meridian-tools. Validation is the process of evaluating a candidate model specification on held-out data before committing to a final production fit.
  • Model selection guide — This guide explains how meridian-tools supports Bayesian model selection using Leave-One-Out (LOO) cross-validation and the Watanabe-Akaike Information Criterion (WAIC). It covers when model selection is available, how to interpret the outputs, and how to compare multiple candidate models.
  • Lifecycle management guidemeridian-tools treats completed runs as immutable artefacts. The lifecycle module provides tools to load, compare, and refresh past runs without mutating them. This guide explains each lifecycle operation and when to use it.
  • Meridian Tools workflow guide — This guide shows the supported end-to-end agency workflow for meridian-tools. It starts with one YAML config, moves through candidate validation, separates the final full-sample fit from the validation runs, and ends with the artefacts you should hand over or inspect later. The examples in this guide stay inside the implemented package surface. They do not assume notebooks, dashboards, or unpublished helper scripts.
  • Meridian Tools demo guide — This is the canonical guide to the bundled meridian-tools demos. Use it when you want one safe, reproducible, end-to-end example without client data.
  • Troubleshooting — Common issues and solutions when working with meridian-tools.

Subsections of Guides

Configuration guide

meridian-tools is driven by one YAML configuration file. This guide explains every section, its purpose, and its constraints. For a field-level schema reference, see yaml-schema.md.

Configuration philosophy

The YAML file owns the authored project definition: project metadata, data paths, model specification, fit settings, validation strategy, and export switches. Runtime-only values — output_dir, run_name, and concrete validation_spec — belong in PipelineRunConfig or CLI flags, not in the YAML file. This separation ensures that the same YAML file can drive multiple runs with different runtime options while remaining reproducible.

Minimal valid config

project:
  name: my-project

data:
  path: ./data.csv
  coord_to_columns:
    time: week

This is the smallest config that will pass validation. It uses defaults for everything else: no validation, all exports enabled, no response curves, no optimisation.

Section reference

project

Top-level project metadata.

project:
  name: client-mmm        # Default: "meridian-project"
  • name — Human-readable project name. Used as the base for run directory names unless overridden by --run-name at runtime.

data

CSV data loader configuration. Maps directly to Meridian’s CsvDataLoader.

data:
  path: ./client_dataset.csv
  kpi_type: revenue                    # "revenue" (default) or "non-revenue"
  coord_to_columns:
    time: week
    geo: market                        # optional for national models
    kpi: revenue
    population: population
    media: [impressions_tv, impressions_search]
    media_spend: [spend_tv, spend_search]
    controls: [promo_flag, price_index]
  media_to_channel: null               # optional channel mapping overrides
  media_spend_to_channel: null
  reach_to_channel: null
  frequency_to_channel: null
  rf_spend_to_channel: null
  organic_reach_to_channel: null
  organic_frequency_to_channel: null
  • path — Path to the CSV data file. Relative paths are resolved against the directory containing the YAML config file, not the current working directory.
  • kpi_type — Either "revenue" or "non-revenue". Controls how Meridian interprets the KPI column.
  • coord_to_columns — Maps Meridian coordinate names to CSV column names. time is required. geo is optional (omit for national models).

model_spec

Raw keyword arguments forwarded to Meridian’s ModelSpec.

model_spec:
  kwargs:
    max_lag: 8
    media_prior_type: roi
  • kwargs — Dictionary passed through to ModelSpec(**kwargs). Supports any argument that Meridian’s ModelSpec accepts.
  • Special handling for holdout_id: if present in kwargs, the run is treated as an “authored holdout” validation run. See the validation guide for details.

fit

Sampling configuration for Meridian posterior fitting.

fit:
  sample_prior_draws: null     # Optional prior-only sampling
  n_chains: 4                  # Number of MCMC chains
  n_adapt: 500                 # Adaptation steps per chain
  n_burnin: 500                # Burn-in steps per chain
  n_keep: 1000                 # Posterior samples to keep per chain
  seed: 20260331               # Reproducibility seed (int, list[int], or null)
  max_tree_depth: 10           # NUTS max tree depth
  max_energy_diff: 500.0       # NUTS max energy difference
  unrolled_leapfrog_steps: 1   # NUTS leapfrog steps
  parallel_iterations: 10      # TF parallel iterations

All fields have sensible defaults. Override only what you need.

  • seed — Accepts a single integer, a list of integers (one per chain), or null for non-deterministic sampling.
  • sample_prior_draws — If set, prior predictive samples are drawn before posterior sampling. This is optional and primarily for model diagnostics.

validation

Validation and holdout orchestration settings. See the validation guide for strategy selection advice.

# Option 1: No validation (default)
validation:
  strategy: none

# Option 2: Blocked tail
validation:
  strategy: blocked_tail
  holdout_size: 8

# Option 3: Rolling origin
validation:
  strategy: rolling_origin
  initial_train_size: 52
  test_size: 4
  step_size: 4          # Must equal test_size
  max_splits: 3         # At least 2
  • strategy — One of "none", "blocked_tail", or "rolling_origin".
  • holdout_size — Required for blocked_tail. Number of time periods to hold out from the end of the series.
  • initial_train_size, test_size — Required for rolling_origin.
  • step_size — Optional for rolling_origin. Must equal test_size if set. Defaults to test_size.
  • max_splits — Optional for rolling_origin. Must be at least 2.

Validation rules:

  • blocked_tail rejects rolling-origin parameters.
  • rolling_origin rejects holdout_size.
  • none rejects all holdout and rolling-origin parameters.
  • Legacy holdout_size without explicit strategy is rejected.

exports

Output switches for diagnostics and model-selection artefacts.

exports:
  use_kpi: false                       # Use KPI-based metrics
  batch_size: 1000                     # Batch size for Meridian analysis
  export_predictive_accuracy: true     # Write predictive_accuracy.csv
  export_review_summary: true          # Write review_summary.json
  export_model_selection: true         # Write LOO/WAIC outputs
  export_plots: true                   # Write PNG plot artefacts

All fields have defaults. If the entire exports section is omitted, all exports are enabled with default settings.

response_curves

Optional. If omitted, the response curves stage is skipped.

response_curves:
  spend_multipliers: [0.0, 0.5, 1.0, 1.5, 2.0]
  use_posterior: true
  by_reach: true
  use_optimal_frequency: false
  confidence_level: 0.9
  • spend_multipliers — Required. Non-empty list of non-negative floats.
  • confidence_level — Must be strictly between 0 and 1.

optimisation

Optional. If omitted, the optimisation stage is skipped.

optimisation:
  start_date: "2025-01-01"
  end_date: "2025-12-31"
  budget:
    mode: fixed_total                  # or "relative_reference_window_total"
    value: 1000000.0
  use_posterior: true
  use_optimal_frequency: true
  confidence_level: 0.9
  • start_date, end_date — ISO format YYYY-MM-DD. end_date must be on or after start_date.
  • budget.mode — Either "fixed_total" (absolute budget) or "relative_reference_window_total" (multiplier against the reference window’s total spend).
  • budget.value — Positive float. For fixed_total, this is the absolute budget. For relative_reference_window_total, this is a multiplier (e.g. 1.1 means 110% of the reference window total).

Validation strictness

All configuration models use Pydantic’s extra="forbid" mode. Any unexpected key in the YAML file will produce a clear validation error. This prevents silent misconfiguration from typos or outdated keys.

$ meridian-tools run --config bad.yml
# pydantic.ValidationError: 1 validation error for MeridianToolsConfig
# exports -> export_pridictive_accuracy
#   Extra inputs are not permitted

Path resolution

Relative paths in data.path are resolved against the directory containing the YAML config file, not the current working directory. This means:

# If config is at /workspace/configs/project.yml
data:
  path: ../inputs/weekly.csv
# Resolves to /workspace/inputs/weekly.csv

The resolved path is written to config.resolved.yaml in the run directory. The original authored path is preserved in config.source.yaml.

Wrapper-owned preflight

Before meridian-tools creates a dated run directory, it performs one narrow wrapper-owned preflight check on the authored config and the resolved input CSV. Phase 10 keeps this boundary intentionally small so the wrapper does not become a second Meridian schema layer.

The wrapper checks exactly:

  • the resolved data.path exists and is a regular file
  • the CSV header row can be read
  • the parsed header is non-empty
  • no parsed header cell is blank after trimming whitespace
  • every authored scalar entry in data.coord_to_columns exists in the header
  • every authored list member in data.coord_to_columns exists in the header
  • every authored key in data.media_to_channel exists in the header
  • every authored key in data.media_spend_to_channel exists in the header
  • every authored key in data.reach_to_channel exists in the header
  • every authored key in data.frequency_to_channel exists in the header
  • every authored key in data.rf_spend_to_channel exists in the header
  • every authored key in data.organic_reach_to_channel exists in the header
  • every authored key in data.organic_frequency_to_channel exists in the header
  • authored list-valued coord families are non-empty
  • authored mapping fields above are non-empty
  • coord_to_columns.media and media_to_channel must be authored together
  • coord_to_columns.media_spend and media_spend_to_channel must be authored together
  • coord_to_columns.reach, coord_to_columns.frequency, reach_to_channel, and frequency_to_channel must be authored together
  • coord_to_columns.rf_spend and rf_spend_to_channel must be authored together
  • coord_to_columns.organic_reach and organic_reach_to_channel must be authored together
  • coord_to_columns.organic_frequency and organic_frequency_to_channel must be authored together

Matching is exact and case-sensitive. The wrapper does not normalise headers, apply aliases, or use fuzzy matching.

What remains Meridian-owned:

  • deep ModelSpec semantics
  • fit-dependent tensor or shape constraints
  • statistical validity checks that depend on model construction or sampling

So Phase 10 moves obvious wrapper-detectable mistakes earlier, but it does not promise to catch everything Meridian may reject later.

Full example

project:
  name: client-mmm

data:
  path: ./client_dataset.csv
  kpi_type: revenue
  coord_to_columns:
    time: week
    geo: market
    kpi: revenue
    population: population
    media: [impressions_tv, impressions_search]
    media_spend: [spend_tv, spend_search]
    controls: [promo_flag, price_index]

model_spec:
  kwargs:
    max_lag: 8
    media_prior_type: roi

fit:
  n_chains: 4
  n_adapt: 500
  n_burnin: 500
  n_keep: 1000
  seed: 20260331

validation:
  strategy: blocked_tail
  holdout_size: 8

exports:
  export_predictive_accuracy: true
  export_review_summary: true
  export_model_selection: true

response_curves:
  spend_multipliers: [0.0, 0.5, 1.0, 1.5, 2.0]
  use_posterior: true
  by_reach: true
  use_optimal_frequency: false
  confidence_level: 0.9

optimisation:
  start_date: "2025-01-01"
  end_date: "2025-12-31"
  budget:
    mode: fixed_total
    value: 1000000.0
  use_posterior: true
  use_optimal_frequency: true
  confidence_level: 0.9

Validation guide

This guide explains how to choose and configure validation strategies in meridian-tools. Validation is the process of evaluating a candidate model specification on held-out data before committing to a final production fit.

Why validation matters for MMM

Marketing Mix Models are fitted to time series data. Unlike standard supervised learning, the temporal structure of the data means that naive IID cross-validation (random train/test splits) is statistically inappropriate. meridian-tools does not implement random shuffling or naive k-fold splits. Instead, it provides two time-respecting validation strategies and a clear separation between validation runs and the final production fit.

Validation strategies

none — No validation

validation:
  strategy: none

The model is fitted on the full dataset with no holdout. Use this when you do not need candidate evaluation — for example, when rerunning a previously validated specification.

blocked_tail — Single contiguous tail holdout

validation:
  strategy: blocked_tail
  holdout_size: 8

Reserves the last holdout_size time periods as a test block. The model is fitted on all preceding periods. This is the recommended default for short MMM time series where you want one simple candidate evaluation.

When to use: Most standard MMM projects with fewer than 150 weekly observations.

How it works:

Time axis: [t1, t2, t3, t4, t5, t6, t7, t8, t9, t10]
holdout_size: 3

Train: [t1, t2, t3, t4, t5, t6, t7]
Test:  [t8, t9, t10]

The holdout mask is generated automatically and injected into Meridian’s holdout_id parameter. For geo-panel models, the mask is broadcast across all geos.

rolling_origin — Expanding-window validation

validation:
  strategy: rolling_origin
  initial_train_size: 52
  test_size: 4
  step_size: 4
  max_splits: 3

Creates multiple expanding-window splits where each successive split adds more training data. This provides a more robust evaluation signal than a single blocked tail, but requires enough history to support multiple splits.

When to use: Projects with longer time series (typically 100+ weekly observations) where you want multiple evaluation windows.

How it works:

Time axis: [t1, t2, ..., t52, t53, ..., t56, t57, ..., t60]

Split 1: Train [t1..t52], Test [t53..t56]
Split 2: Train [t1..t56], Test [t57..t60]

Constraints:

  • step_size must equal test_size (non-overlapping test windows).
  • max_splits must be at least 2.
  • initial_train_size + test_size must not exceed the number of observations.
  • The plan must yield at least two splits.

authored_holdout — User-provided holdout mask

This is not a YAML strategy setting. Instead, you provide holdout_id directly in model_spec.kwargs:

model_spec:
  kwargs:
    holdout_id: [false, false, false, true, true]

When the runner detects an authored holdout_id in the YAML, it treats the run as an authored_holdout validation run. The mask is passed through to Meridian verbatim and recorded in the validation spec artefact.

When to use: When you need a specific holdout pattern that does not follow blocked-tail or rolling-origin conventions.

CLI vs Python API

Blocked tail from the CLI

blocked_tail runs directly from the CLI because they produce one run:

meridian-tools run --config project.yml --output-dir runs

Rolling origin requires the Python API

rolling_origin is a Python-first planning surface because it produces multiple runs — one per split plus a final fit. The CLI will reject direct rolling_origin execution:

# This will fail:
meridian-tools run --config project.yml  # with strategy: rolling_origin
# ValueError: cannot execute `rolling_origin` directly

Instead, use the Python API:

from pathlib import Path

import pandas as pd

from meridian_tools.config import PipelineRunConfig, load_yaml_config
from meridian_tools.cv import build_validation_plan
from meridian_tools.runner import run_pipeline

config_path = Path("project.yml")
config = load_yaml_config(config_path)

# Read the time index from your data
data_path = config.data.path
if not data_path.is_absolute():
    data_path = (config_path.parent / data_path).resolve()

frame = pd.read_csv(data_path)
time_column = config.data.coord_to_columns["time"]
geo_column = config.data.coord_to_columns.get("geo")

time_index = frame[time_column].drop_duplicates().tolist()
geo_index = None
if geo_column is not None:
    geo_index = frame[geo_column].drop_duplicates().tolist()

# Build the validation plan
validation_plan = build_validation_plan(
    config.validation,
    time_index=time_index,
    geo_index=geo_index,
)

# Execute each validation split
for run_spec in validation_plan.validation_runs:
    run_pipeline(
        PipelineRunConfig(
            config_path=config_path,
            output_dir=Path("runs"),
            validation_spec=run_spec,
        )
    )

Separating validation from the final fit

Validation runs and the final production fit are different jobs. First you evaluate candidate specifications on held-out splits. Then, once you have chosen the specification, you run a separate full-sample fit with no holdout.

Do not reuse a validation fit as the production artefact. The validation fit was trained on a subset of the data and its posterior reflects that subset.

Final fit after blocked tail

For blocked_tail, build_validation_plan provides a final_fit_run spec:

validation_plan = build_validation_plan(config.validation, time_index, geo_index)

# Run the final fit on all data
final_result = run_pipeline(
    PipelineRunConfig(
        config_path=config_path,
        output_dir=Path("runs"),
        validation_spec=validation_plan.final_fit_run,
    )
)

Final fit after rolling origin

The same pattern works for rolling origin:

# After running all validation splits...
final_result = run_pipeline(
    PipelineRunConfig(
        config_path=config_path,
        output_dir=Path("runs"),
        validation_spec=validation_plan.final_fit_run,
    )
)

The final_fit_run spec has mode="final_fit", strategy="none", and holdout_id=None. It trains on the full time axis with no holdout.

Run directory naming

The runner automatically appends a validation-aware suffix to the run name:

Scenario Run name pattern
No validation <project_name>_<timestamp>
Blocked tail <project_name>_blocked_tail_<timestamp>
Rolling origin split 1 <project_name>_split_01_<timestamp>
Final fit <project_name>_final_fit_<timestamp>
Authored holdout <project_name>_authored_holdout_<timestamp>

Override the name with --run-name or PipelineRunConfig(run_name=...).

Validation spec artefact

Every validation-aware run writes a validation_spec.json artefact in the 10_validation/ stage directory. This JSON records:

  • mode"validation" or "final_fit"
  • strategy — the validation strategy used
  • split_label — human-readable split identifier
  • holdout_source"generated_validation", "authored_model_spec", or "none"
  • generated_holdout — whether the holdout mask was auto-generated
  • holdout_shape — shape of the holdout mask (without the actual data)
  • train_indices / test_indices — integer indices into the time axis
  • train_dates / test_dates — corresponding date values

The actual holdout mask is not stored in the JSON artefact (it can be large). It is injected into the model at runtime.

Interaction with model selection

Bayesian model selection (LOO/WAIC) is only available for runs where holdout_id is None — meaning full-sample fitted models and final-fit runs. Validation fits and authored-holdout runs write a model_selection_status.json artefact instead of LOO/WAIC outputs. See the model selection guide for details.

Model selection guide

This guide explains how meridian-tools supports Bayesian model selection using Leave-One-Out (LOO) cross-validation and the Watanabe-Akaike Information Criterion (WAIC). It covers when model selection is available, how to interpret the outputs, and how to compare multiple candidate models.

What model selection provides

Bayesian model selection uses information criteria computed from pointwise log-likelihood values to compare model specifications. Unlike predictive accuracy on a held-out set, LOO and WAIC evaluate the model’s expected predictive performance using the full posterior without requiring a separate validation split.

meridian-tools wraps ArviZ’s az.loo and az.waic with:

  • Automatic log-likelihood reconstruction for fitted Meridian models
  • Structured error handling when model selection is not possible
  • A compare_models surface for ranking multiple candidates
  • Artefact-level compatibility status in every run directory

Compatibility boundary

Model selection is only available for models where holdout_id is None. This means:

Run type Model selection available
Full-sample fit (no validation) Yes
Final-fit run (mode: final_fit) Yes
Blocked-tail validation run No
Rolling-origin validation split No
Authored-holdout run No
Bare InferenceData without log_likelihood No

This restriction exists because LOO and WAIC require the full observed likelihood surface. A holdout fit has a modified likelihood that does not represent the full data generating process. Comparing a holdout fit’s ELPD against a full fit’s ELPD would be statistically meaningless.

How it works in the pipeline

When exports.export_model_selection: true in the YAML config, the runner’s 30_model_assessment stage attempts model selection after writing diagnostics.

Compatible runs

For compatible models, the stage writes:

  • loo_summary.json — LOO summary statistics (ELPD, p_loo, SE, etc.)
  • waic_summary.json — WAIC summary statistics
  • loo_pointwise.csv — Per-observation LOO values and Pareto k diagnostics
  • waic_pointwise.csv — Per-observation WAIC values
  • model_comparison.csv — Ranked comparison table (single-model for individual runs)

Incompatible runs

For incompatible models, the stage writes a single status artefact:

  • model_selection_status.json
{
  "status": "unavailable",
  "reason_code": "holdout_fit_unsupported",
  "reason": "Model selection requires holdout_id is None ..."
}

Known reason codes:

Code Meaning
holdout_fit_unsupported The model was fitted with a holdout mask
requires_fitted_meridian_model Missing posterior samples or ArviZ InferenceData
missing_log_likelihood_group Bare InferenceData without reconstructable likelihood
meridian_internal_seam_incompatible Meridian version lacks required internal reconstruction methods

Incompatibility is non-fatal. The pipeline completes successfully and records the reason in the artefact.

Using the Python API directly

Compute LOO for a single model

from meridian_tools.model_selection import compute_loo

result = compute_loo(fitted_model, pointwise=True)

print(result.kind)          # "loo"
print(result.summary)       # {"kind": "loo", "elpd_loo": -123.4, ...}
print(result.pointwise)     # DataFrame with loo_i, pareto_k per observation

Compute WAIC for a single model

from meridian_tools.model_selection import compute_waic

result = compute_waic(fitted_model, pointwise=True)

print(result.kind)          # "waic"
print(result.summary)       # {"kind": "waic", "elpd_waic": -125.1, ...}

Compare multiple models

from meridian_tools.model_selection import compare_models

comparison = compare_models(
    {
        "model_a": fitted_model_a,
        "model_b": fitted_model_b,
    },
    ic="loo",   # or "waic"
)

print(comparison)
# DataFrame with columns: model, rank, elpd_loo, p_loo, elpd_diff, weight, se, dse, warning, scale

The comparison table is ranked by ELPD. The best model has rank 0 and elpd_diff == 0. The weight column gives stacking weights.

Check log-likelihood availability

from meridian_tools.model_selection import has_log_likelihood

if has_log_likelihood(fitted_model):
    result = compute_loo(fitted_model)

Log-likelihood reconstruction

Meridian does not store pointwise log-likelihood in its InferenceData by default. meridian-tools reconstructs it automatically when you pass a fitted Meridian model to compute_loo, compute_waic, or compare_models.

The reconstruction:

  1. Recovers unsaved posterior parameters (e.g. geo deviations, tau_g)
  2. Rebuilds the joint distribution from the posterior samples
  3. Computes observation-level log-likelihood
  4. Returns a new InferenceData with the log_likelihood group attached

The original model is never mutated. The reconstruction produces a temporary copy used only for the ArviZ computation.

You can also control this explicitly:

from meridian_tools.log_likelihood import attach_log_likelihood

# Returns new InferenceData with log_likelihood group (original unchanged)
idata_with_ll = attach_log_likelihood(fitted_model, in_place=False)

# Mutates the model's inference_data in place
attach_log_likelihood(fitted_model, in_place=True)

Interpreting the outputs

LOO summary

Field Meaning
elpd_loo Expected log pointwise predictive density (higher is better)
p_loo Effective number of parameters
se Standard error of elpd_loo
warning Whether Pareto k diagnostics indicate unreliable estimates

WAIC summary

Field Meaning
elpd_waic Expected log pointwise predictive density (WAIC estimate)
p_waic Effective number of parameters (WAIC estimate)
se Standard error of elpd_waic
warning Whether posterior variance diagnostics indicate unreliable estimates

Pareto k diagnostics

The pointwise LOO output includes a pareto_k column. Values above 0.7 indicate that the LOO approximation is unreliable for those observations. ArviZ will emit a warning if any Pareto k values exceed the threshold.

Model comparison

When comparing two or more models:

  • elpd_diff — Difference in ELPD from the best model (0 for the best)
  • dse — Standard error of the ELPD difference
  • weight — Stacking weight (how much to trust each model)
  • Models are ranked by ELPD (rank 0 is best)

A single-model comparison returns a one-row table with rank=0, elpd_diff=0, and weight=1.0.

Error handling

All model-selection errors are raised as ModelSelectionError with a structured reason_code:

from meridian_tools.model_selection import ModelSelectionError, compute_loo

try:
    result = compute_loo(candidate)
except ModelSelectionError as exc:
    print(exc.reason_code)  # e.g. "holdout_fit_unsupported"
    print(str(exc))         # Human-readable explanation

In the pipeline, these errors are caught and written to model_selection_status.json rather than failing the run.

Lifecycle management guide

meridian-tools treats completed runs as immutable artefacts. The lifecycle module provides tools to load, compare, and refresh past runs without mutating them. This guide explains each lifecycle operation and when to use it.

Core concepts

Run records

A RunRecord encapsulates a run’s metadata and artefact paths. It is loaded from a run directory by reading run_manifest.json and resolving all artefact paths against the directory.

from meridian_tools.lifecycle import load_run_record

record = load_run_record("runs/my-project_blocked_tail_20260402_073500")

print(record.run_dir)                    # Path to the run directory
print(record.manifest)                   # RunManifest with stages, timestamps, versions
print(record.config_source_path)         # Path to config.source.yaml
print(record.config_resolved_path)       # Path to config.resolved.yaml
print(record.input_data_provenance_path) # Path to input_data_provenance.json (or None for older runs)
print(record.diagnostics_bundle_path)    # Path to diagnostics_bundle.json (or None)
print(record.validation_spec_path)       # Path to validation_spec.json (or None)
print(record.model_selection_status_path)  # Path to model_selection_status.json (or None)

All paths in the record are absolute. Required artefacts (config_source, config_resolved) are validated at load time and always present. input_data_provenance is also required for manifest version 3 runs. Optional artefacts (diagnostics_bundle, validation_spec, model_selection_status) are None if not present in the manifest.

Immutability

Lifecycle operations never modify a source run directory. When you refresh a run, the output goes to a new sibling directory. When you compare runs, both source directories remain untouched.

All lifecycle functions raise LifecycleError (a RuntimeError subclass) when they encounter invalid state.

Loading a run record

From a run directory

from meridian_tools.lifecycle import load_run_record

record = load_run_record("runs/my-project_blocked_tail_20260402_073500")

From a manifest path

record = load_run_record("runs/my-project_blocked_tail_20260402_073500/run_manifest.json")

Both forms are accepted. The function detects whether the argument is a directory or a manifest file.

Validation at load time

load_run_record validates:

  • The manifest JSON is well-formed and has a supported version (0, 1, 2, or 3).
  • Required config artefact entries (config_source, config_resolved) exist in the manifest.
  • Manifest version 3 runs also include input_data_provenance.
  • Required artefact files actually exist on disk.
  • No artefact path escapes the run directory (path traversal protection).
  • Claimed optional artefacts exist on disk (a manifest that references a missing file is rejected).

If any check fails, a LifecycleError is raised with a descriptive message.

Listing run records

from meridian_tools.lifecycle import list_run_records

records = list_run_records("runs/")
for record in records:
    print(record.manifest.started_at, record.run_dir.name)

list_run_records discovers all direct child directories that contain a run_manifest.json and returns them sorted by started_at timestamp (most recent first), with run directory name as a secondary sort key.

The function requires a directory path (not a file). It will raise an error if any discovered run directory contains an invalid manifest — it does not silently skip broken runs.

Refreshing a run

Refreshing re-executes a run using its stored configuration but writes the output to a new directory. The source run is never modified.

When to refresh

  • After a Meridian upgrade — to check whether the new version produces comparable results with the same specification.
  • After a code change — to verify that refactoring did not change model outputs.
  • After extending the dataset — to refit the model with additional observations using the same validated specification.

How to refresh

from meridian_tools.lifecycle import build_refresh_run_config
from meridian_tools.runner import run_pipeline

refresh_config = build_refresh_run_config("runs/my-project_blocked_tail_20260402_073500")
result = run_pipeline(refresh_config)

build_refresh_run_config reconstructs a PipelineRunConfig from the source run’s stored configuration:

  • The execution config path points to the source run’s config.resolved.yaml.
  • The source config path points to the source run’s config.source.yaml, so the refreshed run preserves the original authored YAML in its own metadata.
  • The output directory is set to the source run’s parent directory (creating a sibling).
  • The run name suffix is stripped to produce a clean refresh name.
  • For validation runs, the validation spec is reconstructed from the stored validation_spec.json.

Refresh with overrides

You can override specific settings:

from pathlib import Path

refresh_config = build_refresh_run_config(
    "runs/my-project_blocked_tail_20260402_073500",
    output_dir=Path("runs/refreshed"),
    run_name="my-project-refresh",
)

Validation-aware refresh

If the source run was a validation run (blocked tail or rolling origin), build_refresh_run_config reconstructs the validation spec from the stored artefact, including the holdout mask geometry. For authored-holdout runs, it reuses the YAML-owned holdout from the copied config.

For final-fit runs, the refresh produces another final-fit run with the same full-sample training specification.

Comparing runs

from meridian_tools.lifecycle import compare_run_records

comparison = compare_run_records(
    "runs/my-project_blocked_tail_20260402_073500",
    "runs/my-project_blocked_tail_20260415_090000",
)
print(comparison)

compare_run_records accepts run directory paths (not RunRecord objects) and returns a pandas DataFrame with columns field, left, right, status, and changed. The compared fields include:

  • run_name and status — basic identity.
  • meridian_tools_version and meridian_version — version drift.
  • has_validation_spec and has_diagnostics_bundle — artefact presence.
  • predictive_accuracy_status and review_summary_status — diagnostics.
  • has_model_selection_outputs and model_selection_reason_code — model selection.
  • input_authored_path, input_resolved_path, input_sha256, input_size_bytes, input_mtime_utc, input_row_count, input_column_count, and input_ordered_columns — dataset identity and shape.

This is useful for auditing whether a refresh or a specification change produced materially different results.

If either run predates manifest version 3, provenance rows are reported with status == "legacy_unknown" and changed == None. That distinguishes “no stored provenance exists” from “the dataset definitely changed”.

Lifecycle workflow example

A typical lifecycle workflow for a quarterly model refresh:

from pathlib import Path
from meridian_tools.lifecycle import (
    load_run_record,
    list_run_records,
    build_refresh_run_config,
)
from meridian_tools.runner import run_pipeline

# 1. Find the most recent production run
records = list_run_records("runs/")
production_run = records[0]  # Most recent by started_at

# 2. Refresh with the updated dataset
refresh_config = build_refresh_run_config(
    production_run.run_dir,
    output_dir=Path("runs/quarterly-refresh"),
)
refresh_result = run_pipeline(refresh_config)

# 3. Compare the results
comparison = compare_run_records(production_run.run_dir, refresh_result.run_dir)
print(comparison)

Manifest versioning

The lifecycle layer supports manifest versions 0, 1, 2, and 3. Older manifests are handled gracefully with default values for fields that were added in later versions. The current version is 3.

This means you can load run directories created by earlier versions of meridian-tools without issues. The loaded RunRecord keeps the same shape, but input_data_provenance_path is None for pre-v3 runs because those manifests predate provenance capture.

Meridian Tools workflow guide

This guide shows the supported end-to-end agency workflow for meridian-tools. It starts with one YAML config, moves through candidate validation, separates the final full-sample fit from the validation runs, and ends with the artefacts you should hand over or inspect later. The examples in this guide stay inside the implemented package surface. They do not assume notebooks, dashboards, or unpublished helper scripts.

Before you start

Install Meridian first, then install meridian-tools in the same environment:

pip install -e /path/to/meridian
pip install -e ".[dev]"

Use the CLI for ordinary run execution. Use the Python API when you need rolling-origin planning, an explicit final-fit run, or lifecycle compare and refresh operations. Phase 07 does not provide a lifecycle CLI.

If you want packaged reference examples before authoring your own YAML, use the bundled demo guide in demos.md. The packaged demo launcher is meridian-tools demo .... The repo-root python runme.py ... wrapper remains available when you are working from a source checkout.

Author one YAML config

Keep the authored project definition in YAML. Keep runtime-only choices out of the YAML file. In practice, that means your source file owns the project metadata, data path, model specification, fit settings, validation settings, and export switches. Runtime-only values such as output_dir, run_name, and one concrete validation_spec belong in PipelineRunConfig or the CLI call, not in config.resolved.yaml.

Here is one exact blocked-tail config:

project:
  name: client-mmm

data:
  path: ./client_dataset.csv
  kpi_type: revenue
  coord_to_columns:
    time: week
    geo: market
    kpi: revenue
    population: population
    media: [impressions_tv, impressions_search]
    media_spend: [spend_tv, spend_search]
    controls: [promo_flag, price_index]

model_spec:
  kwargs:
    max_lag: 8
    media_prior_type: roi

fit:
  n_chains: 4
  n_adapt: 500
  n_burnin: 500
  n_keep: 1000
  seed: 20260331

validation:
  strategy: blocked_tail
  holdout_size: 8

exports:
  export_predictive_accuracy: true
  export_review_summary: true
  export_model_selection: true

Choose the right validation path

Use blocked_tail when you want one contiguous future block for candidate evaluation. This is often the right default for short MMM time series. Use rolling_origin when you have enough history to evaluate more than one expanding-window split. Do not treat rolling_origin as ordinary k-fold cross-validation. The package does not implement naive IID folds or random shuffling because that is not the right statistical workflow for MMM time series.

Validation runs and the final production fit are different jobs. First, you evaluate candidate specifications on blocked time splits. Then, once you have chosen the specification, you run a separate full-sample fit with no holdout.

Run one blocked-tail candidate from the CLI

Once the YAML file is authored, you can execute a blocked-tail candidate run directly through the CLI:

meridian-tools run --config project.yml --output-dir runs

The same packaged runner surface is available through the thin repo-root wrapper:

python runme.py run --config project.yml --output-dir runs

This command creates a dated run directory under runs/. If you need to change the output location or the visible run name, pass --output-dir or --run-name at execution time. Those are runtime-only overrides. They affect the run directory and manifest, but they do not become part of the authored YAML contract.

Plan and run rolling-origin validation through the Python API

rolling_origin is a Python-first planning surface because you need one concrete split at a time. Start with an explicit YAML definition:

validation:
  strategy: rolling_origin
  initial_train_size: 52
  test_size: 4
  step_size: 4
  max_splits: 3

Then materialise and execute the validation runs:

from pathlib import Path

import pandas as pd

from meridian_tools.config import PipelineRunConfig, load_yaml_config
from meridian_tools.cv import build_validation_plan
from meridian_tools.runner import run_pipeline

config_path = Path("project.yml")
config = load_yaml_config(config_path)

data_path = config.data.path
if not data_path.is_absolute():
    data_path = (config_path.parent / data_path).resolve()

frame = pd.read_csv(data_path)
time_column = config.data.coord_to_columns["time"]
geo_column = config.data.coord_to_columns.get("geo")

time_index = frame[time_column].drop_duplicates().tolist()
geo_index = None
if geo_column is not None:
    geo_index = frame[geo_column].drop_duplicates().tolist()

validation_plan = build_validation_plan(
    config.validation,
    time_index=time_index,
    geo_index=geo_index,
)

for run_spec in validation_plan.validation_runs:
    run_pipeline(
        PipelineRunConfig(
            config_path=config_path,
            output_dir=Path("runs"),
            validation_spec=run_spec,
        )
    )

build_validation_plan(...) gives you one concrete ValidationRunSpec per split. run_pipeline(...) remains the primitive that executes one actual run.

Run the final full-sample fit separately

After you have chosen the winning specification, run the final fit on the full sample. Do not reuse a validation fit as the production artefact.

from pathlib import Path

from meridian_tools.config import PipelineRunConfig
from meridian_tools.runner import run_pipeline

final_result = run_pipeline(
    PipelineRunConfig(
        config_path=Path("project.yml"),
        output_dir=Path("runs"),
        validation_spec=validation_plan.final_fit_run,
    )
)

print(final_result.run_dir)
print(final_result.manifest_path)

For rolling_origin and blocked_tail workflows, validation_plan.final_fit_run is the explicit no-holdout runtime spec. It keeps the boundary clear. Candidate validation and final production fitting are separate steps.

Know which artefacts matter for handoff

Each successful run directory is the handoff unit. The important files are:

  • run_manifest.json for stage status, versions, timestamps, and top-level artefact links
  • 00_run_metadata/config.source.yaml for the authored source config
  • 00_run_metadata/config.resolved.yaml for the YAML-owned config after path resolution
  • 00_run_metadata/input_data_provenance.json for the exact dataset identity used by the run
  • 10_validation/validation_spec.json when the run is validation-aware
  • 30_model_assessment/diagnostics_bundle.json for stable diagnostics metadata
  • 30_model_assessment/model_results_summary.html for the wrapped Meridian assessment summary
  • 30_model_assessment/plots/ for assessment PNG plots such as model fit and rhat review
  • 40_decomposition/summary_metrics.csv and summary_metrics.nc for decomposition exports
  • 40_decomposition/plots/ for decomposition PNG plots
  • 60_response_curves/plots/response_curves_plot.png when response-curve export is enabled
  • 70_optimisation/plots/ when optimisation export is enabled
  • 30_model_assessment model-selection outputs when the run is compatible, or 30_model_assessment/model_selection_status.json when it is not

Read those artefacts together. 30_model_assessment/diagnostics_bundle.json tells you whether predictive accuracy and review summary were exported or disabled. The assessment stage either contains the real Bayesian model-selection outputs or one explicit compatibility status artefact.

The supported Bayesian model-selection boundary is narrow and deliberate. The package supports fitted Meridian models where holdout_id is None. That means full-sample fitted models and explicit final-fit runs are compatible. Validation fits and authored holdout fits are not.

Use lifecycle helpers after a run exists

Once you have stored run directories, the lifecycle API lets you reload, compare, and refresh them without going back to notebook state.

from pathlib import Path

from meridian_tools.lifecycle import compare_run_records, load_run_record, refresh_run

validation_run_dir = Path("runs/client-mmm_blocked_tail_20260401_101500")
final_fit_run_dir = Path("runs/client-mmm_final_fit_20260401_114200")

final_fit_record = load_run_record(final_fit_run_dir)
comparison = compare_run_records(validation_run_dir, final_fit_run_dir)
refreshed = refresh_run(final_fit_run_dir, run_name="client-mmm_final_fit_refresh")

print(final_fit_record.manifest.run_name)
print(comparison)
print(refreshed.run_dir)

compare_run_records(...) gives you a metadata-level comparison. It does not attempt a raw-file diff across every output. refresh_run(...) rebuilds a new sibling run from the stored run-local artefacts. It does not overwrite the source run. Phase 07 does not provide lifecycle CLI commands, so use the Python API for these operations.

Know the staged output schema

The current run layout is:

<run_dir>/
  run_manifest.json
  00_run_metadata/
  10_validation/
  20_model_fit/
  30_model_assessment/
    plots/
  40_decomposition/
    plots/
  60_response_curves/
    plots/
  70_optimisation/
    plots/

The runner always writes:

  • 00_run_metadata
  • 20_model_fit
  • 30_model_assessment
  • 40_decomposition

The runner writes these only when applicable:

  • 10_validation
  • 60_response_curves
  • 70_optimisation

For the bundled reference examples and the exact stage-level file set, see demos.md.

A practical analyst sequence

If you want one concrete operating pattern, use this one. Author a YAML file. Run a blocked-tail candidate through the CLI when you need one held-out tail block. Use rolling_origin through build_validation_plan(...) when you need multiple expanding-window validation splits. Choose the modelling specification. Run the final full-sample fit as its own job. Review the run directory artefacts. Then use compare_run_records(...) and refresh_run(...) when you need to inspect or rerun stored work later.

Meridian Tools demo guide

This is the canonical guide to the bundled meridian-tools demos. Use it when you want one safe, reproducible, end-to-end example without client data.

The public story is simple:

  • Meridian is the modelling engine.
  • meridian-tools is the workflow wrapper.
  • The bundled demos are launched through meridian-tools surfaces, not by calling Meridian directly.

What the bundled demos are for

Phase 08 adds two bundled reference workflows:

  • timeseries
    • a national timeseries demo shipped as packaged demo data
  • geo_panel
    • a geo-panel demo shipped as packaged demo data

Both datasets are bundled non-client reference data. They exist so analysts and stakeholders can inspect the workflow, run structure, and review artefacts without using client material.

What the package adds on top of Meridian

Meridian remains responsible for the modelling and analysis primitives. meridian-tools adds the operational surface that agencies usually need around it:

  • typed YAML configuration
  • blocked-tail and rolling-origin validation workflow
  • manifest-backed run directories
  • diagnostics bundling
  • compatibility-aware Bayesian model-selection outputs
  • lifecycle compare and refresh helpers
  • a thin demo launcher for bundled reference workflows

This is why the demos are useful. They show the wrapper workflow directly, rather than asking users to reconstruct it from notebooks or internal scripts.

Demo entrypoints

List the supported demos:

meridian-tools demo --list

Run the bundled timeseries demo:

meridian-tools demo timeseries

Run the bundled geo-panel demo:

meridian-tools demo geo_panel

By default, demo runs are written under runs/demos/. If you want a different root, pass --output-dir. If you want a custom visible run name, pass --run-name.

Example:

meridian-tools demo timeseries --output-dir sandbox/demo-runs --run-name demo-timeseries-review

The repo-root checkout wrapper remains available when you are working from the source tree:

python runme.py demo --list
python runme.py demo timeseries

The same package can also run an explicit authored config:

meridian-tools run --config /path/to/project.yml --output-dir runs

The repo-root wrapper can run an explicit authored config too:

python runme.py run --config /path/to/project.yml --output-dir runs

Bundled YAML surface

The bundled demo YAML files are real meridian-tools configs. They are not legacy Abacus-style placeholders.

The authored sections used in Phase 08 are:

  • project
  • data
  • model_spec
  • fit
  • validation
  • exports
  • response_curves
  • optimisation

The Phase 08 additions are:

  • response_curves
    • required if you want the response-curve export stage to run
  • optimisation
    • required if you want the optimisation export stage to run

The bundled demos include both sections so that the full staged schema is exercised.

The default demo configs use validation.strategy: none. That keeps the reference runs model-selection compatible, so LOO and WAIC outputs are written by default.

Output schema

Each successful demo run writes one manifest-backed staged directory layout:

<run_dir>/
  run_manifest.json
  00_run_metadata/
    config.source.yaml
    config.resolved.yaml
  20_model_fit/
    meridian_model.binpb
    fit_metadata.json
  30_model_assessment/
    diagnostics_bundle.json
    predictive_accuracy.csv
    review_summary.json
    model_results_summary.html
    plots/
      model_fit.png
      rhat_boxplot.png
    loo_summary.json
    waic_summary.json
    loo_pointwise.csv
    waic_pointwise.csv
    model_comparison.csv
    # or model_selection_status.json when unavailable
  40_decomposition/
    summary_metrics.nc
    summary_metrics.csv
    plots/
      channel_contribution_area_chart.png
      contribution_waterfall_chart.png
      spend_vs_contribution_chart.png
      roi_bar_chart.png
  60_response_curves/
    response_curves.nc
    response_curves.csv
    plots/
      response_curves_plot.png
  70_optimisation/
    optimisation_summary.html
    optimised_data.nc
    optimised_data.csv
    nonoptimised_data.nc
    nonoptimised_data.csv
    optimisation_grid.csv
    plots/
      incremental_outcome_delta_plot.png
      budget_allocation_optimised_plot.png
      budget_allocation_nonoptimised_plot.png
      spend_delta_plot.png
      optimisation_response_curves_plot.png

run_manifest.json stays top-level and remains the source of truth for artefact discovery, stage status, version metadata, and relative file paths.

Always exported versus config-gated outputs

For the current Phase 08 contract:

  • always exported for successful runs:
    • 00_run_metadata
    • 20_model_fit
    • 30_model_assessment
    • 40_decomposition
    • exported only when applicable:
    • 10_validation
      • written for validation-aware runs
      • skipped for runs with no validation metadata
    • 60_response_curves
      • requires the authored response_curves section
    • 70_optimisation
      • requires the authored optimisation section

Within 30_model_assessment, model selection remains compatibility-aware:

  • compatible runs write loo, waic, and comparison outputs
  • incompatible runs write model_selection_status.json
  • compatibility unavailability is non-fatal

How to read the important outputs

Start with these artefacts:

  • run_manifest.json
    • run identity, versions, timestamps, stage status, and top-level artefact links
  • 00_run_metadata/config.source.yaml
    • the authored YAML
  • 00_run_metadata/config.resolved.yaml
    • the same YAML after runtime path resolution
  • 10_validation/validation_spec.json
    • validation provenance for validation-aware runs only
    • not present in the default bundled demos because they run as full-sample fits
  • 30_model_assessment/diagnostics_bundle.json
    • the stable machine-readable record of diagnostics export state
  • 30_model_assessment/model_results_summary.html
    • the wrapped Meridian assessment summary
  • 40_decomposition/summary_metrics.csv
    • the easiest tabular decomposition output to inspect first

For model selection, keep the boundary honest:

  • LOO and WAIC are only available for compatible fitted Meridian models
  • validation fits and other incompatible cases will record model_selection_status.json instead
  • the package does not pretend unsupported runs have valid Bayesian comparison outputs
  • the bundled demos are configured as full-sample fits, so they should write loo_summary.json and waic_summary.json by default

For response curves and optimisation:

  • these outputs are useful for scenario and allocation review
  • they are not a substitute for checking diagnostics, validation provenance, or model-selection compatibility first

For visual review, each stage now keeps its PNG exports inside a local plots/ subdirectory rather than mixing image files into the stage root. That keeps the machine-readable exports and the human-review plots in one predictable place.

If you are new to the repository, use this order:

  1. run meridian-tools demo --list
  2. run one of the bundled demos
  3. open run_manifest.json
  4. inspect 00_run_metadata/config.source.yaml
  5. inspect 30_model_assessment/diagnostics_bundle.json
  6. inspect 40_decomposition/summary_metrics.csv
  7. inspect 60_response_curves/ and 70_optimisation/ if those stages ran

If you are working from a source checkout, python runme.py demo --list and python runme.py demo ... remain equivalent convenience wrappers.

That sequence shows the wrapper value quickly: one YAML config in, one structured run directory out, with the Meridian and meridian-tools artefacts kept in one predictable place.

Troubleshooting

Common issues and solutions when working with meridian-tools.

Installation issues

meridian-tools --help fails with ImportError

Cause: The package is not installed in the active environment, or Meridian is missing.

Fix:

pip install -e ".[dev]"

If Meridian is not installed:

pip install "google-meridian[schema]==1.5.3"

RuntimeError: Saving meridian_model.binpb requires Meridian schema support

Cause: Meridian was installed without the [schema] extra.

Fix:

pip install "google-meridian[schema]==1.5.3"

RuntimeError: Saving PNG plots requires vl-convert-python

Cause: The vl-convert-python package is not installed or not importable.

Fix:

pip install "vl-convert-python>=1.7.0,<2"

Configuration errors

pydantic.ValidationError: Extra inputs are not permitted

Cause: The YAML file contains a key that is not part of the schema. This is often a typo.

Fix: Check the key name against the YAML schema reference. All config models use extra="forbid", so unexpected keys are always rejected.

Legacy holdout_size shorthand is no longer supported

Cause: The YAML has validation.holdout_size without an explicit validation.strategy.

Fix: Add strategy: blocked_tail:

validation:
  strategy: blocked_tail
  holdout_size: 8

validation.strategy: blocked_tail does not accept rolling-origin parameters

Cause: The YAML mixes blocked_tail strategy with initial_train_size, test_size, or other rolling-origin fields.

Fix: Choose one strategy. Use blocked_tail with holdout_size only, or rolling_origin with its own parameters.

optimisation.end_date must be on or after optimisation.start_date

Cause: The dates in the optimisation section are reversed.

Fix: Ensure start_date precedes end_date:

optimisation:
  start_date: "2025-01-01"
  end_date: "2025-12-31"

response_curves.spend_multipliers must not be empty

Cause: The spend_multipliers list is empty or missing.

Fix: Provide at least one non-negative value:

response_curves:
  spend_multipliers: [0.0, 0.5, 1.0, 1.5, 2.0]

Pipeline execution errors

Dependency preflight failure

Cause: A required wrapper dependency check failed before config/data preflight or run-directory creation.

Common triggers:

  • google-meridian[schema] support is unavailable
  • exports.export_plots: true is set but vl-convert-python PNG support is unavailable

Fix: Install or repair the missing runtime dependency first, then rerun.

ConfigPreflightError

Cause: meridian-tools found a wrapper-owned config or input-data issue before run-directory creation.

Common triggers:

  • data.path resolves to a missing file or a directory
  • the CSV header row cannot be read
  • the header is empty or contains blank cells
  • an authored column name does not appear in the header exactly
  • a supported media/RF family is only half-authored

Fix: Correct the authored YAML or the input CSV first, then rerun. Header matching is exact and case-sensitive in Phase 10.

ValidationExecutionContractError

Cause: The requested single-run execution path is incompatible with the authored validation setup.

Common triggers:

  • you tried to run a rolling_origin config directly from the CLI or run_pipeline(...)
  • you passed PipelineRunConfig.validation_spec while the YAML already authors model_spec.kwargs.holdout_id

Fix: For rolling_origin, build a validation plan and execute one concrete split at a time through the Python API. For authored holdouts, either keep the YAML-authored holdout_id path or remove it before supplying a runtime validation_spec. See the validation guide for the full workflow.

ModelSelectionError with reason_code: holdout_fit_unsupported

Cause: LOO/WAIC was requested for a model fitted with a holdout mask.

Not a bug. Model selection is only available for full-sample fits. The pipeline records the incompatibility in model_selection_status.json and continues. See the model selection guide.

ModelSelectionError with reason_code: meridian_internal_seam_incompatible

Cause: The installed Meridian version does not expose the internal reconstruction methods needed for log-likelihood computation.

Fix: Check the Meridian version. This package requires google-meridian[schema]==1.5.3. If you recently upgraded Meridian, the private reconstruction seams may have changed. Check the Meridian integration notes.

Run fails mid-pipeline

If a run fails after the dated run directory already exists, meridian-tools raises PipelineRunFailure. The CLI and runme.py print the concrete failed run directory, manifest path, and stage name when available. The original exception is preserved as __cause__, so --traceback still shows the underlying failure.

The manifest is written to disk after each stage. If a run fails, the run_manifest.json is left on disk and marked failed. You can inspect it to determine which stage failed:

cat runs/my-project_*/run_manifest.json | python -m json.tool

Look at the stages array. A failed stage is recorded with status: "failed" and an error message.

Validation errors

time_index must be strictly increasing with no duplicate values

Cause: The time column in your data contains duplicates or is not sorted.

Fix: Ensure your CSV data has unique, monotonically increasing time values. For geo-panel data, the time column should be unique per time period (not per geo × time combination — the function expects the deduplicated time axis).

rolling_origin must yield at least two splits

Cause: The combination of initial_train_size, test_size, and data length does not produce enough splits.

Fix: Either reduce initial_train_size, reduce test_size, or use blocked_tail instead for shorter series.

holdout_size must be smaller than the time axis

Cause: The holdout size is greater than or equal to the number of time periods.

Fix: Reduce holdout_size to leave at least one training period.

Lifecycle errors

LifecycleError when loading a run record

Cause: The run manifest is missing required entries, references a file that does not exist, or has a malformed JSON structure.

Fix: Check that the run directory was not manually modified. Required artefacts are config.source.yaml and config.resolved.yaml. diagnostics_bundle.json is optional for loading but required for new runs.

Path traversal rejection

Cause: An artefact path in the manifest resolves outside the run directory.

Not fixable by editing the manifest. This is a security check. The manifest was likely corrupted or manually edited with an invalid path.

Performance issues

Pipeline takes very long

MCMC sampling (the 20_model_fit stage) dominates wall-clock time. The meridian-tools orchestration layer adds negligible overhead.

To speed up exploratory runs:

fit:
  n_chains: 2       # Fewer chains (minimum 1)
  n_adapt: 200      # Fewer adaptation steps
  n_burnin: 200     # Fewer burn-in steps
  n_keep: 500       # Fewer kept samples

For production runs, use the defaults or increase these values for better posterior quality.

Out-of-memory during model selection

Log-likelihood reconstruction loads the full posterior into memory and creates a temporary copy of the InferenceData. For large models, this can double memory usage temporarily.

Mitigation: Reduce n_keep or n_chains if memory is constrained.

Warnings

ArviZ Pareto k warnings

Estimated shape parameter of Pareto distribution is greater than 0.7 ...

This means the LOO approximation is unreliable for some observations. Check the pointwise pareto_k values in loo_pointwise.csv. Values above 0.7 indicate influential observations.

Meridian national model auto-zeroing warnings

Hierarchical distribution parameters must be deterministically zero for
national models. eta_orf has been automatically set to Deterministic(0).

This is expected for national (non-geo) models. Meridian automatically zeros out geo-level hierarchical parameters. The warning is informational.

TensorFlow deprecation warnings

These come from TensorFlow and Meridian internals. meridian-tools groups and deduplicates them in the terminal output to reduce noise. They do not indicate a problem with your run.