Model Specification

This section explains how PanelMMM is defined: the model structure, media transforms, priors, panel dimensions, optional time variation, and calibration hooks.

Pages

  • Model Overview - The actual PanelMMM mean structure, scaled-space formulation, and optional components.
  • Adstock and Saturation - Supported media transforms, their priors, and the adstock_first composition order.
  • Priors and Configuration - Default prior keys, model_config, transform-prior overrides, and directional control priors.
  • Time-Varying Parameters - How time_varying_intercept and time_varying_media use SoftPlusHSGP.
  • Seasonality and Trends - Built-in yearly seasonality plus custom Fourier, trend, and event effects.
  • Panel Dimensions - How dims change the shape of the data and parameters.
  • Calibration - Lift-test and cost-per-target calibration for a built model.

Subsections of Model Specification

Model Overview

PanelMMM is an additive Bayesian marketing mix model built in PyMC. This page describes the model structure that Abacus actually builds.

For input layout, see Data Preparation. For individual configuration surfaces, see the other pages in this section.

Core structure

At fit time, Abacus builds the model mean in scaled target space as:

mu =
  intercept_contribution
  + sum(channel_contribution over channel)
  + sum(control_contribution over control), if control_columns are configured
  + mundlak_contribution, if use_mundlak_cre=True
  + yearly_seasonality_contribution, if yearly_seasonality is enabled
  + any additional mu_effects

The observed target is then attached through the configured likelihood distribution with mu=mu.

What is scaled and what is not

Before the PyMC graph is built:

  • channel data is scaled according to Scaling.channel
  • the target is scaled according to Scaling.target
  • controls are not scaled automatically

That means media and target priors operate on the model scale, not directly on the original business units. For the scaling surface, see Scaling and Preprocessing.

Model components

Component Built when Shape
intercept_contribution Always effectively ("date", *dims) in the model mean
channel_contribution Always ("date", *dims, "channel")
control_contribution control_columns is set ("date", *dims, "control")
mundlak_contribution use_mundlak_cre=True dims
yearly_seasonality_contribution yearly_seasonality is set ("date", *dims)
Additional additive effects You add entries to mu_effects ("date", *dims)

Abacus also adds total_media_contribution_original_scale automatically as a deterministic on the original target scale.

Media path

Each channel column goes through the configured media transform path:

  1. scale channel input
  2. apply adstock and saturation through forward_pass(...)
  3. optionally apply a time-varying media multiplier
  4. contribute the result through channel_contribution

See Adstock and Saturation and Time-Varying Parameters.

Controls

Controls enter the model as a separate additive term:

control_contribution = control_data * gamma_control

Use controls for non-media regressors such as price, macro indicators, or competitor measures. Controls are configured with control_columns and use gamma_control priors from model_config.

Panel dimensions

dims adds extra indexing axes such as geo, brand, or market.

With dims=("geo",), the model is indexed over date and geo. With dims=("geo", "brand"), it is indexed over date, geo, and brand.

Abacus does not automatically add hierarchical pooling just because dims is set. By default, parameters are indexed over the configured panel coordinates. If you want hierarchical shrinkage across those coordinates, encode it in the priors you pass to transforms or model_config.

See Panel Dimensions.

Optional components

Need Main setting
Extra non-media regressors control_columns
Correlated random effects adjustment use_mundlak_cre=True
Built-in yearly seasonality yearly_seasonality=<int>
Time-varying intercept time_varying_intercept=True or custom HSGPBase
Time-varying media time_varying_media=True or custom HSGPBase
Additional additive effects append to mmm.mu_effects or use YAML effects
Calibration add_lift_test_measurements(...), add_cost_per_target_calibration(...)

What target_type changes

target_type is semantic metadata, not a different likelihood family.

It affects downstream reporting such as the default efficiency metric label:

  • "revenue" -> ROAS
  • "conversion" -> CPA

It does not change the fitted functional form on its own.

Python example

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

mmm = PanelMMM(
    date_column="date",
    target_column="sales",
    channel_columns=["tv", "search"],
    control_columns=["price_index"],
    dims=("geo",),
    yearly_seasonality=2,
    adstock=GeometricAdstock(l_max=8),
    saturation=LogisticSaturation(),
)

This specification gives you:

  • an intercept
  • transformed media contributions for tv and search
  • a control contribution for price_index
  • yearly Fourier seasonality
  • a panel axis over geo

Next steps

Adstock and Saturation

PanelMMM requires one adstock transform and one saturation transform. Abacus applies them inside the model graph rather than as a fixed preprocessing step.

For the econometrics framing of these transforms, see Adstock and Saturation for Econometricians.

How the transform path works

Abacus combines the two transforms through forward_pass(...):

  • if adstock_first=True, the order is adstock then saturation
  • if adstock_first=False, the order is saturation then adstock

The transformed result becomes channel_contribution on the model scale.

Adstock options

PanelMMM accepts any AdstockTransformation. The built-in options include:

Class Key parameter priors by default Notes
GeometricAdstock alpha ~ Beta(1, 3) Standard geometric carryover
BinomialAdstock alpha ~ Beta(1, 3) Alternative finite-lag carryover
DelayedAdstock alpha ~ Beta(1, 3), theta ~ HalfNormal(1) Allows a delayed peak
WeibullPDFAdstock lam ~ Gamma(mu=2, sigma=1), k ~ Gamma(mu=3, sigma=1) Flexible PDF-shaped carryover
WeibullCDFAdstock lam ~ Gamma(mu=2, sigma=1), k ~ Gamma(mu=3, sigma=1) Flexible CDF-shaped carryover

All adstock transforms also take:

  • l_max: maximum lag
  • normalize: whether the carryover weights are normalised
  • mode: convolution mode

Saturation options

PanelMMM accepts any SaturationTransformation. Common built-ins include:

Class Key parameter priors by default Notes
LogisticSaturation lam ~ Gamma(alpha=3, beta=1), beta ~ HalfNormal(2) Default retained choice
MichaelisMentenSaturation alpha ~ Gamma(mu=2, sigma=1), lam ~ HalfNormal(1) Common diminishing-returns form
HillSaturation slope ~ HalfNormal(1.5), kappa ~ HalfNormal(1.5), beta ~ HalfNormal(1.5) Flexible Hill curve
HillSaturationSigmoid sigma ~ HalfNormal(1.5), beta ~ HalfNormal(1.5), lam ~ HalfNormal(1.5) Sigmoid Hill variant
RootSaturation alpha ~ Beta(alpha=1, beta=2), beta ~ Gamma(mu=1, sigma=1) Square-root style curvature
TanhSaturation b ~ HalfNormal(1), c ~ HalfNormal(1) Hyperbolic tangent form
TanhSaturationBaselined x0, gain, r, beta all HalfNormal(1) Baselined tanh form

Default prior dims

When you pass a transform to PanelMMM, Abacus assigns default prior dims for any transform prior that does not already have explicit dims:

  • adstock priors default to (*dims, "channel")
  • saturation priors default to (*dims, "channel")

If you want a different structure, set the prior dims explicitly on the transform.

Configure transforms in Python

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

mmm = PanelMMM(
    date_column="date",
    channel_columns=["tv", "search"],
    target_column="sales",
    adstock=GeometricAdstock(l_max=8),
    saturation=LogisticSaturation(),
    adstock_first=True,
)

Example with more customised transforms:

from pymc_extras.prior import Prior

from abacus.mmm import MichaelisMentenSaturation, WeibullCDFAdstock

adstock = WeibullCDFAdstock(
    l_max=12,
    priors={
        "lam": Prior("Gamma", mu=2, sigma=1, dims=("geo", "channel")),
        "k": Prior("Gamma", mu=3, sigma=1, dims=("geo", "channel")),
    },
)

saturation = MichaelisMentenSaturation(
    priors={
        "alpha": Prior("Gamma", mu=2, sigma=1, dims=("geo", "channel")),
        "lam": Prior("HalfNormal", sigma=1, dims="geo"),
    }
)

Configure transforms in YAML

data:
  date_column: date

target:
  column: sales
  type: revenue

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

Override transform priors through priors

Transform priors also appear in model_config under prefixed variable names. For example:

  • adstock_alpha
  • adstock_lam
  • adstock_k
  • saturation_lam
  • saturation_beta

That means you can override transform priors centrally through the top-level priors if you prefer. See Priors and Configuration.

Choose the composition order

adstock_first is part of the model specification, not a plotting choice.

The current public YAML schema does not expose adstock_first; it uses the library default. If you need to change the composition order, use the Python API.

Use adstock_first=True when you want the model to interpret carryover before diminishing returns. Use False when you want each period’s spend to saturate before the carryover step.

The code path is explicit:

  • True -> saturation(adstock(x))
  • False -> adstock(saturation(x))

Common pitfalls

  • Forgetting that l_max is required for adstock classes
  • Assuming dims automatically change transform priors even when you have already set explicit incompatible dims on the transform
  • Using adstock_first=False without a substantive reason
  • Treating transform priors as if they were on original business units rather than the model scale

Next steps

Seasonality and Trends

Abacus supports one built-in seasonality switch on PanelMMM and a broader additive-effect mechanism for custom seasonality, trend, and event terms.

Built-in yearly seasonality

Set yearly_seasonality=<int> to add a yearly Fourier term directly to the main model specification.

mmm = PanelMMM(
    date_column="date",
    target_column="sales",
    channel_columns=["tv", "search"],
    yearly_seasonality=3,
    adstock=GeometricAdstock(l_max=8),
    saturation=LogisticSaturation(),
)

This creates:

  • fourier_contribution as the unsummed Fourier basis contribution
  • yearly_seasonality_contribution as the additive contribution to mu

The prior for this built-in seasonality comes from model_config["gamma_fourier"].

What yearly_seasonality means

yearly_seasonality is the Fourier order passed to YearlyFourier.

It must be a positive integer. Abacus validates this at construction time.

Custom additive effects

For anything beyond built-in yearly seasonality, use mu_effects.

Each effect must add a tensor with dims:

("date", *dims)

Abacus ships three retained additive-effect types:

Effect Use it for
FourierEffect Custom seasonal structure such as weekly or monthly Fourier terms
LinearTrendEffect Piecewise linear trend with changepoints
EventAdditiveEffect Dated events such as launches, promotions, or holidays

FourierEffect

FourierEffect wraps a FourierBase implementation such as:

  • YearlyFourier
  • MonthlyFourier
  • WeeklyFourier

Example:

from abacus.mmm.additive_effect import FourierEffect
from abacus.mmm.fourier import WeeklyFourier

mmm.mu_effects.append(
    FourierEffect(
        fourier=WeeklyFourier(n_order=3, prefix="weekly_fourier")
    )
)

LinearTrendEffect

LinearTrendEffect wraps LinearTrend, which models piecewise linear trend changes through changepoints.

Example:

from abacus.mmm import LinearTrend
from abacus.mmm.additive_effect import LinearTrendEffect

mmm.mu_effects.append(
    LinearTrendEffect(
        trend=LinearTrend(
            n_changepoints=8,
            include_intercept=False,
            dims=("geo",),
        ),
        prefix="trend",
    )
)

Events

For events, the retained public surface on PanelMMM is add_events(...).

Example:

import pandas as pd

from pymc_extras.prior import Prior

from abacus.mmm.events import EventEffect, GaussianBasis

df_events = pd.DataFrame(
    {
        "name": ["promo", "launch"],
        "start_date": pd.to_datetime(["2025-02-01", "2025-03-10"]),
        "end_date": pd.to_datetime(["2025-02-07", "2025-03-14"]),
    }
)

effect = EventEffect(
    basis=GaussianBasis(
        priors={"sigma": Prior("Gamma", mu=7, sigma=1, dims="event")}
    ),
    effect_size=Prior("Normal", mu=0, sigma=1, dims="event"),
    dims=("event",),
)

mmm.add_events(
    df_events=df_events,
    prefix="event",
    effect=effect,
)

The event effect dims must include the event prefix plus the model dims.

When to register effects

Add custom effects before you build or fit the model.

That applies to:

  • mmm.mu_effects.append(...)
  • mmm.add_events(...)

If you build the model first and only then append effects, those new terms are not part of the existing graph.

YAML effects

The YAML builder supports top-level effects: entries. Example:

effects:
  - type: linear_trend
    prefix: trend
    n_changepoints: 8
    include_intercept: false
  - type: weekly_fourier
    order: 3
    prefix: weekly_fourier

The builder appends these effects before calling build_model(...).

Choosing between built-in and custom seasonality

Use yearly_seasonality when you need a compact built-in annual effect.

Use FourierEffect when you need:

  • weekly seasonality
  • monthly seasonality
  • multiple seasonal effects together
  • custom Fourier prefixes or priors

Common pitfalls

  • Adding effects after the model has already been built
  • Using event effect dims that do not include the required prefix
  • Treating yearly_seasonality and a custom yearly Fourier effect as if they were separate concepts when they are both additive seasonal terms

Next steps

  • Read Time-Varying Parameters if you want trend or media behaviour to vary smoothly over time.
  • Read Calibration if you want to constrain the specification with external measurements.

Priors and Configuration

Abacus uses model_config to control priors on the underlying PyMC variables. Transform priors can be configured either on the transform objects themselves or through their prefixed variable names in model_config.

Where configuration lives

Surface Use it for
model_config Intercept, likelihood, controls, seasonality, Mundlak terms, time-varying config, and prefixed transform priors
adstock=... and saturation=... Transform choice plus direct transform-prior overrides
control_impacts and control_sign_policy Directional expectations for controls

Default model_config

PanelMMM.default_model_config is built from the current model state.

The default keys are:

Key Default
intercept Prior("Normal", mu=0, sigma=2, dims=dims)
likelihood Prior("Normal", sigma=Prior("HalfNormal", sigma=2, dims=dims), dims=("date", *dims))
gamma_control Prior("Normal", mu=0, sigma=2, dims=(*dims, "control"))
gamma_fourier Prior("Laplace", mu=0, b=1, dims=(*dims, "fourier_mode"))
gamma_channel_mundlak Added only when use_mundlak_cre=True
gamma_control_mundlak Added only when use_mundlak_cre=True
intercept_tvp_config Added when time_varying_intercept is enabled
media_tvp_config Added when time_varying_media is enabled

Abacus also merges in the transform-specific config exposed by the selected adstock and saturation objects.

Configure priors in Python

Use pymc_extras.prior.Prior objects when you want explicit control:

from pymc_extras.prior import Prior

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

model_config = {
    "intercept": Prior("Normal", mu=0, sigma=1, dims=("geo",)),
    "likelihood": Prior(
        "Normal",
        sigma=Prior("HalfNormal", sigma=1.5, dims=("geo",)),
        dims=("date", "geo"),
    ),
    "gamma_control": Prior("Normal", mu=0, sigma=1, dims=("geo", "control")),
    "saturation_lam": Prior("Gamma", alpha=3, beta=1, dims=("geo", "channel")),
}

mmm = PanelMMM(
    date_column="date",
    target_column="sales",
    channel_columns=["tv", "search"],
    control_columns=["price_index"],
    dims=("geo",),
    adstock=GeometricAdstock(l_max=8),
    saturation=LogisticSaturation(),
    model_config=model_config,
)

Configure priors in YAML

YAML config can express the same priors as serialised distribution mappings:

data:
  date_column: date

target:
  column: sales
  type: revenue

dimensions:
  panel: [geo]

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

priors:
  intercept:
    distribution: Normal
    mu: 0
    sigma: 1
    dims: ["geo"]
  likelihood:
    distribution: Normal
    sigma:
      distribution: HalfNormal
      sigma: 1.5
      dims: ["geo"]
    dims: ["date", "geo"]
  saturation_lam:
    distribution: Gamma
    alpha: 3
    beta: 1
    dims: ["geo", "channel"]

Abacus parses these mappings into runtime Prior or HSGPKwargs objects.

Transform priors and prefixed names

Transform parameters appear in the model under prefixed variable names.

Examples:

  • adstock alpha -> adstock_alpha
  • saturation lam -> saturation_lam
  • saturation beta -> saturation_beta

So you can override transform priors in either of these ways:

  1. pass priors={...} to the transform object
  2. override the prefixed variable in model_config

Use one style consistently within a project if you want the configuration to be easy to read.

Directional control priors

Controls are the right place for exogenous drivers whose effect may be negative, such as competitor spend, competitor price pressure, or supply-side disruptions. By default, control coefficients remain unrestricted.

You can declare expected control directions with:

  • control_impacts
  • control_sign_policy

Allowed impact values:

  • positive
  • negative
  • unrestricted

Allowed policies:

  • soft: bias the prior toward the expected sign
  • strict: use a sign-constrained prior

Python example

mmm = PanelMMM(
    date_column="date",
    channel_columns=["tv", "search"],
    control_columns=["competitor_spend", "price_index"],
    control_impacts={
        "competitor_spend": "negative",
        "price_index": "negative",
    },
    control_sign_policy="strict",
    adstock=GeometricAdstock(l_max=8),
    saturation=LogisticSaturation(),
)

YAML note

The current public YAML schema does not expose control_impacts or control_sign_policy. If you need directional control settings today, use the Python API for that part of the specification.

Constraints for directional controls

When control_impacts is configured, Abacus expects:

  • gamma_control and gamma_control_mundlak to be Normal or TruncatedNormal
  • scalar numeric mu and sigma values for those priors
  • the prior dims to include "control"

If you violate those assumptions, model build fails with a validation error.

Time-varying configuration keys

When you enable a boolean time-varying effect, Abacus uses these model_config keys:

  • intercept_tvp_config
  • media_tvp_config

Those keys can be:

  • an HSGPKwargs instance
  • a dict with HSGPKwargs fields
  • a dict in SoftPlusHSGP.parameterize_from_data(...) style, such as {"ls_lower": 1, "ls_upper": 10}

See Time-Varying Parameters.

Important scope note

Directional control priors apply to control_columns, not channel_columns. Media channels are modelled through the adstock and saturation path.

If you need full manual control over the control prior, override gamma_control and gamma_control_mundlak directly in model_config.

Common pitfalls

  • Putting control priors on media variables instead of using transform priors
  • Forgetting the prefixed transform variable names in model_config
  • Assuming dims automatically create hierarchical priors
  • Using directional control priors with incompatible gamma_control distributions

Next steps

Panel Dimensions

Use dims when your dataset is a panel rather than a single timeseries.

Examples of useful panel dimensions:

  • geo
  • brand
  • market
  • country

For the input row layout, see Panel Data Layout.

What dims does

dims tells PanelMMM which extra categorical axes exist alongside date.

With no extra dims, the model is indexed by:

  • date
  • channel
  • optionally control

With dims=("geo",), the model is indexed by:

  • date
  • geo
  • channel
  • optionally control

With dims=("geo", "brand"), it is indexed by:

  • date
  • geo
  • brand
  • channel
  • optionally control

What changes inside the model

Setting dims changes the coordinates and parameter shapes used in the PyMC graph.

Quantity No extra dims dims=("geo",)
channel_data ("date", "channel") ("date", "geo", "channel")
target_data ("date",) ("date", "geo")
channel_contribution ("date", "channel") ("date", "geo", "channel")
control_contribution ("date", "control") ("date", "geo", "control")
intercept prior dims by default () ("geo",)

Reserved names

Do not use these names in dims:

  • date
  • channel
  • control
  • fourier_mode

Abacus rejects them because they are reserved for internal coordinates.

dims does not imply automatic pooling

This is the most important modelling point.

By default, dims gives you parameters indexed by the panel coordinates, but not automatic hierarchical shrinkage across those coordinates.

For example:

  • the default intercept prior is Normal(..., dims=dims)
  • transform priors default to (*dims, "channel")
  • control coefficients default to (*dims, "control")

Those defaults create per-slice parameters. If you want hierarchical pooling across geo, brand, or another dimension, you need to encode that in the priors you supply.

Example: independent panel slices

mmm = PanelMMM(
    date_column="date",
    target_column="sales",
    channel_columns=["tv", "search"],
    dims=("geo",),
    adstock=GeometricAdstock(l_max=8),
    saturation=LogisticSaturation(),
)

With this specification, the default priors are geo-indexed, but not hierarchical by default.

Example: explicit hierarchical prior

If you want hierarchical structure, define it in the prior itself.

from pymc_extras.prior import Prior

model_config = {
    "intercept": Prior(
        "Normal",
        mu=0,
        sigma=Prior("HalfNormal", sigma=0.3),
        dims="geo",
    ),
}

You can do the same for transform priors and additive effects.

Mundlak CRE and panel dimensions

use_mundlak_cre=True only makes sense when you have at least one panel dim. Abacus enforces that.

When enabled, Abacus builds extra correlated-random-effects terms from training period means:

  • channel_mundlak_contribution
  • control_mundlak_contribution
  • mundlak_contribution

These terms live on the panel coordinates defined by dims.

Custom HSGP dims

If you use a custom SoftPlusHSGP for time-varying effects, its dims must be compatible with the panel structure.

Examples:

  • no extra dims: ("date",) or ("date", "channel") for media
  • dims=("geo",): ("date", "geo") or ("date", "geo", "channel") for media

See Time-Varying Parameters.

YAML example

data:
  date_column: date

target:
  column: sales
  type: revenue

dimensions:
  panel: [geo, brand]

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

Your dataset must then contain both geo and brand columns.

Common pitfalls

  • Using reserved names in dims
  • Assuming dims implies automatic partial pooling
  • Enabling use_mundlak_cre with no panel dimensions
  • Forgetting that every date + dims combination must be present in the data

Next steps

Time-Varying Parameters

Abacus supports time-varying intercept and media effects through SoftPlusHSGP, a Hilbert Space Gaussian Process approximation.

Two ways to enable time variation

For both time_varying_intercept and time_varying_media, you can pass:

  • True to use the config-driven default path
  • a custom HSGPBase instance such as SoftPlusHSGP(...)

Boolean mode

The simplest entry point is a boolean flag:

mmm = PanelMMM(
    date_column="date",
    target_column="sales",
    channel_columns=["tv", "search"],
    adstock=GeometricAdstock(l_max=8),
    saturation=LogisticSaturation(),
    time_varying_intercept=True,
    time_varying_media=True,
)

When you do this, Abacus builds a SoftPlusHSGP internally from:

  • model_config["intercept_tvp_config"]
  • model_config["media_tvp_config"]

The default config keys are HSGPKwargs with:

  • m=200
  • L=None
  • eta_lam=1
  • ls_mu=5
  • ls_sigma=10
  • cov_func=None

What the boolean defaults mean

With boolean mode:

  • time_varying_intercept=True creates intercept_latent_process over ("date", *dims)
  • time_varying_media=True creates media_temporal_latent_multiplier over ("date", *dims)

That second point matters:

boolean time_varying_media=True gives you one shared temporal multiplier per panel slice, not a different time-varying multiplier per channel

If you want channel-specific time variation, pass a custom HSGP with channel in its dims.

Custom SoftPlusHSGP

Use a custom HSGP instance when you need precise control over dims, covariance, or priors.

Example: channel-specific time-varying media in a simple timeseries model.

import numpy as np

from abacus.mmm import SoftPlusHSGP

n_dates = X["date"].nunique()

media_hsgp = SoftPlusHSGP.parameterize_from_data(
    X=np.arange(n_dates),
    dims=("date", "channel"),
    cov_func="matern32",
)

mmm = PanelMMM(
    date_column="date",
    target_column="sales",
    channel_columns=["tv", "search"],
    adstock=GeometricAdstock(l_max=8),
    saturation=LogisticSaturation(),
    time_varying_media=media_hsgp,
)

For a panel model with dims=("geo",), valid media HSGP dims include:

  • ("date", "geo")
  • ("date", "geo", "channel")

For the intercept, the custom dims should align with the target axes, typically ("date",) or ("date", *dims).

Supported covariance choices

For SoftPlusHSGP.parameterize_from_data(...), the supported covariance keywords are:

  • "expquad"
  • "matern32"
  • "matern52"

Config formats for boolean mode

The *_tvp_config entries in model_config support two formats.

HSGPKwargs style

from abacus.hsgp_kwargs import HSGPKwargs

model_config = {
    "intercept_tvp_config": HSGPKwargs(
        m=50,
        L=None,
        eta_lam=1.0,
        ls_mu=5.0,
        ls_sigma=10.0,
        cov_func=None,
    )
}

Equivalent dict form is also accepted.

parameterize_from_data style

You can also pass a dict that is forwarded to SoftPlusHSGP.parameterize_from_data(...):

model_config = {
    "intercept_tvp_config": {
        "ls_lower": 1.0,
        "ls_upper": 10.0,
    }
}

Abacus preserves that dict and uses it when constructing the HSGP.

How the latent process enters the model

Time-varying intercept

Abacus creates:

intercept_contribution = intercept_baseline * intercept_latent_process

Time-varying media

Abacus first creates a baseline transformed media contribution, then multiplies it by the temporal latent process:

channel_contribution =
  baseline_channel_contribution * media_temporal_latent_multiplier

If the custom media HSGP dims include channel, the multiplier can vary by channel. Otherwise it is broadcast across channels.

Save and load

Custom SoftPlusHSGP instances round-trip through PanelMMM.save(...) and PanelMMM.load(...).

That includes custom dims such as:

  • ("date",)
  • ("date", "channel")
  • ("date", "geo")
  • ("date", "geo", "channel")

Common pitfalls

  • Expecting time_varying_media=True to create channel-specific media multipliers
  • Using custom HSGP dims that do not align with the model dims
  • Forgetting that boolean mode uses model_config["intercept_tvp_config"] and model_config["media_tvp_config"]

Next steps

Calibration

Calibration lets you add external evidence to a built PanelMMM.

Abacus currently supports two retained calibration paths:

  • lift-test measurements through add_lift_test_measurements(...)
  • cost-per-target calibration through add_cost_per_target_calibration(...)

General rule

Both calibration methods operate on a built model, not a bare constructor.

Typical sequence:

mmm.build_model(X, y)

# optional calibration step(s) here

idata = mmm.fit(X, y)

If you try to add calibration before the model graph exists, Abacus raises an error.

Lift-test calibration

Use add_lift_test_measurements(...) to add external lift measurements against the modelled saturation behaviour.

df_lift_test = pd.DataFrame(
    {
        "channel": ["tv", "search"],
        "x": [100.0, 80.0],
        "delta_x": [20.0, 10.0],
        "delta_y": [15.0, 6.0],
        "sigma": [3.0, 2.0],
    }
)

mmm.build_model(X, y)
mmm.add_lift_test_measurements(df_lift_test)

Required columns for lift tests

Lift-test data always needs:

  • channel
  • x
  • delta_x
  • delta_y
  • sigma

It also needs:

  • every configured entry in dims
  • any additional coordinate columns required by the calibrated variables

In practice, time-varying media models usually require date, because the time-varying multiplier is indexed by date.

What Abacus does

add_lift_test_measurements(...):

  1. validates the mapping columns
  2. scales the lift-test channel and target values to the model scale
  3. maps the rows to the model coordinates
  4. adds a likelihood term named lift_measurements by default

If time_varying_media is enabled, Abacus includes the media temporal multiplier in the calibrated saturation function automatically.

Practical notes

  • Lift measurements must be monotonic in the sense enforced by the calibration graph helpers.
  • The calibration distribution defaults to pm.Gamma.
  • You can change the registered variable name with name=....

Cost-per-target calibration

Use add_cost_per_target_calibration(...) when you want soft penalties on channel cost-per-target values.

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

calibration_data = pd.DataFrame(
    {
        "geo": ["UK", "US"],
        "channel": ["tv", "search"],
        "cost_per_target": [30.0, 45.0],
        "sigma": [2.0, 3.0],
    }
)

mmm.add_cost_per_target_calibration(
    data=X,
    calibration_data=calibration_data,
    name_prefix="cpt_calibration",
)

Required prerequisites

Before you add cost-per-target calibration:

  1. build the model
  2. add channel_contribution_original_scale

The second step is required because cost-per-target calibration operates against original-scale channel contribution.

Required columns for calibration_data

calibration_data must include:

  • channel
  • cost_per_target
  • sigma
  • every configured entry in dims

Requirements for data

The data argument is the spend dataset used to compute calibrated cost per target.

After Abacus reshapes it into xarray form, its coordinates must match the built model’s:

  • same shape
  • same coordinate labels
  • same channel list

If the reshaped spend data does not match the model coordinates, Abacus raises a validation error instead of silently reordering it.

YAML calibration

The YAML builder supports calibration through a top-level calibration: list. Each step must provide an explicit method plus a params mapping.

Supported YAML calibration methods:

  • add_lift_test_measurements
  • add_cost_per_target_calibration

Example:

original_scale_vars:
  - channel_contribution

calibration:
  - method: add_lift_test_measurements
    params:
      df_lift_test:
        dataframe:
          data:
            channel: ["channel_1", "channel_2"]
            x: [100.0, 80.0]
            delta_x: [20.0, 10.0]
            delta_y: [15.0, 6.0]
            sigma: [3.0, 2.0]

Important YAML constraints:

  • calibration steps run after build_model(...)
  • original_scale_vars is applied before calibration
  • only the supported calibration methods above are available in YAML
  • dist is not supported in YAML yet for add_lift_test_measurements
  • other calibration actions should be applied through the Python API until they have explicit YAML support

Choose the right calibration path

Use lift tests when you have measured incremental response data for a specific spend change.

Use cost-per-target calibration when you want the fitted channel contribution to stay consistent with observed cost efficiency.

You can use either or both, provided the model has been built first.

Common pitfalls

  • Adding calibration before build_model(...)
  • Forgetting to add channel_contribution_original_scale before cost-per-target calibration
  • Omitting required dims columns from calibration data
  • Assuming YAML supports every Python calibration argument; dist does not currently round-trip through YAML

Next steps

  • Read Model Fitting for the fit workflow once the model has been fully specified.
  • Read Save and Load if you plan to keep calibrated models on disk.