Getting Started

This section helps you install Abacus and run your first model.

Start here if you want to:

  • set up a local environment from this repository
  • fit PanelMMM directly from Python
  • build a model from YAML
  • run the structured pipeline against one of the bundled demo configs

Pages

Subsections of Getting Started

Installation

These instructions assume you are working from a local checkout of the Abacus repository.

Prerequisites

Item Notes
Python The package requires Python 3.11 or later. The repo development environment uses Python 3.12.
Local checkout Install from the repository root, not from a published package index.
Writable temp/cache directory Useful for PyTensor compiledir and local verification commands.

This is the supported local development path for the repository.

conda env create -f environment.yml
conda activate abacus-dev
python3 -m pip install -e .

This gives you:

  • the repo-managed development environment from environment.yml
  • an editable install, so local code changes are picked up immediately

Minimal pip install from source

If you do not want the full Conda environment, you can still install Abacus directly from the repository root.

Standard install

python3 -m pip install .

Editable install

python3 -m pip install -e .

Use the editable install if you are changing code, configs, or docs locally.

Optional extras

Abacus defines a small set of optional extras in pyproject.toml.

Extra Install command Use when you need
lint python3 -m pip install .[lint] Ruff, MyPy, and related local linting tools
test python3 -m pip install .[test] Pytest and test-only dependencies
planner python3 -m pip install -e ".[planner]" Deprecated no-op compatibility marker for old install commands

If you created the environment from environment.yml, most development dependencies are already present.

The statistical scenario API does not require the planner extra. New library code should import scenario objects from abacus.scenarios. The experimental abacus-dashboard application is deprecated. Use the scenario Python API or CLI; no dashboard installation is required. Legacy app-layer paths under abacus.scenario_planner remain as advisory compatibility facades. Abacus does not vendor Dash, Plotly, or Flask dependencies.

Verify the install

A quick smoke check from the repository root:

python3 -c "from abacus.mmm.panel import PanelMMM; print(PanelMMM.__name__)"

For a real end-to-end verification path, use the repo smoke target:

make smoke_mmm

If you are working on the repo itself, the main local verification commands are:

make test
make verify_local
make verify_package

Runtime defaults for restricted environments

Some local runs need writable cache directories. If you hit PyTensor compiledir or cache-permission issues, export the same defaults used by the repo verification scripts:

export PYTENSOR_FLAGS="base_compiledir=/tmp/pytensor,linker=py"
export JAX_PLATFORMS=cpu
export XDG_CACHE_HOME=/tmp

Next steps

Quickstart: Python API

This page shows the fastest direct path from a pandas dataset to a fitted PanelMMM.

If you have not prepared your dataset yet, read Data Preparation first.

Load a dataset

The repository includes bundled demo datasets under data/demo/. The timeseries bundle is the simplest starting point because it has no extra panel dimensions.

import pandas as pd

dataset = pd.read_csv("data/demo/timeseries/dataset.csv")
dataset["date"] = pd.to_datetime(dataset["date"])

X = dataset.drop(columns=["revenue"])
y = dataset["revenue"].rename("revenue")

Construct PanelMMM

from abacus.mmm import GeometricAdstock, LogisticSaturation
from abacus.mmm.panel import PanelMMM

mmm = PanelMMM(
    date_column="date",
    target_column="revenue",
    channel_columns=[
        "channel_1",
        "channel_2",
        "channel_3",
        "channel_4",
        "channel_5",
        "channel_6",
    ],
    yearly_seasonality=2,
    adstock=GeometricAdstock(l_max=4),
    saturation=LogisticSaturation(),
)

This example uses a plain timeseries. If your dataset has panel dimensions such as geo or brand, add them with dims=(...) and keep those columns in X.

Fit the model

You can call fit() directly. If the model graph has not been built yet, Abacus builds it for you.

idata = mmm.fit(
    X,
    y,
    draws=200,
    tune=200,
    chains=2,
    cores=2,
    progressbar=False,
    compute_convergence_checks=False,
    random_seed=42,
)

fit() returns an arviz.InferenceData object and also stores it on the model instance as mmm.idata.

Prior and posterior predictive checks

You can sample prior predictive draws before fitting:

prior = mmm.sample_prior_predictive(
    X=X,
    y=y,
    samples=50,
    random_seed=42,
)

After fitting, you can sample posterior predictive draws:

post = mmm.sample_posterior_predictive(
    X=X,
    progressbar=False,
    random_seed=42,
)

By default, this also stores posterior predictive draws on mmm.idata.

When to call build_model()

Call build_model(X, y) explicitly when you want to inspect or modify the PyMC graph before sampling.

For example, you might build first so that you can add stored original-scale deterministics:

mmm.build_model(X, y)
mmm.add_original_scale_contribution_variable(
    var=["channel_contribution", "y"]
)

After that, fit the already-built model:

idata = mmm.fit(
    X,
    y,
    draws=200,
    tune=200,
    chains=2,
    cores=2,
    progressbar=False,
    compute_convergence_checks=False,
    random_seed=42,
)

Reusing a built or fitted model

An existing model graph can only be fitted with the data used to construct it. This applies to both fit() and approximate_fit(), including models restored with PanelMMM.load(). Equal copies of the training inputs are accepted; changed outcomes, predictors, dates or panel units raise ValueError before inference starts. Use a fresh model instance to fit a different dataset.

Abacus checks the retained training values and coordinates, not Python object identity. Mutating the original DataFrame or array after construction does not change the recorded training data. The check also rejects fitting after an in-place update of the graph’s training data. Posterior prediction clones the PanelMMM graph by default; keep that default if you intend to fit it again.

Building explicitly and then fitting identical data preserves additions to the graph, including calibration terms and extra deterministics. Abacus does not silently rebuild the graph when fitting inputs change.

Supply the real target when constructing a graph for prior predictive checks that you intend to fit later:

mmm.sample_prior_predictive(X=X, y=y, samples=100, random_seed=42)
idata = mmm.fit(X, y, random_seed=42)

Omitting y during prior construction creates a graph with zero targets. A later fit with different target values is rejected; construct a fresh instance for that fit.

This guard does not retrospectively validate saved results. If an earlier fit reused a graph with changed inputs, rerun it from a fresh instance.

Basic outputs

After fitting, common next steps are:

mmm.save("mmm.nc")
fig, axes = mmm.plot.posterior_predictive()

You can also inspect:

  • mmm.posterior
  • mmm.posterior_predictive
  • mmm.summary
  • mmm.diagnostics

Next steps

Quickstart: YAML Builder

Use the YAML builder to create an in-memory PanelMMM from a model specification. It builds the PyMC graph; you then fit and predict in Python. For staged outputs and manifests, use the Pipeline Runner.

Complete Installation first. The example below uses small synthetic data and short chains to check execution. It does not establish convergence, parameter recovery or a suitable model for real data.

Create a builder configuration

Work in a new scratch directory, such as sandbox/yaml-quickstart/ within your checkout. Save this as model.yml in that directory:

data:
  date_column: date

target:
  column: revenue
  type: revenue

estimator:
  type: time_series

media:
  channels: [tv, search]
  adstock:
    type: geometric
    l_max: 4
  saturation:
    type: logistic

fit:
  draws: 50
  tune: 50
  chains: 2
  cores: 1
  random_seed: 42
  progressbar: false

This configuration accepts X and y from Python. The bundled data/demo/timeseries/config.yml is a runner configuration: it also contains diagnostics, validation, prior_sensitivity and ai_advisor. The public builder rejects those runner-only blocks. Use the runner for that file; do not pass it directly to build_mmm_from_yaml(...).

Build from synthetic data

Run these Python blocks in order from the directory containing model.yml:

from pathlib import Path

import numpy as np
import pandas as pd

from abacus.mmm.builders.yaml import build_mmm_from_yaml

rng = np.random.default_rng(42)
X = pd.DataFrame({
    "date": pd.date_range("2025-01-06", periods=24, freq="W-MON"),
    "tv": rng.uniform(1, 10, size=24),
    "search": rng.uniform(1, 5, size=24),
})
y = pd.Series(
    100 + 3 * X["tv"] + 2 * X["search"] + rng.normal(0, 1, size=24),
    name="revenue",
)
config_path = Path("model.yml")
mmm = build_mmm_from_yaml(config_path, X=X, y=y)

The result is a PanelMMM with a built graph. Keep the same training data for fitting; changing it after construction raises a data-identity error.

Fit and predict

fit(...) uses the sampler defaults in the YAML fit block:

idata = mmm.fit(X, y)
predictions = mmm.sample_posterior_predictive(
    X=X,
    random_seed=42,
    progressbar=False,
    extend_idata=False,
)
print(dict(predictions.sizes))

With this configuration, the returned dataset has 24 dates and 100 combined posterior samples (two chains of 50 draws). Sampling warnings and divergences can occur with such short chains. Do not interpret these smoke-run estimates. Before interpreting a real model, choose adequate sampling settings and assess Diagnostics.

Override configuration from Python

model_kwargs takes precedence over the translated YAML constructor arguments. For sampler_config, provide the complete mapping you want to use: it replaces the YAML sampler mapping at this boundary.

short_model = build_mmm_from_yaml(
    config_path,
    X=X,
    y=y,
    model_kwargs={
        "sampler_config": {
            "draws": 20,
            "tune": 20,
            "chains": 2,
            "cores": 1,
            "random_seed": 42,
            "progressbar": False,
        }
    },
)
short_idata = short_model.fit(X, y)

This second fit only checks the override path. It is not a sensitivity or convergence assessment.

Load your own data

Pattern What you provide
In-memory data Both X and y, as above
Combined CSV data.dataset_path; the file must contain the target column
Separate CSVs data.x_path and data.y_path

Configured relative paths resolve from the YAML file’s directory. The builder normalises the configured date column. To fit after loading a CSV, load and split that data in Python too, then pass matching X and y to fit(...). See Input Data Requirements for index, missing-data and panel rules.

Optional builder blocks

Key Purpose
estimator Select a named estimator preset
dimensions Legacy panel-dimension configuration; do not combine with estimator
scaling Target and channel scaling rules
effects Additive effects attached before graph construction
priors Model-level prior overrides
fit Sampler defaults
holidays Holiday/event configuration
original_scale_vars Original-scale deterministic variables added after build
inference_data Attach saved inference data
calibration Calibration steps after build, subject to estimator support
optimization Optimisation settings accepted by the shared schema; the builder does not run optimisation

Check Choose an Estimator before adapting this example to a panel. For the Python interfaces, see Builders and Pipeline.

Quickstart: Pipeline Runner

Use the pipeline runner when you want a full staged run instead of only an in-memory model fit.

The runner writes:

  • a run manifest
  • copied and resolved config files
  • fitted model artefacts
  • posterior predictive assessment outputs
  • decomposition, diagnostics, and response-curve artefacts

Full bundled demo

From the repository root, run the complete demo with:

python3 runme.py --demo timeseries

Other bundled demos are:

  • timeseries_controls
  • geo_fe
  • geo_cre
  • geo_panel
  • geo_brand_panel

timeseries and timeseries_controls use the released time_series estimator preset. geo_fe and geo_cre use the released one-unit FE and CRE presets. geo_panel and geo_brand_panel use the advanced dimensions.panel surface; they are not named RE, FE, or CRE estimators. The named RE preset remains release-gated. See the demo catalogue for the exact semantics and release status of each recipe.

List them explicitly with:

python3 runme.py --list-demos

runme.py is a convenience wrapper around the structured pipeline. It resolves the demo config under data/demo/<demo_name>/config.yml and runs the pipeline for you.

Run the named panel presets with:

python3 runme.py --demo geo_fe
python3 runme.py --demo geo_cre

Read Choose an Estimator before treating either recipe as the basis for a real model.

The timeseries demo requests four chains with 2,000 tuning and 3,000 retained draws per chain for Stage 20. Its enabled validation stage separately requests four chains with 2,000 tuning and 2,000 retained draws per chain. Explicit validation.sampler settings override main-fit CLI settings for that refit. make smoke_mmm launches this full demo without reducing either budget.

Bounded software smoke

Use this synthetic example to check execution with a small, explicit budget. Run the following from the repository root in the installed Abacus environment. It creates a separate scratch configuration and dataset, leaving the bundled demos unchanged. Repeating the setup overwrites these two scratch inputs; each pipeline execution creates a new run directory.

from pathlib import Path

import numpy as np
import pandas as pd
import yaml

workspace = Path("sandbox/runner-smoke")
workspace.mkdir(parents=True, exist_ok=True)
rng = np.random.default_rng(42)
data = pd.DataFrame({
    "date": pd.date_range("2024-01-01", periods=24, freq="W-MON"),
    "tv": rng.uniform(10, 100, 24),
    "search": rng.uniform(5, 50, 24),
})
data["revenue"] = 100 + 0.4 * data["tv"] + 0.8 * data["search"] + rng.normal(0, 2, 24)
data.to_csv(workspace / "data.csv", index=False)
config = {
    "data": {"dataset_path": "data.csv", "date_column": "date"},
    "target": {"column": "revenue", "type": "revenue"},
    "estimator": {"type": "time_series"},
    "media": {
        "channels": ["tv", "search"],
        "adstock": {"type": "geometric", "l_max": 4},
        "saturation": {"type": "logistic"},
    },
    "original_scale_vars": ["y", "channel_contribution"],
    "fit": {
        "draws": 20, "tune": 20, "chains": 2, "cores": 1,
        "random_seed": 42, "progressbar": False,
    },
    "validation": {"enabled": False},
}
(workspace / "config.yml").write_text(yaml.safe_dump(config), encoding="utf-8")

Then run:

python3 -m abacus.pipeline.runner \
  --config sandbox/runner-smoke/config.yml \
  --output-dir sandbox/runner-smoke/results \
  --run-name execution_check \
  --prior-samples 5 \
  --random-seed 42 \
  --curve-samples 10 \
  --curve-points 10

Stage 20 uses two chains, each with 20 tuning and 20 retained draws, on one core. Stage 35 performs no fit: run_manifest.json must record validation as skipped, and no holdout scoring artefacts are expected. Prior sensitivity, both AI stages and optimisation are also skipped because their configuration blocks are absent. The run should complete through Stage 80’s evidence inventory. The resolved main-fit settings are in 00_run_metadata/config.resolved.yaml.

These small sampling budgets check software execution only. Warnings or retained divergences can occur; do not interpret these fitted estimates. To exercise holdout scoring, prepare a separate config with explicit budgets for both fits and follow Blocked Holdout Validation. A skipped validation stage is not evidence of predictive performance.

Run the pipeline from Python

The direct Python API below runs the bundled geo-panel model. It is separate from the bounded synthetic smoke above. Check the selected YAML’s validation budget before running it:

from pathlib import Path

from abacus.pipeline import PipelineRunConfig, run_pipeline

result = run_pipeline(
    PipelineRunConfig(
        config_path=Path("data/demo/geo_panel/config.yml"),
        output_dir=Path("results"),
        run_name="geo_panel_quickstart",
        prior_samples=10,
        draws=200,
        tune=200,
        chains=2,
        cores=2,
        random_seed=42,
        curve_samples=50,
        curve_points=50,
    )
)

print(result.run_dir)
print(result.manifest_path)

If the YAML config already contains data.dataset_path, you do not need to pass dataset_path again.

Run the thin CLI directly

The pipeline also exposes a thin CLI in abacus.pipeline.runner:

python3 -m abacus.pipeline.runner \
  --config data/demo/geo_panel/config.yml \
  --output-dir results \
  --run-name geo_panel_quickstart \
  --prior-samples 10 \
  --draws 200 \
  --tune 200 \
  --chains 2 \
  --cores 2 \
  --random-seed 42 \
  --curve-samples 50 \
  --curve-points 50

The CLI prints the final run directory and manifest when the pipeline completes. When the run manifest records them, it also prints paths to the estimator summary, diagnostics summary, and interpretation report. These paths are relative to the run directory.

Pipeline completion means that the configured stages finished. It does not mean that the fitted model passed its diagnostic gates or is suitable for interpretation. Review the listed diagnostic and interpretation artefacts before using model outputs.

Override data paths

Use one of these patterns:

Pattern Arguments
Combined dataset override dataset_path= in Python or --dataset-path in the CLI
Separate feature and target files x_path= and y_path= in Python or --x-path and --y-path in the CLI
Target column override target_column= in Python or --target-column in the CLI

Configured relative paths are resolved relative to the YAML config directory.

If you want Stage 50 to use different warn/fail cutoffs, add a runner-only diagnostics.thresholds block to the YAML. See YAML Configuration.

What you get back

run_pipeline(...) returns a PipelineRunResult with:

  • run_dir
  • manifest_path

The runner creates all stage directories up front, including those for skipped stages. Use the manifest to distinguish completed, skipped and failed work. See the canonical stage sequence and artefact locations, including Stage 80’s evidence inventory.

Named estimator runs also record their resolved contract in 00_run_metadata/estimator_summary.txt and 00_run_metadata/estimator_manifest.yaml. FE writes its transformed within-design screen under 10_pre_diagnostics. CRE writes structural and reference summary-basis screens under 10_pre_diagnostics, a bounded post-fit screen under 20_model_fit, and separate CRE-adjustment decomposition evidence under 40_decomposition.

60_response_curves now includes three complementary curve families:

  • saturation-only transformation artefacts
  • forward-pass direct contribution artefacts built from scaled observed history
  • adstock carryover artefacts

When to use the runner

Choose the runner when you want:

  • a reproducible run directory on disk
  • structured metadata and manifest files
  • staged artefacts for diagnostics and reporting
  • a config-driven workflow for repeated runs

If you only need to fit a model interactively in a notebook or script, start with Quickstart: Python API or Quickstart: YAML Builder.