---
name: mmm-modeling
description: |
  Bayesian Media Mix Modeling with PyMC-Marketing + custom fallback. Acid-test validation (Holt-Winters, Granger, VIF), industry-calibrated priors, trade marketing decomposition, Meridian scenario planning, budget optimization.

  TRIGGERS: MMM, marketing mix model, media mix, ROI, marketing attribution, budget optimization, channel contribution, adstock, saturation curve, incrementality, ROAS, scenario planning, lift test, sensitivity analysis, marginal ROI, diminishing returns, media-driven sales, trade marketing

  Use when: analyzing channel ROI, optimizing budgets, measuring incremental contribution, building saturation curves, scenario planning, decomposing KPI drivers (base+media+trade), calibrating with lift tests, geo-hierarchical modeling, validating MMM integrity
---

# Media Mix Modeling (MMM) Skill

Bayesian Media Mix Modeling using **PyMC-Marketing** (primary) with a **custom scipy-based fallback** for restricted environments. Includes Meridian-inspired scenario planning patterns for forward-looking budget optimization.

## Architecture

| Layer | Engine | When |
|-------|--------|------|
| **Primary** | `pymc-marketing` MMM class | Default. Full Bayesian MCMC, geo-hierarchical, lift calibration, HSGP TVP |
| **Fallback** | Custom scipy (scripts/) | When `pymc-marketing` unavailable. MLE + bootstrap CIs |
| **Scenario** | Adapted from Meridian patterns | Budget sweeps, fixed/flexible optimization, flighting |

**Detection logic** — try `import pymc_marketing` first; if `ImportError`, fall back to custom scripts.

## Agency Growth OS Integration

This skill operates within the 17-skill Agency Growth OS. Its position in the three cycles:

### Execution Cycle
```
media-routing-planner → mmm-modeling → measurement-incrementality
                                     ↘ budget_optimization → media-routing-planner (re-optimize)
```

### Intelligence Cycle
```
weekly-control-tower → performance-diagnosis → mmm-modeling (if root cause is channel mix)
mmm-modeling → qbr-generator (quarterly decomposition + scenario slides)
mmm-modeling → client-memory-synthesizer (store model parameters + integrity score)
```

### Chain Routing
After mmm-modeling completes, suggest the next skill via `sendPrompt()`:

| Output Produced | Suggest Next |
|----------------|-------------|
| Channel ROAS + contribution share | `measurement-incrementality` (validate claims) |
| Budget optimization allocation | `media-routing-planner` (implement in platform) |
| Integrity report with flags | `performance-diagnosis` (investigate anomalies) |
| Scenario planning deck | `qbr-generator` (embed in quarterly review) |
| Context brief + model params | `client-memory-synthesizer` (persist to tenant) |

## Data Ingestion

### Digital media data → Adspirer MCP (automated)
When the model needs digital campaign spend data (Google Ads, Meta, LinkedIn, TikTok), call the Adspirer MCP tools already connected:

```
# Claude calls Adspirer tools to pull spend data:
# - get_campaign_performance → spend by channel × date
# - get_campaign_structure → channel taxonomy
# - analyze_search_terms → search query volume (control variable)
```

**Use `tool_search` for Adspirer tools** when the user mentions digital spend data. The skill does NOT embed Adspirer tool calls directly — Claude resolves them at runtime via the connected MCP.

### Non-digital data → Human batch upload (CSV/Excel)
Data the human must provide (no MCP available):

| Data Type | Format | Used As |
|-----------|--------|---------|
| Sales / KPI | CSV with date + geo + value | Target variable (`y`) |
| TV / Radio / OOH spend | CSV with date + channel + value | Media channels |
| Trade marketing (promo, price, ACV) | CSV/Excel | Control + trade decomposition |
| Lift test results | CSV (channel, x, delta_x, delta_y, sigma) | Calibration |
| Distribution / sell-out (Nielsen/Kantar) | CSV/Excel | Control variables |

**Workflow**: Human uploads → `data_validator.py` validates → context layer resolves priors → model fits.

## Visualization Strategy

This skill ALWAYS produces visual deliverables. The strategy controls WHERE they render to avoid crashing the app.

### In-chat: Tables + lightweight SVG (always)
Every MMM run produces formatted tables in chat as immediate output:
- Channel ROAS with HDI ranges (table)
- Integrity scorecard pass/warn/fail (table)
- Contribution share % breakdown (table)
- Scenario comparison (table)

For visual charts in chat, use the Visualizer with **lightweight SVG only**:
- Max 6 channels × 50 data points per chart
- No animations, no JS interactivity
- Keep total SVG under 50KB
- One chart per Visualizer call (never stack multiple)

Suitable for inline SVG:
- Horizontal bar: contribution waterfall, ROAS comparison
- Line chart: saturation curves (≤6 lines), efficient frontier
- Donut: four-way decomposition (base/media/trade/interaction)
- Status indicators: integrity scorecard with color-coded pass/warn/fail

### File deliverables: Full charts + dashboards (always generated)
After in-chat summary, ALWAYS generate file artifacts in `/mnt/user-data/outputs/`:

| File | Content | When |
|------|---------|------|
| `mmm_results.html` | Full interactive dashboard: contributions over time, saturation curves, waterfall, scenario sliders | Every MMM run |
| `integrity_report.html` | Acid-test results with expandable detail per test | Every MMM run |
| `mmm_scenarios.xlsx` | Scenario comparison data + allocation tables | When optimization runs |
| `mmm_executive.pptx` | Slides: integrity, decomposition, ROAS, scenarios (via pptx skill) | When user requests deck |
| `integrity_report.json` | Machine-readable results for client BI integration | Every MMM run |

### Chart generation in HTML files
HTML dashboards use inline `<svg>` or lightweight Chart.js (CDN) — NOT matplotlib, NOT plotly, NOT heavy React state. This ensures they open fast in browser without crashing.

```html
<!-- Pattern for HTML dashboard charts -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<canvas id="saturation_chart"></canvas>
<script>
  new Chart(document.getElementById('saturation_chart'), {
    type: 'line',
    data: { /* from mmm results JSON */ },
    options: { responsive: true, animation: false }
  });
</script>
```

### PPTX charts
Use the `pptx` skill to embed static chart images in slides. Generate chart as PNG via matplotlib in script → embed in slide. Charts render server-side, no app crash risk.

## Quick Start (PyMC-Marketing)

```python
import arviz as az
import numpy as np
import pandas as pd
from pymc_extras.prior import Prior
from pymc_marketing.mmm import GeometricAdstock, LogisticSaturation
from pymc_marketing.mmm.multidimensional import MMM

data_df = pd.read_csv("data.csv", parse_dates=["date"])
X = data_df.drop(columns=["y"])
y = data_df["y"]

mmm = MMM(
    date_column="date",
    channel_columns=["tv", "radio", "social"],
    target_column="y",
    adstock=GeometricAdstock(l_max=6),
    saturation=LogisticSaturation(),
    yearly_seasonality=5,
)

# CRITICAL: Always fit on FULL dataset. Train/test splits are ONLY for stability assessment.
mmm.build_model(X, y)
mmm.fit(X=X, y=y, nuts_sampler="nutpie", target_accept=0.9, random_seed=42)
mmm.sample_posterior_predictive(X=X, random_seed=42)
```

## Quick Start (Custom Fallback)

```bash
# Validate data
python scripts/data_validator.py data.csv

# Generate report from results JSON
python scripts/report_generator.py model.json --roi roi.json --html
```

## Reference Architecture

| Reference File | Content | Read When |
|----------------|---------|-----------|
| `references/model_specification.md` | MMM constructor, adstock/saturation, priors, dims, scaling, prior predictive | Specifying a new model |
| `references/data_analysis.md` | EDA patterns, data format, spend shares, long format for geo | Preparing data |
| `references/model_fit.md` | Fitting, diagnostics checklist, TimeSliceCrossValidator, common issues | Fitting and diagnosing |
| `references/media_deep_dive.md` | Contributions, ROAS, saturation curves, sensitivity, incrementality | Post-fit media analysis |
| `references/budget_optimization.md` | MultiDimensionalBudgetOptimizerWrapper, bounds, sweeps, constraints | Optimizing budgets |
| `references/scenario_planning.md` | Meridian-inspired scenario planning adapted for PyMC-Marketing | Forward-looking what-ifs |
| `references/lift_test_calibration.md` | add_lift_test_measurements, data format, sigma estimation | Calibrating with experiments |
| `references/time_varying_parameters.md` | HSGPKwargs, time-varying intercept/media, when to use TVP | Non-stationary effects |
| `references/custom_model.md` | Standalone components with plain PyMC, spline baselines, custom likelihoods | Beyond MMM class |
| `references/plot_api.md` | Complete mmm.plot namespace with exact signatures | Any visualization |
| `references/diagnostics_benchmarks.md` | Convergence thresholds, industry ROI benchmarks, business logic guards | Validating results |
| `references/acid_test_validation.md` | Pre/post-model integrity tests (Holt-Winters, Granger, VIF, permutation) | Validating model truthfulness |
| `references/context_layer.md` | Industry/category/market prior calibration, benchmark resolution | Setting informed priors |
| `references/trade_marketing_decomposition.md` | Trade vs media vs base demand separation, flexible schema | CPG/FMCG/Retail decomposition |

## Scripts

| Script | Purpose | Depends On |
|--------|---------|------------|
| `scripts/data_validator.py` | Pre-modeling EDA, quality scoring, synthetic data generation | pandas, numpy, scipy |
| `scripts/report_generator.py` | CoTA-formatted HTML/MD reports with embedded charts | matplotlib (optional) |
| `scripts/acid_test.py` | Model integrity validation (Holt-Winters, Granger, VIF, absorption check) | statsmodels |
| `scripts/context_resolver.py` | Industry benchmark resolution → calibrated priors | json (no deps) |

## Model Specification (PyMC-Marketing)

### Transformations

**GeometricAdstock** — exponential decay carryover:
```python
from pymc_marketing.mmm import GeometricAdstock
adstock = GeometricAdstock(l_max=6, normalize=True)
# Default prior: alpha ~ Beta(1, 3) — favors fast decay
# Custom: priors={"alpha": Prior("Beta", alpha=2, beta=5, dims="channel")}
```

**LogisticSaturation** — S-shaped diminishing returns:
```python
from pymc_marketing.mmm import LogisticSaturation
saturation = LogisticSaturation()
# Default: lam ~ Gamma(3, 1), beta ~ HalfNormal(2)
# Informed: priors={"beta": Prior("HalfNormal", sigma=spend_shares, dims="channel")}
```

### Full Model Config

```python
from pymc_extras.prior import Prior

model_config = {
    "intercept": Prior("Normal", mu=0.2, sigma=0.05),
    "saturation_beta": Prior("HalfNormal", sigma=spend_shares, dims="channel"),
    "gamma_control": Prior("Normal", mu=0, sigma=1, dims="control"),
    "gamma_fourier": Prior("Laplace", mu=0, b=1, dims="fourier_mode"),
    "likelihood": Prior("TruncatedNormal", lower=0, sigma=Prior("HalfNormal", sigma=1)),
}
```

See `references/model_specification.md` for constructor reference, all saturation alternatives (Hill, MichaelisMenten, Tanh, Root, etc.), hierarchical prior patterns, and scaling config.

### Multidimensional (Geo-Hierarchical)

Activate with `dims=("geo",)`. Use partial pooling (default recommendation):
```python
from pymc_marketing.special_priors import LogNormalPrior

model_config = {
    "saturation_beta": LogNormalPrior(
        mean=Prior("Gamma", mu=1.0, sigma=1.0),
        std=Prior("HalfNormal", sigma=1.0),
        dims=("channel", "geo"), centered=False,
    ),
}
```

## Workflow

```
Client Brief (industry, category, market, channels, KPI)
    ↓
Context Resolution (context_resolver.py → calibrated priors)
    ↓
EDA & Data Prep (data_validator.py)
    ↓
Acid-Test Pre-Model (acid_test.py → integrity baseline)
    ↓
Model Specification (priors from context layer)
    ↓
Build Model (mmm.build_model)
    ↓
Prior Predictive Checks
    ↓
[Optional] Add Lift Test Calibration
    ↓
[Optional] Add Trade Marketing Variables (see trade_marketing_decomposition.md)
    ↓
Fit on FULL Dataset (mmm.fit with nutpie)
    ↓
Diagnostics (divergences=0, R-hat<1.01, ESS>400)
    ↓
Acid-Test Post-Model (absorption check, ROI plausibility, permutation)
    ↓
Media Deep Dive (contributions, ROAS, saturation, sensitivity)
    ↓
Four-Way Decomposition (base + media + trade + interaction)
    ↓
Budget Optimization + Scenario Planning
    ↓
Report Generation (HTML dashboard + PPTX deck + JSON API)
```

## Key APIs

### Incrementality (preferred for ROAS/CAC)

```python
roas = mmm.incrementality.contribution_over_spend(frequency="all_time")
marginal_roas = mmm.incrementality.marginal_contribution_over_spend(frequency="all_time", spend_increase_pct=0.01)
cac = mmm.incrementality.spend_over_contribution(frequency="quarterly")
```

### Summary DataFrames

```python
mmm.summary.posterior_predictive()    # mean, median, HDI, observed
mmm.summary.contributions()           # per-channel contributions
mmm.summary.roas()                     # ROAS with HDI
mmm.summary.saturation_curves()        # saturation response
mmm.summary.adstock_curves()           # decay profiles
```

### Budget Optimization

```python
from pymc_marketing.mmm.multidimensional import MultiDimensionalBudgetOptimizerWrapper

optimizer = MultiDimensionalBudgetOptimizerWrapper(model=mmm, start_date=..., end_date=...)
allocation, result = optimizer.optimize_budget(budget=1_000_000, budget_bounds=bounds)
response = optimizer.sample_response_distribution(allocation_strategy=allocation, include_carryover=True)
```

See `references/budget_optimization.md` for bounds setup, channel fixing, custom constraints, and budget sweeps.

### Scenario Planning

See `references/scenario_planning.md` for Meridian-inspired patterns adapted for PyMC-Marketing:
- Fixed budget optimization (maximize ROI at given budget)
- Flexible budget optimization (find max budget at target ROI)
- Budget sweeps with efficient frontier
- Flighting / temporal distribution
- Multi-scenario comparison (conservative/moderate/aggressive)
- Cost-per-media-unit sensitivity

### Save/Load

```python
mmm.save("mmm_model.nc", engine="h5netcdf")
loaded = MMM.load("mmm_model.nc")
```

### YAML Specification

```python
from pymc_marketing.mmm.builders.yaml import build_mmm_from_yaml
mmm = build_mmm_from_yaml("model_spec.yaml", X=X, y=y)
```

## Layer 1: Acid-Test Validation

Runs pre/post-model integrity checks to verify MMM truthfulness. **This is how you expose holdco manipulation.**

```python
from scripts.acid_test import AcidTestValidator

# Pre-model (before fitting)
validator = AcidTestValidator(df, "date", "sales", ["tv", "social", "search"])
pre_report = validator.run_pre_model_tests()

# Post-model (after fitting, with MMM results)
post_report = validator.run_post_model_tests({
    "r2": 0.82,
    "channel_contributions": {"tv": 500000, "social": 200000, "search": 300000},
    "channel_roas": {"tv": 2.1, "social": 1.5, "search": 3.2},
    "baseline_pct": 0.55,
})

print(validator.to_summary())
validator.to_json("integrity_report.json")
```

Key tests: Holt-Winters baseline, Granger causality, VIF, baseline absorption check, ROI plausibility. See `references/acid_test_validation.md`.

## Layer 2: Context Layer

Resolves industry/category/market benchmarks into calibrated priors:

```python
from scripts.context_resolver import ContextResolver

resolver = ContextResolver(
    industry="CPG", category="Beverages", market="Mexico",
    channels=["tv", "social", "search", "ooh"],
    distribution_model="indirect", trade_marketing_share=0.45,
)
brief = resolver.to_json("context_brief.json")
print(resolver.to_summary())

# Use resolved priors in model config
# brief.suggested_model_config → ready-to-use Prior strings
# brief.roi_benchmarks → feeds acid-test ROI plausibility check
# brief.adstock_priors → calibrated Beta distributions per channel
```

See `references/context_layer.md` for full benchmark databases and adaptation rules.

## Layer 3: Trade Marketing Decomposition

Four-way decomposition: base demand + media-driven + trade-driven + interaction.

Add trade variables as controls with domain-informed priors:
```python
control_columns = ["promo_depth", "price_index", "distribution_acv", "feature_flag"]

model_config = {
    "saturation_beta": Prior("HalfNormal", sigma=spend_shares, dims="channel"),
    "gamma_control": Prior("Normal", mu=trade_prior_means, sigma=trade_prior_sds, dims="control"),
}
```

For sophisticated decomposition with custom trade response curves, use a custom PyMC model. See `references/trade_marketing_decomposition.md` for flexible data schema (Tier 1-4), functional forms, and the holdco exposure play.

## Critical Rules

1. **Always fit on FULL dataset** — train/test splits are ONLY for `TimeSliceCrossValidator` stability checks
2. **Zero divergences required** — any divergences invalidate the posterior
3. **R-hat < 1.01** for all parameters before proceeding
4. **Call `add_original_scale_contribution_variable`** before `sample_posterior_predictive` to get `*_original_scale` variables
5. **Use `mmm.incrementality`** for ROAS (accounts for adstock carryover), not element-wise division
6. **Use `MultiDimensionalBudgetOptimizerWrapper`** not `BudgetOptimizer` directly (handles geo allocation)
7. **Never present platform ROAS as incremental** without caveating (Agency Growth OS rule)
8. **Search web for current industry benchmarks** before comparing ROI results

## Diagnostics Quick Reference

| Metric | Target | Critical |
|--------|--------|----------|
| Divergences | 0 | Must be 0 |
| R-hat | < 1.01 | All params |
| ESS (bulk) | > 400 | > 800 preferred |
| Posterior R² | > 0.70 | > 0.80 preferred |
| Baseline % | 20-80% | Flags over/under-attribution |

See `references/diagnostics_benchmarks.md` for full checklist and industry ROI ranges.
