Guide: Calculating Informative Priors for MMM
This guide describes how to calculate tighter, more informative priors for PyMC-Marketing MMM based on data characteristics.
Important: The default wide priors (intercept: Normal(mu=0, sigma=2)) often lead to better-calibrated posteriors. Only use informative priors when you have strong domain knowledge or specific requirements. Overly tight priors can lead to overconfident (underdispersed) posteriors.
When to Use Informative Priors
- You have domain knowledge about expected ROAS ranges, decay rates, or effect sizes
- You have results from previous studies or models to inform your beliefs
- Business constraints require certain parameter ranges
- You're doing sensitivity analysis to understand prior impact
Step 1: Calculate Scaled Statistics
PyMC-Marketing uses MaxAbsScaler for target and channels (divides by max absolute value).
# Calculate how your data looks in scaled space
y_scaled_max = 1.0 # By definition (MaxAbsScaler)
y_scaled_mean = y.mean() / y.max() # Typically 0.6-0.8
y_scaled_min = y.min() / y.max() # Typically 0.4-0.6
print(f"Scaled y statistics:")
print(f" min: {y_scaled_min:.3f}")
print(f" mean: {y_scaled_mean:.3f}")
print(f" max: {y_scaled_max:.3f}")
Step 2: Reason About Intercept Prior
The intercept represents predicted y when all channels = 0 (no marketing spend).
# Intercept should be LESS than scaled mean (marketing contributes positively!)
# Reasonable range in scaled space: 0.3 to 0.7
# If you believe ~20% of sales come from marketing:
intercept_mu = y_scaled_mean * 0.8
# Sigma controls uncertainty - smaller = more confident
# 0.2 = tight (use if you have strong beliefs)
# 0.5 = moderate
# 1.0+ = wide (closer to default)
intercept_sigma = 0.2
print(f"Intercept prior: Normal(mu={intercept_mu:.2f}, sigma={intercept_sigma})")
Step 3: Reason About Channel Effect Priors
saturation_beta represents the effect size per channel in scaled space.
# If 3 channels share ~20% of y, each contributes ~0.07 on average
n_channels = len(channel_columns)
expected_total_channel_contribution = 1 - intercept_mu # What's left after baseline
expected_per_channel = expected_total_channel_contribution / n_channels
# Prior sigma: allow 2-3x expected value for exploration
beta_sigma = expected_per_channel * 2.5
print(f"Channel beta prior: HalfNormal(sigma={beta_sigma:.2f})")
print(f" Expected per-channel contribution: ~{expected_per_channel:.2f}")
Step 4: Set model_config
from pymc_extras.prior import Prior
model_config = {
"intercept": Prior("Normal", mu=intercept_mu, sigma=intercept_sigma),
"saturation_beta": Prior("HalfNormal", sigma=beta_sigma),
# adstock_alpha: Beta(2,3) favors values 0.3-0.6, good default
"adstock_alpha": Prior("Beta", alpha=2, beta=3),
}
mmm = MMM(
date_column=date_column,
channel_columns=channel_columns,
adstock=GeometricAdstock(l_max=l_max),
saturation=LogisticSaturation(),
model_config=model_config,
# ... other parameters
)
Step 5: Validate with Prior Predictive Checks
After setting informative priors, always validate:
mmm.build_model(X=X, y=y)
mmm.sample_prior_predictive(X=X, samples=1000, random_seed=None # Set for reproducibility)
# Check prior predictive coverage
from mmm_lib import check_prior_predictive_coverage
coverage = check_prior_predictive_coverage(mmm, y, hdi_prob=0.94)
# With informative priors, expect:
# - HDI should be ~2-5x observed range (tighter than defaults)
# - Some negative predictions OK, but <10% of samples
# - Coverage should still be high (>80%)
Adjusting Based on Prior Predictive
| Observation | Action |
|---|---|
| HDI too wide (>10x observed range) | Decrease sigma values |
| HDI too narrow (<2x observed range) | Increase sigma values |
| Too many negative predictions (>10%) | Increase intercept mu or use HalfNormal |
| Coverage too low (<50%) | Loosen priors (increase sigmas) |
Example: Full Workflow
import pandas as pd
import numpy as np
from pymc_marketing.mmm.multidimensional import MMM
from pymc_marketing.mmm import GeometricAdstock, LogisticSaturation
from pymc_extras.prior import Prior
# Load data
df = pd.read_parquet('cleaned_data.parquet')
y = df[target_column]
channel_columns = ['tv', 'digital', 'radio']
# Step 1: Calculate scaled statistics
y_scaled_mean = y.mean() / y.max()
print(f"Scaled y mean: {y_scaled_mean:.3f}")
# Step 2: Set intercept (assume 25% from marketing)
intercept_mu = y_scaled_mean * 0.75
intercept_sigma = 0.3
# Step 3: Set channel effects
n_channels = len(channel_columns)
expected_per_channel = (1 - intercept_mu) / n_channels
beta_sigma = expected_per_channel * 2
# Step 4: Create model config
model_config = {
"intercept": Prior("Normal", mu=intercept_mu, sigma=intercept_sigma),
"saturation_beta": Prior("HalfNormal", sigma=beta_sigma),
"adstock_alpha": Prior("Beta", alpha=2, beta=3),
}
print(f"Model config:")
print(f" intercept: Normal(mu={intercept_mu:.2f}, sigma={intercept_sigma})")
print(f" saturation_beta: HalfNormal(sigma={beta_sigma:.2f})")
print(f" adstock_alpha: Beta(alpha=2, beta=3)")
# Create MMM with informative priors
mmm = MMM(
date_column='date',
channel_columns=channel_columns,
adstock=GeometricAdstock(l_max=8),
saturation=LogisticSaturation(),
model_config=model_config,
)
# Step 5: Validate
X = df[['date'] + channel_columns]
mmm.build_model(X=X, y=y)
mmm.sample_prior_predictive(X=X, samples=1000, random_seed=None # Set for reproducibility)
# Check and adjust if needed...
Warning: Posterior Calibration
Informative priors that are too tight can cause underdispersed posteriors (overconfident uncertainty estimates). Signs of this:
- 94% HDI coverage << 94% (e.g., 50-60%)
- Posterior intervals that don't contain true values
If you observe this, loosen your priors by increasing sigma values, or revert to defaults.
Additional Prior Types
Beta Distribution for Bounded Parameters
For parameters that must be between 0 and 1 (like adstock decay):
# Beta(2, 3) - mode around 0.33, favors lower values
# Beta(2, 2) - symmetric around 0.5
# Beta(3, 2) - mode around 0.67, favors higher values
model_config = {
"adstock_alpha": Prior("Beta", alpha=2, beta=3),
}
HalfNormal for Positive Parameters
For strictly positive parameters:
model_config = {
"saturation_beta": Prior("HalfNormal", sigma=0.5),
"sigma": Prior("HalfNormal", sigma=1.0),
}
LogNormal for Positive Parameters with Heavy Tails
When you expect occasional large values:
model_config = {
"saturation_lam": Prior("LogNormal", mu=0, sigma=1),
}
Domain Knowledge Integration
From Historical Data
If you have previous MMM results:
# Use posterior mean from previous model as prior mean
# Use posterior std * 2 as prior std (to allow for model differences)
previous_intercept_mean = 0.65
previous_intercept_std = 0.1
model_config = {
"intercept": Prior("Normal",
mu=previous_intercept_mean,
sigma=previous_intercept_std * 2),
}
From Industry Benchmarks
If you have industry benchmarks for ROAS or effect sizes:
# Example: TV typically has ROAS of 2-5x
# In scaled space, this might translate to beta of 0.05-0.15
model_config = {
"saturation_beta": Prior("TruncatedNormal",
mu=0.1,
sigma=0.05,
lower=0),
}
From Business Constraints
If business logic requires certain constraints:
# Example: Marketing cannot contribute more than 50% of sales
# Set intercept prior to be at least 0.5
model_config = {
"intercept": Prior("TruncatedNormal",
mu=0.7,
sigma=0.1,
lower=0.5),
}
When to Use This Skill
- Setting up initial priors for a new MMM
- Adjusting priors based on prior predictive checks
- Incorporating domain knowledge into the model
- Debugging overly wide or narrow posterior distributions
- Sensitivity analysis to understand prior impact on results