Halo effects between revenue channels

Clients ask for a “halo study” when a new revenue channel starts to matter and they suspect it moves sales somewhere else. AMMM3 does not estimate halo as a model feature. This note explains why, separates the request into four distinct estimands, and sets out the designs that answer each one, ranked by the evidence they can support.

The worked example uses the released PanelMMM surface with one model per outcome. It does not add interaction terms, cross-channel parameters or a new model type.

Why the request arrives now

Social commerce channels such as TikTok Shop sell inside the platform that also carries the advertising. A brand can see TikTok Shop revenue rise while Amazon, direct-to-consumer (DTC) and retail revenue also move. The client wants to know whether TikTok activity caused those other movements, whether it took sales from them, or whether the attribution is simply landing in the wrong place. All three questions get called halo.

The same pattern appears with marketplace launches, retail media networks and any channel that is both a medium and a point of sale. The estimand problem is general; TikTok Shop is the current trigger.

The request is usually underspecified

Requests for halo work tend to be short. Common phrasings:

  • “Can you show the halo from TikTok onto Amazon?”
  • “We think TikTok is lifting everything else. Can the model prove it?”
  • “Finance says TikTok Shop is just cannibalising DTC. Is that true?”
  • “Amazon revenue is up but Amazon ads did not change. Is that TikTok?”

Each sentence points at a different causal question, a different outcome series and a different treatment variable. The client often has not decided which one they mean, and the phrasing does not force them to. “Halo” is a label for an expected positive spillover; it is not an estimand.

Vague motivation compounds the problem. The request may come from a channel owner defending a budget, a finance team suspecting cannibalisation, or a platform partner supplying a favourable case study. Each has a preferred answer. If the estimand is left open, the analysis will drift towards whichever result is easiest to produce and hardest to refute.

Before any modelling, resolve the request into one of the four designs below.

Four estimands in one halo request, each as a small graph with treatment, outcome series and sign of interest: cross-revenue halo, a positive effect of TikTok activity on another channel’s revenue, which PanelMMM expresses; media synergy, where TikTok changes another medium’s response curve, which the additive mean cannot express; cannibalisation, the same design as cross-revenue halo with a negative sign, expressible only as an unrestricted control; and misattribution, statistically the same as cross-revenue halo but reconciled against a reporting ledger

1. Cross-revenue halo

TikTok Shop activity causes additional sales on Amazon, DTC or retail.

ElementDefinition
Causal questionDoes TikTok activity increase revenue in a different sales channel?
Outcome seriesAmazon, DTC or retail revenue, one series per channel
Treatment variableTikTok media spend, impressions, or TikTok Shop activity
Sign of interestPositive
Confused withCannibalisation (same outcome series, opposite sign)

The decision this informs is whether TikTok budget should be credited with revenue that lands outside TikTok Shop, and by how much.

2. Media synergy

TikTok advertising makes another paid medium more effective. Search response per unit of search spend is higher when TikTok activity is high.

ElementDefinition
Causal questionDoes the response curve of one medium depend on the level of another?
Outcome seriesTotal revenue or a chosen revenue channel
Treatment variableThe joint level of two media, not one
Sign of interestPositive interaction
Confused withCross-revenue halo (single medium, single outcome)

This is a question about the shape of the response surface, not about where revenue lands. It requires independent co-variation in the two media and is the hardest of the four to identify from observational data.

3. Cannibalisation

TikTok Shop takes sales that would otherwise have occurred on Amazon, DTC or retail. Total revenue is unchanged or rises less than TikTok Shop revenue.

ElementDefinition
Causal questionDoes TikTok Shop activity reduce revenue in another sales channel?
Outcome seriesAmazon, DTC or retail revenue
Treatment variableTikTok Shop activity or availability
Sign of interestNegative
Confused withCross-revenue halo (same design, opposite sign)

Cross-revenue halo and cannibalisation are the same estimation problem with the sign left open. A design that constrains the effect to be positive cannot detect cannibalisation.

4. Misattribution

Sales that appear on Amazon or DTC were caused by TikTok exposure. The reporting system credits the wrong channel because the purchase happened elsewhere.

ElementDefinition
Causal questionHow much of another channel’s recorded revenue is incremental to TikTok exposure?
Outcome seriesThe other channel’s revenue
Treatment variableTikTok exposure
Sign of interestPositive
Confused withCross-revenue halo

Statistically this is cross-revenue halo. The difference is operational. The client wants to correct an attribution ledger, so they need an estimate in revenue units for a specific period, and they need it reconciled against platform-reported figures. A conditional model contribution will not settle a dispute between attribution systems unless the client agrees in advance how it will be used.

Questions to put to the client before modelling

  1. Which revenue series is the outcome? TikTok Shop revenue as the outcome cannot answer any of the four questions.
  2. What is the treatment? TikTok media spend, impressions, Shop availability and Shop revenue are different variables with different causal roles.
  3. Is a negative answer acceptable? If the design must allow cannibalisation, the effect cannot be constrained to positive support.
  4. Over what period and at what granularity? Weekly national totals often contain fewer independent movements than the row count suggests.
  5. What will the client do differently depending on the answer? If no decision changes, the study is a reporting exercise and should be scoped as one.

Record the answers. They define the estimand, and they are the reference against which the eventual claim is judged.

What AMMM3 estimates by construction

PanelMMM builds an additive mean in scaled target space:

mu = intercept
   + sum over channels of saturation(adstock(channel spend))
   + sum over controls of control * gamma_control
   + optional seasonality, trend, events and other additive effects

Each channel passes through its own adstock and saturation. The transformed channels are summed. There is no term whose value depends on two channels at once. Changing TikTok spend does not change the search coefficient or the search saturation curve.

compute_incrementality(...) records this as additive_channel_response: True in its metadata. Because each channel’s contribution depends only on its own spend, a joint intervention across all channels equals the sum of the corresponding unilateral interventions. Saturation curves the response to a channel’s own spend; it does not create a gap between the joint result and the sum of its parts. Any intuition that cutting two channels together differs from cutting each alone has no expression in this model.

Two further properties matter for halo designs.

Media channel contributions are non-negative by construction. Saturation beta priors default to HalfNormal. A variable entered as a channel cannot carry a negative coefficient, so it cannot represent cannibalisation.

Controls are unrestricted by default. gamma_control defaults to Normal(0, 2) and control_impacts accepts "unrestricted", "positive" or "negative". Controls are not scaled automatically and do not pass through adstock or saturation.

A panel dimension such as brand or revenue_channel indexes separate outcome series. Each row carries its own media. Brand A’s rows do not contain brand B’s spend, so the model excludes cross-brand effects by construction unless you copy the other channel’s activity onto the row as a regressor.

EstimandEstimated by PanelMMM today?Reason
Cross-revenue haloOnly if the other channel’s revenue is the outcome and TikTok activity is a regressorAdditive model, one outcome per fit
Media synergyNoNo interaction term in the mean
CannibalisationOnly as above, with TikTok activity as an unrestricted controlChannel priors have positive support
MisattributionSame as cross-revenue haloSame estimand, different use of the result

Design options ranked by evidential value

Claim classes follow the Agency Workflow table.

Option A: geo intervention with other-channel outcomes

Reduce or withhold TikTok advertising in a set of geographies for a fixed period. Measure Amazon, DTC and retail revenue in treated and control geographies. Analyse the contrast with the design’s own method, such as a pre-registered difference-in-differences or a matched-market comparison with stated assumptions.

The treatment is the advertising, not TikTok Shop itself. Shop availability usually cannot be varied by geography, and the client’s question concerns exposure in any case. Treat Shop activity as a downstream consequence of the advertising rather than as the intervention. Geographies are the assignment unit; stores can serve as outcome units where store-level revenue exists, but they cannot be assigned independently because they sit inside geographies.

Claim class: causal evidence for that intervention, population and period.

This is the only option that answers cross-revenue halo and cannibalisation in revenue units with an argument a sceptical client will accept. It requires geo-level control of TikTok advertising and geo-level revenue for the other channels.

The lift estimate can be used as external evidence for the time-series PanelMMM path through add_lift_test_measurements(...). Named FE and CRE do not support calibration. Calibration does not extend the causal claim to untested geographies, dates or spend ranges.

Option B: one model per outcome

Fit a separate PanelMMM for each other-channel revenue series. Use that channel’s own media as channel_columns. Enter TikTok activity as a standardised unrestricted control. Report the TikTok coefficient’s posterior sign and interval as a conditional model allocation.

Claim class: conditional model result.

This is the best observational use of AMMM3 for the question. It runs on the released surface and needs no new code. It does not identify the effect. The usual threats apply and are often severe here: TikTok activity co-moves with paid social, promotions, seasonality and platform-driven demand. Report the sensitivity of the coefficient to those controls rather than treating one specification as the answer.

A panel version is possible by stacking the outcome series under a revenue_channel dim and copying TikTok activity onto every row. This note uses one model per outcome because it keeps the identification argument visible and avoids implying that the dimension carries cross-channel structure.

Option C: interaction terms

Add a term such as f(TikTok) * g(Search) to the mean. This addresses media synergy only. It does not address where revenue lands.

Claim class: conditional model result, and a weak one without independent co-variation.

AMMM3 does not build this term. It would be a custom MuEffect. The costs are specific:

  • named FE and CRE reject custom additive effects
  • compute_incrementality(...) rejects mu_effects
  • budget optimisation, ROAS and contribution decomposition assume additive channel response and will not interpret the interaction

Interaction terms are also poorly identified when the two media are planned together, which is the normal case for TikTok and paid social. Do not add them to satisfy a halo request. Add them only when the client’s question is synergy and the data contain independent variation in both media.

Worked example: one model per outcome

The example generates synthetic weekly data with a known positive effect of TikTok activity on Amazon revenue. It fits Amazon revenue as the outcome, uses two Amazon media channels as channel_columns, and enters standardised TikTok activity as an unrestricted control. It then repeats the fit on a dataset in which TikTok activity co-moves with one of the Amazon channels.

import arviz as az
import numpy as np
import pandas as pd

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


def make_dataset(rho: float, seed: int = 7, n_weeks: int = 156) -> pd.DataFrame:
    """Return weekly Amazon revenue with a known TikTok effect.

    ``rho`` is the target correlation between TikTok activity and
    ``amazon_search`` spend. Use 0.0 for independent variation and 0.8 for
    the co-movement case.
    """
    rng = np.random.default_rng(seed)
    dates = pd.date_range("2023-01-02", periods=n_weeks, freq="W-MON")
    amazon_search = rng.gamma(shape=4.0, scale=25.0, size=n_weeks)
    amazon_display = rng.gamma(shape=3.0, scale=20.0, size=n_weeks)
    noise = rng.normal(size=n_weeks)
    search_z = (amazon_search - amazon_search.mean()) / amazon_search.std()
    tiktok_z = rho * search_z + np.sqrt(1 - rho**2) * noise
    halo = 400.0  # revenue units per standard deviation of TikTok activity
    revenue = (
        5000.0
        + 12.0 * np.sqrt(amazon_search)
        + 8.0 * np.sqrt(amazon_display)
        + halo * tiktok_z
        + rng.normal(scale=300.0, size=n_weeks)
    )
    return pd.DataFrame(
        {
            "date": dates,
            "amazon_search": amazon_search,
            "amazon_display": amazon_display,
            "tiktok_activity_z": tiktok_z,
            "amazon_revenue": revenue,
        }
    )


def fit_amazon_model(dataset: pd.DataFrame) -> PanelMMM:
    """Fit Amazon revenue with TikTok activity as an unrestricted control."""
    X = dataset.drop(columns=["amazon_revenue"])
    y = dataset["amazon_revenue"].rename("amazon_revenue")
    mmm = PanelMMM(
        date_column="date",
        target_column="amazon_revenue",
        channel_columns=["amazon_search", "amazon_display"],
        control_columns=["tiktok_activity_z"],
        control_impacts={"tiktok_activity_z": "unrestricted"},
        adstock=GeometricAdstock(l_max=4),
        saturation=LogisticSaturation(),
    )
    mmm.fit(
        X,
        y,
        draws=500,
        tune=500,
        chains=2,
        cores=2,
        progressbar=False,
        compute_convergence_checks=False,
        random_seed=42,
    )
    return mmm


def tiktok_coefficient(mmm: PanelMMM) -> tuple[float, float, float]:
    """Return the posterior mean and 94% HDI of the TikTok control coefficient."""
    gamma = mmm.idata.posterior["gamma_control"].sel(control="tiktok_activity_z")
    hdi = az.hdi(gamma, hdi_prob=0.94)["gamma_control"].values
    return float(gamma.mean()), float(hdi[0]), float(hdi[1])


independent = fit_amazon_model(make_dataset(rho=0.0))
comoving = fit_amazon_model(make_dataset(rho=0.8))

print("independent variation:", tiktok_coefficient(independent))
print("co-moving with search:", tiktok_coefficient(comoving))

gamma_control is reported on the scaled target scale. The target is max-abs scaled by default, so multiply by the target scale to recover revenue units. Compare the sign and the interval width across the two datasets rather than the point estimates alone.

The control is standardised in the generating code because AMMM3 does not scale controls. If TikTok activity is supplied in raw units, standardise it before fitting and record the transformation.

Why TikTok enters as a control

Entering TikTok activity as a control rather than a channel makes two choices explicit.

The coefficient is unrestricted. It can be negative, so the same fit can detect cannibalisation. A channel would carry a HalfNormal saturation beta and could not.

The regressor receives no adstock or saturation. If the client believes the cross-channel effect carries over several weeks, either supply a pre-computed lagged or smoothed activity series as the control, or enter TikTok as a channel and accept that the design then excludes cannibalisation by construction. State which choice was made and why.

What to inspect

Compare the two fits on these points.

CheckIndependent variationCo-movement with search
Posterior sign of gamma_controlShould recover the generating signMay still recover the sign, with a wider interval
Interval widthNarrow relative to the meanWider; TikTok and search compete for the same variation
amazon_search contributionStableShifts in the opposite direction to the TikTok coefficient
Raw correlation between tiktok_activity_z and amazon_searchNear zeroNear 0.8

Run the raw correlation check on the real dataset before fitting. If TikTok activity co-moves with the outcome channel’s own media, the model will allocate the shared variation by prior and functional form, and predictive fit will not tell you which allocation is right. See Baseline and media trade-offs for the same problem between baseline and media, and Always-on and low-variance channels for the limiting case in which a channel supplies no variation of its own and the prior decides its contribution outright.

Fit the model with and without the TikTok control and report the change in the own-channel contributions. Fit it with and without paid social, promotion and seasonality controls and report the change in the TikTok coefficient. A coefficient that survives those comparisons is still a conditional result, but it is a better-supported one.

What the example does not establish

The synthetic data have a known effect because the generating code wrote it in. Real data do not. Recovering the sign in the example shows that the model can express the estimand; it does not show that a real TikTok coefficient is causal. Demand-led budget setting, shared campaign calendars, platform promotions and measurement changes remain threats.

Reporting language by claim class

Use sentences from the class the evidence supports.

Data description. “Amazon revenue rose 14% in the twelve weeks after TikTok Shop launched. TikTok media spend rose 60% over the same period.”

Conditional model result. “In a model of Amazon revenue with Amazon media, seasonality and price as covariates, standardised TikTok activity has a posterior mean coefficient of X with a 94% interval that excludes zero. This is the model’s allocation under those assumptions. It is not an identified causal effect.”

Predictive evidence. “Adding TikTok activity to the Amazon model improved blocked holdout error by Y%. Predictive improvement does not establish that the allocation to TikTok is correct.”

Causal evidence. “In a geo experiment that reduced TikTok media by 80% in eight of thirty regions for six weeks, Amazon revenue in treated regions fell by Z% relative to control, with a stated interval. This estimate applies to that intervention, period and set of regions.”

Decision recommendation. “Given the experimental estimate and its interval, crediting TikTok with a share of Amazon revenue in the range A to B is consistent with the evidence. Extending that credit to future periods or to other channels requires further testing.”

Do not report a conditional model coefficient as “the halo”. Do not report a positive coefficient from a design that could not have produced a negative one as evidence against cannibalisation.

Modelling synergy directly

Estimating media synergy inside the model would need an interaction term in the mean, priors for it, and changes to incrementality, optimisation and decomposition so that they interpret a non-additive channel response. AMMM3 does not build this and it is not planned. The one-model-per-outcome design and the geo intervention cover the revenue-channel questions clients usually mean by halo. Synergy is a separate question with a separate, and weaker, observational identification argument.