Troubleshooting
A runbook for the moments most likely to stop a first run: a failed install, a slow or diverging sampler, a rejected dataset, a model that won't reload, or a web app that won't start. Each entry names the symptom, the cause, and the fix. For "what does a negative coefficient mean?"-style questions, see the conceptual FAQ instead.
First, confirm the install is intact
A surprising share of "it's broken" reports are a half-finished environment. Verify:
import mmm_framework
print(mmm_framework.__version__) # expect 0.2.0
from mmm_framework import load_example, BayesianMMM
panel = load_example("national") # bundled data — no files needed
print(panel.n_obs, "observations,", panel.n_channels, "channels")
If that prints without error, the library is installed correctly and the problem is in your data, config, or environment — read on.
Installation & environment
import mmm_frameworkDependencies aren't installed, or you're in the wrong environment. From a source checkout, install the library (and the dev/app groups if you need tests or the web app):
$ uv sync
# or, with pip
$ pip install -e .
# add dev + app tooling (tests, API backend, Streamlit)
$ uv sync --group dev --group app
If you installed into a global Python but run inside uv, prefix commands with
uv run so they use the project's virtual environment.
NumPyro is the fast default sampler and rides on JAX. On macOS (Intel or Apple Silicon) install the CPU JAX build — it is fast enough for MMM-scale models and avoids the experimental Metal backend, which is not supported here:
If a fit still fails inside JAX, fall back to the pure-PyMC sampler
(ModelConfigBuilder().bayesian_pymc()) or an
approximate fit to isolate whether the problem is JAX-specific.
PyTensor compiles the model graph with a C++ compiler. A conda base
environment often puts its own clang first on PATH, which then
fails to link against the system libraries. Pin the system compiler in
~/.pytensorrc:
[global]
cxx = /usr/bin/clang++
Then reactivate your shell and re-run. (Deactivating the conda base
environment entirely also works.)
npm run dev errors)Install the React app's packages once before starting the dev server:
$ npm run dev
Sampling & fitting
A 104-week national model should fit in seconds, not minutes. Two levers:
- Use a fast sampler. NumPyro (JAX) measured ~3× faster than the
default PyMC sampler at equal draws. Select it on the config with
ModelConfigBuilder().bayesian_numpyro(). If a national fit takes minutes, the PyMC sampler (bayesian_pymc()) is probably still selected. - Check the model geometry first with an approximate fit. Before
paying for full NUTS, run a MAP or variational fit in seconds to catch bad priors or a
pathological saturation/adstock setup:
# seconds-fast sanity check — NOT calibrated uncertainty results = model.fit(method="map") # or "laplace", "advi", "fullrank_advi", "pathfinder" print(results.approximate) # True"laplace"adds a Gaussian curvature approximation around the MAP point — cheap uncertainty that is better behaved than bare MAP on high-dimensional models, and a failing Hessian is itself a weak-identification warning.
Approximate ≠ trustworthy intervals
MAP / Laplace / ADVI / Pathfinder are for model checking, not decisions. Their R-hat and ESS are undefined and their uncertainty is not calibrated — re-fit with full NUTS before trusting any ROI, credible interval, or budget recommendation.
The framework annotates every fit with convergence diagnostics and warns when they fail — do not interpret an unconverged posterior. Usual remedies, in order:
- More warmup and draws:
model.fit(tune=2000, draws=2000, chains=4). - Raise the target acceptance rate to tame a stiff geometry:
model.fit(target_accept=0.95). - Tighten or reconsider priors that are fighting the data (a diffuse prior on a weakly-identified channel is a common culprit) — see the Bayesian workflow page.
The full escalation ladder — which diagnostic points to which fix, in cost order — is formalized in the research post When Sampling Fails.
When chains individually mix well yet disagree with each other (high split R-hat with clean per-chain traces), each chain has usually found a different posterior mode — label switching in latent-class models, a reflected latent factor, or an adstock↔carryover ridge. More draws will not fix this. Confirm it with Sequential Monte Carlo, an exact sampler whose independent tempered runs do not get mode-locked:
# exact sampler — approximate stays False; R-hat compares independent runs
results = model.fit(method="smc", draws=2000, chains=4)
print(results.diagnostics["log_marginal_likelihood"]) # model evidence, for Bayes factors
If SMC confirms real modes, the fix is identification — sign-anchor the latent factor, constrain or re-center the offending component — not a different sampler. SMC also estimates the log marginal likelihood, so competing fixes (adstock families, mediation structures) can be compared by Bayes factor. Note SMC is slower than NUTS on well-behaved posteriors and degrades on very high-dimensional models — it is a diagnosis and evidence tool, not a speedup.
Divergences signal the sampler can't explore part of the posterior — usually a funnel in a
hierarchical or tightly-constrained parameter. Raise target_accept (0.9 →
0.95 → 0.99), add data or a lift-test calibration to pin the offending parameter, or
simplify the structure (fewer per-geo effects) until they clear. A handful of divergences
on an otherwise-converged fit with good ESS is usually tolerable; dozens are not.
Reduce the sampler footprint — draws, chains, or both:
results = model.fit(draws=1000, tune=500, chains=2)
Data & MFF
The loader expects Master Flat File (MFF) long format: a
Period column, the dimension columns
(Geography, Product, …), and a
VariableName / VariableValue pair — one row per variable per
period. A raised MFFValidationError lists exactly which columns or variables
are missing. Common causes: a KPI or channel named in the config that doesn't appear in
VariableName, or a wide (one-column-per-variable) file that hasn't been melted
to long form (mff_from_wide_format does that). The
MFF format section shows the exact shape.
Adstock carryover and trend estimation assume an evenly-spaced series, so a gap in the KPI weeks is flagged. Fill the missing periods (carry-forward, interpolation, or a zero where a zero is real) before fitting. The loader anchors the expected weekly grid to your data's own weekday, so genuinely-contiguous Monday- or Sunday-dated weekly data will not trip this warning — if it fires, there is a real gap.
Media variables must be non-negative — a negative spend or impression value is almost always a join or sign error upstream. Screen for it before modeling with the data-quality checks or the Data Studio EDA tab, which flags negatives, outliers, and missing weeks and offers one-click treatments.
The DAG builder rejects graphs it can't compile into a valid model (a cycle, a mediator with
no path to the outcome, an edge referencing an unknown node). The
DAGValidationError message names the offending node or edge; fix the graph and
rebuild. See the causal-inference page for what makes a
well-formed measurement DAG.
Saving & loading models
Model artifacts are written with cloudpickle, which is
version-sensitive: a large mismatch in cloudpickle, PyMC, or
ArviZ between the environment that saved the model and the one loading it can break the
unpickle. (Models saved under 0.1.0 reload cleanly under 0.2.0 despite the PyMC 5→6
jump — see the changelog.)
The robust recovery: re-fit from the config
A fitted model is fully determined by its spec + data, both of which are saved
alongside the trace. If an artifact won't load, rebuild it from those rather than
fighting the pickle — the fit is cheap (seconds for a national model) and reproducible
given the recorded random_seed. In the agent/API path this is the
build_and_fit(spec, dataset_path) flow; from the library, reload the panel
and re-run BayesianMMM(...).fit() with the saved settings. Pinning the exact
version the model was saved with (pip install mmm-framework==<version>)
to load-then-re-save is the fallback when a re-fit isn't possible.
Keep the modeling environment consistent — pin mmm-framework to an exact
release (==0.2.0) and use the same lockfile everywhere you save and load models,
so cloudpickle and the core stack match.
Web app & agent
Which backend am I running?
The modern React app talks to the agent API, which runs fits in-kernel — no Redis, no worker. Only the legacy Streamlit app needs Redis + an ARQ worker. If you're on the React path, none of the Redis fixes below apply. See Running the Web Application.
Start a local Redis server before the legacy REST API and worker:
The ARQ worker that processes fitting jobs isn't running. Start it:
The agent's model is chosen by config/model_config.yaml (or
MMM_LLM_* environment variables), not hard-coded. On Google Cloud, Vertex uses
Application Default Credentials — grant the VM service account
roles/aiplatform.user and run
gcloud auth application-default login. See
the platform overview and the project's
docs/model-configuration.md for provider setup (Anthropic, OpenAI, Google,
Vertex, or a local LM Studio model).
Still stuck?
If none of the above fixes it:
- Re-read the exact error message and the line it points to — the framework's validators are written to say precisely what's wrong.
- Reproduce it against a bundled example (
load_example("national")) to tell a data problem apart from a code or environment problem. - Search or open an issue on
GitHub,
including your version (
mmm_framework.__version__), OS, and the full traceback.