Modelling Event Buildup Effects: A Technical Guide
Target Audience: Data scientists, marketing analysts, econometricians Last Updated: October 2025 AMMM Version: ≥2.0
Table of Contents
Section titled “Table of Contents”- {ref}
Introduction <introduction> - {ref}
Statistical Framework <statistical-framework> - {ref}
Implementation Guide <implementation-guide> - {ref}
Case Study: BFCM Buildup <case-study-bfcm-buildup> - {ref}
Alternative Approaches <alternative-approaches> - {ref}
Model Diagnostics <model-diagnostics> - {ref}
Common Pitfalls <common-pitfalls> - {ref}
References <references>
(introduction)=
Introduction
Section titled “Introduction”What Are Event Buildup Effects?
Section titled “What Are Event Buildup Effects?”Event buildup effects capture the pre-event period during which consumer behaviour changes in anticipation of a major event. For example:
- Black Friday/Cyber Monday (BFCM): Consumers delay purchases in the weeks before the event, expecting discounts
- Christmas: Gift-buying accelerates in the 4-6 weeks before 25 December
- Back-to-School: Purchase patterns shift 2-3 weeks before school starts
These effects are distinct from the event itself and must be modelled separately to avoid biased attribution of media effectiveness.
Why Model Buildup Effects?
Section titled “Why Model Buildup Effects?”Problem: If you don’t account for buildup periods, your MMM will misattribute sales changes to media spend rather than consumer anticipation.
Example:
Week -2 before BFCM: Media spend £100K → Sales £50K (below baseline)Week 0 (BFCM): Media spend £100K → Sales £300K (above baseline)Without buildup variables, the model sees:
- Week -2: Low ROI (£50K/£100K = 0.5×)
- Week 0: High ROI (£300K/£100K = 3.0×)
The model incorrectly concludes media is more effective during BFCM, when the true driver is the event itself (discounts, consumer anticipation). This leads to:
- Overestimated event-week media effectiveness
- Underestimated non-event media effectiveness
- Incorrect budget allocation recommendations
(statistical-framework)=
Statistical Framework
Section titled “Statistical Framework”Distributed Lag Models
Section titled “Distributed Lag Models”Event buildup effects are modelled using distributed lag models from econometrics (Almon, 1965; Greene, 2003).
General Form:
y_t = β₀ + β₁·x_t + β₂·event_minus_4_t + β₃·event_minus_3_t + ... + β_k·event_t + ε_tWhere:
y_t= KPI (sales, revenue, conversions) at time tx_t= Media spend and other controlsevent_minus_k_t= Binary indicator for k weeks before the eventevent_t= Binary indicator for the event week itselfβ_k= Incremental effect of being k weeks before the event
Key Property: Each time period (week) gets its own coefficient β_k, allowing the data to reveal the shape of the buildup curve.
Why Binary Dummies Are Statistically Sound
Section titled “Why Binary Dummies Are Statistically Sound”Common Question: “Should I use binary dummies (0/1) or continuous variables (0.25, 0.5, 0.75, 1.0)?”
Answer: Use binary dummies for exploratory analysis because:
-
No Functional Form Assumption
- Binary dummies: Let data determine if β₁ < β₂ < β₃ or β₁ > β₂ > β₃
- Continuous ramp: Assumes β₁ = 0.25·β₄, β₂ = 0.5·β₄, etc. (linear relationship)
-
Testable Hypotheses
- You can test if buildup effect exists: H₀: β₁ = β₂ = β₃ = 0
- You can test if effect is increasing: H₀: β₁ = β₂ = β₃
-
Standard Regression Framework
- Same as any other control variable
- Well-understood statistical properties
- Posterior credible intervals quantify uncertainty
When to Use Continuous Variables:
- You have strong prior evidence the effect is linear (e.g., from previous research)
- Limited data (can’t afford k parameters)
- Confirmatory analysis after discovering the pattern with dummies
(implementation-guide)=
Implementation Guide
Section titled “Implementation Guide”Step 1: Data Preparation
Section titled “Step 1: Data Preparation”Create binary dummy variables for each buildup week and the event week.
Python Example:
import pandas as pdimport numpy as np
# Load your datadata = pd.read_csv('your_data.csv')data['date'] = pd.to_datetime(data['date'])
# Initialize all buildup dummies to 0data['bfcm_week_minus_4'] = 0data['bfcm_week_minus_3'] = 0data['bfcm_week_minus_2'] = 0data['bfcm_week_minus_1'] = 0data['bfcm_event'] = 0
# Set to 1 for relevant weeks (example for 2024)# BFCM 2024: Black Friday = 29 Nov, Cyber Monday = 2 Dec# Event week (containing both): Monday 25 Nov - Sunday 1 Dec# Using .between() for date filtering (recommended)data.loc[data['date'].between('2024-10-28', '2024-11-03'), 'bfcm_week_minus_4'] = 1data.loc[data['date'].between('2024-11-04', '2024-11-10'), 'bfcm_week_minus_3'] = 1data.loc[data['date'].between('2024-11-11', '2024-11-17'), 'bfcm_week_minus_2'] = 1data.loc[data['date'].between('2024-11-18', '2024-11-24'), 'bfcm_week_minus_1'] = 1data.loc[data['date'].between('2024-11-25', '2024-12-01'), 'bfcm_event'] = 1
# Repeat for other years if you have multi-year data# ...
# Save updated datadata.to_csv('your_data_with_bfcm_dummies.csv', index=False)Critical Detail: Ensure week boundaries align with your data’s definition of “week”. If your data starts on Monday, buildup weeks should also start on Monday.
(step-2-yaml-configuration)=
Step 2: YAML Configuration
Section titled “Step 2: YAML Configuration”Add the buildup variables to your AMMM config as control variables (extra features).
Example: config.yml
# ... existing config ...
# List control variablesextra_features_cols: - temperature # Existing control (if any) - bfcm_week_minus_4 - bfcm_week_minus_3 - bfcm_week_minus_2 - bfcm_week_minus_1 - bfcm_event
# Set global prior for all control variablescustom_priors: gamma_control: dist: TruncatedNormal kwargs: mu: 0.0 sigma: 0.5 # Moderately informative prior lower: -2.0 # Allow negative (purchase delay) upper: 2.0 # Allow positive (early buying)Advanced: Per-Control Priors (Optional)
If you need different priors for different variables (e.g., wider prior for event), use vectorized sigma:
custom_priors: gamma_control: dist: TruncatedNormal kwargs: mu: 0.0 sigma: [0.5, 0.5, 0.5, 0.5, 0.5, 1.0] # Wider for event (last) lower: -2.0 upper: 2.0⚠️ Critical: The vector must match the exact order and length of extra_features_cols. Mismatches cause silent errors.
Verification:
import yamlconfig = yaml.safe_load(open('config.yml'))sigma = config['custom_priors']['gamma_control']['kwargs']['sigma']if isinstance(sigma, list): assert len(sigma) == len(config['extra_features_cols']), \ f"Sigma vector ({len(sigma)}) must match extra_features_cols ({len(config['extra_features_cols'])})"Prior Selection Notes:
- Sigma (σ): Controls coefficient variance. σ=0.5 is moderately informative.
- Lower/Upper Bounds:
- Negative lower bound allows for purchase delay (sales decrease during buildup)
- Positive upper bound allows for purchase acceleration (early buying)
- Use vectorized sigma for different variables
Step 3: Run the Model
Section titled “Step 3: Run the Model”python runme.py --data your_data_with_bfcm_dummies.csv --config config.ymlThe model will estimate 5 coefficients: β₁, β₂, β₃, β₄, β₅ for the buildup weeks and event week.
Step 4: Interpret Results
Section titled “Step 4: Interpret Results”After model fitting, examine the posterior distributions of the buildup coefficients.
Extracting Control Coefficients from Trace:
AMMM stores all control variable coefficients in a single gamma_control variable indexed by the control coordinate.
import numpy as npimport arviz as az
# Access the gamma_control posteriorcoef = trace.posterior['gamma_control']
# List of your control variables (must match extra_features_cols order)control_vars = ['temperature', 'bfcm_week_minus_4', 'bfcm_week_minus_3', 'bfcm_week_minus_2', 'bfcm_week_minus_1', 'bfcm_event']
# Extract BFCM-related coefficientsbfcm_vars = ['bfcm_week_minus_4', 'bfcm_week_minus_3', 'bfcm_week_minus_2', 'bfcm_week_minus_1', 'bfcm_event']
for var in bfcm_vars: # Select by control coordinate vals = coef.sel(control=var).values.flatten() mean = np.mean(vals) ci_low, ci_high = np.percentile(vals, [2.5, 97.5]) print(f"{var}: {mean:.3f} [{ci_low:.3f}, {ci_high:.3f}]")Note: Control coefficients in AMMM are denoted as γ (gamma), not β (beta). β is reserved for media channel coefficients.
Example Output:
Week -4: -0.05 [-0.15, 0.05] → Slight purchase delay (not significant)Week -3: -0.10 [-0.22, 0.02] → Modest purchase delayWeek -2: -0.20 [-0.35, -0.05] → Strong purchase delay (significant)Week -1: -0.15 [-0.30, 0.00] → Strong purchase delayEvent: +1.50 [+1.20, +1.80] → Large positive event effect (significant)Interpretation:
- Negative coefficients in buildup weeks: Consumers delay purchases (waiting for BFCM discounts)
- Positive coefficient in event week: Large sales spike during BFCM
- Pattern confirms U-shaped response: dip before event, spike during event
(case-study-bfcm-buildup)=
Case Study: BFCM Buildup
Section titled “Case Study: BFCM Buildup”Context
Section titled “Context”Black Friday/Cyber Monday (BFCM) is a multi-day shopping event:
- Black Friday: Friday after US Thanksgiving (late November)
- Cyber Monday: Monday following Black Friday (3 days later)
For weekly data (Monday-Sunday), these typically fall within the same week or span two weeks.
Data Structure
Section titled “Data Structure”Assumption: Weekly data starting on Monday
Example for 2024:
- Thanksgiving: Thursday, 28 November 2024
- Black Friday: Friday, 29 November 2024
- Cyber Monday: Monday, 2 December 2024
Week Definitions:
- Week -4: Monday, 28 October - Sunday, 3 November
- Week -3: Monday, 4 November - Sunday, 10 November
- Week -2: Monday, 11 November - Sunday, 17 November
- Week -1: Monday, 18 November - Sunday, 24 November
- Event Week: Monday, 25 November - Sunday, 1 December (contains both Black Friday and Cyber Monday)
Implementation
Section titled “Implementation”1. Create Dummy Variables
# BFCM dates for multiple yearsbfcm_events = { 2022: {'event_week': ('2022-11-21', '2022-11-27')}, 2023: {'event_week': ('2023-11-20', '2023-11-26')}, 2024: {'event_week': ('2024-11-25', '2024-12-01')},}
def create_bfcm_dummies(data, bfcm_events): """Create BFCM buildup and event dummies for multiple years."""
# Initialize all dummies to 0 for col in ['bfcm_week_minus_4', 'bfcm_week_minus_3', 'bfcm_week_minus_2', 'bfcm_week_minus_1', 'bfcm_event']: data[col] = 0
for year, dates in bfcm_events.items(): # Parse event week event_start = pd.to_datetime(dates['event_week'][0]) event_end = pd.to_datetime(dates['event_week'][1])
# Calculate buildup weeks (4 weeks before event_start) week_minus_1_start = event_start - pd.Timedelta(weeks=1) week_minus_2_start = event_start - pd.Timedelta(weeks=2) week_minus_3_start = event_start - pd.Timedelta(weeks=3) week_minus_4_start = event_start - pd.Timedelta(weeks=4)
# Set dummies data.loc[data['date'].between(week_minus_4_start, week_minus_4_start + pd.Timedelta(days=6)), 'bfcm_week_minus_4'] = 1 data.loc[data['date'].between(week_minus_3_start, week_minus_3_start + pd.Timedelta(days=6)), 'bfcm_week_minus_3'] = 1 data.loc[data['date'].between(week_minus_2_start, week_minus_2_start + pd.Timedelta(days=6)), 'bfcm_week_minus_2'] = 1 data.loc[data['date'].between(week_minus_1_start, week_minus_1_start + pd.Timedelta(days=6)), 'bfcm_week_minus_1'] = 1 data.loc[data['date'].between(event_start, event_end), 'bfcm_event'] = 1
return data
# Applydata = create_bfcm_dummies(data, bfcm_events)2. Verify Dummy Variables
# Check that only one dummy is active per weekdummy_cols = ['bfcm_week_minus_4', 'bfcm_week_minus_3', 'bfcm_week_minus_2', 'bfcm_week_minus_1', 'bfcm_event']
# Sum should be 0 or 1 for each row (never >1)data['dummy_sum'] = data[dummy_cols].sum(axis=1)assert data['dummy_sum'].max() <= 1, "Multiple dummies active in same week!"
# Print weeks with active dummiesfor col in dummy_cols: print(f"\n{col}:") print(data[data[col] == 1][['date', col]])3. Add to Config
Use the YAML configuration shown in {ref}Step 2 <step-2-yaml-configuration>.
4. Model Estimation
Run the model:
python runme.py --data data_with_bfcm.csv --config config_bfcm.yml5. Expected Results
Based on typical BFCM patterns, expect:
| Week | Expected Effect | Interpretation |
|---|---|---|
| Week -4 | β₁ ≈ -0.05 | Minimal delay effect |
| Week -3 | β₂ ≈ -0.10 | Consumers start holding off |
| Week -2 | β₃ ≈ -0.20 | Peak purchase delay (awareness of coming deals) |
| Week -1 | β₄ ≈ -0.15 | Continued delay, some early buying |
| Event Week | β₅ ≈ +1.5 to +2.5 | Large positive spike from event |
Visualization:
import matplotlib.pyplot as plt
weeks = ['Week -4', 'Week -3', 'Week -2', 'Week -1', 'Event']effects = [-0.05, -0.10, -0.20, -0.15, +1.80]
plt.figure(figsize=(10, 6))plt.axhline(y=0, color='black', linestyle='--', alpha=0.3)plt.plot(weeks, effects, marker='o', linewidth=2, markersize=8)plt.title('BFCM Buildup Effect Pattern', fontsize=14)plt.ylabel('Coefficient Estimate (β)', fontsize=12)plt.xlabel('Time Relative to BFCM', fontsize=12)plt.grid(alpha=0.3)plt.tight_layout()plt.savefig('bfcm_buildup_pattern.png')Post-Event Decay (Optional)
Section titled “Post-Event Decay (Optional)”You may also want to model post-event decay (hangover effect):
# Add post-event weeksdata['bfcm_week_plus_1'] = 0data['bfcm_week_plus_2'] = 0
# Set for weeks after BFCMdata.loc['2024-12-02':'2024-12-08', 'bfcm_week_plus_1'] = 1data.loc['2024-12-09':'2024-12-15', 'bfcm_week_plus_2'] = 1Expected post-event coefficients:
- Week +1: β₆ ≈ -0.10 (purchases fulfilled, reduced demand)
- Week +2: β₇ ≈ -0.05 (returning to baseline)
(alternative-approaches)=
Alternative Approaches
Section titled “Alternative Approaches”1. Step Function for Hypothesis Testing (Recommended for Simplicity)
Section titled “1. Step Function for Hypothesis Testing (Recommended for Simplicity)”Goal: Test if buildup effect exists (yes/no), not estimate its shape.
Approach: Model entire buildup period as a single uniform effect.
# Create single buildup variable (all buildup weeks = 1)data['bfcm_buildup'] = 0data['bfcm_event'] = 0
# Buildup period: 4 weeks before event (all weeks get same value)data.loc['2024-10-28':'2024-11-24', 'bfcm_buildup'] = 1 # Weeks -4 to -1data.loc['2024-11-25':'2024-12-01', 'bfcm_event'] = 1 # Event weekYAML Configuration:
extra_features_cols: - bfcm_buildup # Single variable covering all buildup weeks - bfcm_event
custom_priors: gamma_control: dist: TruncatedNormal kwargs: mu: 0.0 sigma: [0.5, 1.0] # Wider sigma for event (second in list) lower: -2.0 upper: 2.0Statistical Framework:
This is a restricted distributed lag model where all buildup weeks share the same coefficient:
y_t = β₀ + β₁·media_t + β_buildup·buildup_t + β_event·event_t + ε_t
where: buildup_t = 1 if week ∈ {-4, -3, -2, -1}, else 0Hypothesis Test:
- H₀: β_buildup = 0 (no buildup effect)
- H₁: β_buildup ≠ 0 (buildup effect exists)
Interpretation:
- β_buildup: Average effect across all buildup weeks
- If credible interval excludes 0 → buildup effect confirmed
- Magnitude = average weekly impact during buildup period
Example Results:
β_buildup: -0.15 [-0.25, -0.05] → Significant negative effect (purchase delay)β_event: +1.80 [+1.50, +2.10] → Large positive event effectInterpretation: On average, sales are 15% lower during the 4-week buildup period (consumers delay purchases), then spike 180% during the event week.
Pros:
- Parsimonious: Only 2 parameters (buildup + event)
- Statistical power: More power to detect effect existence (pooling data)
- Clear hypothesis test: Yes/no answer to “does buildup exist?”
- Simple interpretation: Single number for buildup magnitude
- Efficient: Works well with limited data
Cons:
- Assumes uniform effect: Cannot detect if delay accelerates near event
- Averages over pattern: May underestimate peak delay (e.g., if week -1 has stronger effect)
- Shape unknown: Cannot tell if β₁ = β₂ = β₃ = β₄ or β₁ < β₂ < β₃ < β₄
When to Use:
- ✅ Primary goal: Test if buildup effect exists (hypothesis testing)
- ✅ Quantify average buildup magnitude (not week-by-week pattern)
- ✅ Limited data (cannot afford 4+ parameters)
- ✅ Simplicity valued over granularity
- ✅ Hypothesis-driven rather than exploratory analysis
When NOT to Use:
- ❌ Need to estimate buildup shape (use multiple dummies instead)
- ❌ Want to detect acceleration pattern (e.g., week -1 > week -4)
- ❌ Sufficient data to estimate multiple coefficients
Validation:
This approach is statistically sound and aligned with econometric practice (Greene, 2003):
- It’s a restricted distributed lag model with equality constraint: β₁ = β₂ = β₃ = β₄ = β
- The restriction is testable via model comparison (ELPD)
- If assumption holds (uniform effect), it’s more efficient than unrestricted model
- If assumption fails, unrestricted model (multiple dummies) will fit better
Comparison to Multiple Dummies:
| Aspect | Step Function | Multiple Dummies |
|---|---|---|
| Parameters | 1 (β_buildup) | 4 (β₁, β₂, β₃, β₄) |
| Degrees of Freedom | Low (1 DOF) | High (4 DOF) |
| Statistical Power | High (pooled data) | Lower (split data) |
| Assumption | Uniform effect | No assumption |
| Use Case | Hypothesis testing | Pattern discovery |
| Interpretation | Average effect | Week-by-week effect |
2. Single Event Dummy (Baseline)
Section titled “2. Single Event Dummy (Baseline)”Simplest approach: Only model the event week itself, ignore buildup.
control: - col: bfcm_event # Only 1 dummy prior: dist: TruncatedNormal kwargs: {mu: 0.0, sigma: 1.0, lower: -2.0, upper: 5.0}Pros:
- Simple (1 parameter)
- Easy to interpret
Cons:
- Misses purchase delay effect in buildup weeks
- Biased estimates if buildup effect exists
When to use: As a baseline for model comparison.
3. Continuous Ramp Function
Section titled “3. Continuous Ramp Function”Approach: Use a predetermined functional form.
Linear Ramp:
# Create single variable with linear rampdata['bfcm_buildup'] = 0data.loc['2024-10-28':'2024-11-03', 'bfcm_buildup'] = -0.20 # Week -4data.loc['2024-11-04':'2024-11-10', 'bfcm_buildup'] = -0.40 # Week -3data.loc['2024-11-11':'2024-11-17', 'bfcm_buildup'] = -0.60 # Week -2data.loc['2024-11-18':'2024-11-24', 'bfcm_buildup'] = -0.80 # Week -1data.loc['2024-11-25':'2024-12-01', 'bfcm_buildup'] = +1.00 # Eventcontrol: - col: bfcm_buildup # Single parameter prior: dist: TruncatedNormal kwargs: {mu: 0.0, sigma: 1.0, lower: -2.0, upper: 5.0}Pros:
- Parsimonious (1 parameter)
- Smooth curve
Cons:
- Strong assumption about functional form
- Cannot test if linear relationship holds
When to use:
- Confirmatory analysis after discovering linear pattern with dummies
- Limited data (cannot afford multiple parameters)
4. Hierarchical Prior for Smoothness (Advanced)
Section titled “4. Hierarchical Prior for Smoothness (Advanced)”Approach: Encourage smooth buildup pattern without imposing rigid functional form.
# In PyMC model (pseudo-code)with pm.Model() as model: # Global BFCM effect beta_bfcm_global = pm.Normal('beta_bfcm_global', mu=0, sigma=1)
# Smooth deviations from global effect beta_bfcm_deviations = pm.GaussianRandomWalk( 'beta_bfcm_deviations', mu=0, sigma=0.1, # Small sigma enforces smoothness shape=5 # 5 time periods )
# Final coefficients beta_bfcm_final = beta_bfcm_global + beta_bfcm_deviationsPros:
- Flexible yet regularized
- Data can override smoothness prior if evidence is strong
Cons:
- Requires custom PyMC code (not supported in AMMM YAML config)
- More complex to implement and interpret
When to use: Advanced Bayesian modelling for complex event patterns.
(bayesian-perspective-purist-vs-pragmatic)=
Bayesian Perspective: Purist vs Pragmatic
Section titled “Bayesian Perspective: Purist vs Pragmatic”The Bayesian Purist’s View
Section titled “The Bayesian Purist’s View”A Bayesian purist would argue that the step function approach (single buildup variable) imposes a hard equality constraint (β₁ = β₂ = β₃ = β₄ = β), which is equivalent to a point mass prior with infinite certainty. This violates the core Bayesian principle of encoding uncertainty about everything.
The Critique:
“The step function says there’s zero probability that week -1 differs from week -4. If you truly believe all weeks have equal effect, you should encode this as a hierarchical prior with soft constraints, not a deterministic hard constraint. Your approach is frequentist hypothesis testing masquerading as Bayesian inference.”
Mathematical interpretation:
Step function constraint: β₁ = β₂ = β₃ = β₄ = β
Bayesian translation:P(β₁ = β₂ = β₃ = β₄) = 1.0 (infinite certainty)P(β₁ ≠ β₂) = 0.0 (impossible by construction)The Pragmatic Bayesian’s Defence
Section titled “The Pragmatic Bayesian’s Defence”However, a pragmatic Bayesian (in the tradition of Andrew Gelman) would counter:
“The step function is acceptable if it genuinely represents your prior belief. If you have domain knowledge that all buildup weeks should have similar effects, encoding this as a constraint is informative prior specification, not a violation of Bayesian principles. The key question is: Is this your actual prior belief, or just a computational shortcut?”
When the step function is Bayesian enough:
✅ If you genuinely believe (based on domain knowledge):
- All buildup weeks have similar psychological effects on consumers
- Consumer behaviour doesn’t change substantially week-to-week during buildup
- The distinction between week -4 and week -3 is measurement noise, not meaningful signal
Then encoding β₁ = β₂ = β₃ = β₄ is valid prior specification.
✅ Model comparison via ELPD is fundamentally Bayesian:
- Compare step function vs unrestricted model
- Let out-of-sample predictive performance decide
- This is Bayesian model selection, not frequentist hypothesis testing
Making Your Approach More Bayesian
Section titled “Making Your Approach More Bayesian”Option 1: Hierarchical Prior (Most Bayesian)
Section titled “Option 1: Hierarchical Prior (Most Bayesian)”Replace hard constraint with soft prior:
# PyMC pseudo-codewith pm.Model() as model: # Shared mean for all buildup weeks μ_buildup = pm.Normal('mu_buildup', mu=0, sigma=1)
# How much can individual weeks deviate? σ_weeks = pm.HalfNormal('sigma_weeks', sigma=0.1) # Small = similar
# Individual week effects (pooled towards μ_buildup) β_weeks = pm.Normal('beta_weeks', mu=μ_buildup, sigma=σ_weeks, shape=4) # β_weeks[0] = week -4, β_weeks[1] = week -3, etc.Bayesian interpretation:
- Prior: Weeks are similar (σ_weeks small) but can differ
- Posterior: If data shows divergence, it will override the prior
- Uncertainty: Full posterior distribution for each β_k
- Partial pooling: Weeks borrow strength from each other
Key difference from step function:
- Step function: β₁ = β₂ = β₃ = β₄ (hard constraint, 0% chance of difference)
- Hierarchical prior: β₁ ≈ β₂ ≈ β₃ ≈ β₄ (soft constraint, small but non-zero chance of difference)
Option 2: Informative Prior on Single Parameter
Section titled “Option 2: Informative Prior on Single Parameter”If you stick with the step function, make the prior informative:
control: - col: bfcm_buildup prior: dist: Normal # Not TruncatedNormal kwargs: mu: -0.15 # Domain knowledge: expect ~15% delay sigma: 0.10 # Moderate uncertainty (allow data to override)This encodes:
- Prior belief: β_buildup ≈ -0.15 (based on previous research/industry knowledge)
- Uncertainty: Could reasonably be anywhere from -0.35 to +0.05 (95% prior interval)
- Bayesian updating: Data shifts posterior away from prior if evidence is strong
Contrast with weakly informative prior:
# Current approach (weakly informative)control: - col: bfcm_buildup prior: dist: TruncatedNormal kwargs: mu: 0.0 # No prior belief (neutral) sigma: 0.5 # Wide uncertainty lower: -2.0 upper: 2.0This is maximally agnostic - doesn’t encode domain knowledge, so it’s less Bayesian in spirit.
Option 3: Gaussian Random Walk (Advanced)
Section titled “Option 3: Gaussian Random Walk (Advanced)”For smooth but flexible buildup patterns:
# PyMC codewith pm.Model() as model: # Smooth evolution of buildup effect β_buildup = pm.GaussianRandomWalk( 'beta_buildup', mu=0, sigma=0.05, # Small sigma = smooth transitions shape=4 # 4 buildup weeks )Properties:
- Smoothness: Adjacent weeks are constrained to be similar (β_k ≈ β_k+1)
- Flexibility: Can still capture acceleration patterns if data demands
- Bayesian: Encodes belief in smooth temporal evolution without hard constraints
The Bayesian Hierarchy of Virtue
Section titled “The Bayesian Hierarchy of Virtue”From most Bayesian to least Bayesian:
-
Gaussian Random Walk (Most Bayesian)
- Smooth temporal evolution
- Flexible (data can override)
- Full posterior uncertainty
- Implementation: Requires custom PyMC code
-
Hierarchical Prior with Partial Pooling
- Soft constraint on similarity
- Learns shared mean from data
- Weeks can differ if evidence is strong
- Implementation: Requires custom PyMC code
-
Informative Prior on Single Parameter (Pragmatically Bayesian)
- Encodes domain knowledge
- Simple and interpretable
- Appropriate for hypothesis testing
- Implementation: Standard AMMM YAML config
-
Weakly Informative Prior (Frequentist-flavored Bayesian)
- Hard constraint (step function)
- Vague prior (mu=0, wide sigma)
- Resembles frequentist null hypothesis
- Implementation: Standard AMMM YAML config
Practical Recommendation
Section titled “Practical Recommendation”For hypothesis testing use cases, the pragmatic Bayesian compromise:
- Use step function for simplicity (as validated in Section 5.1)
- Make the prior informative using domain knowledge:
control: - col: bfcm_buildup prior: dist: Normal kwargs: mu: -0.10 # Expect negative (purchase delay) sigma: 0.15 # Moderate uncertainty
- col: bfcm_event prior: dist: Normal kwargs: mu: +1.00 # Expect positive (event spike) sigma: 0.50 # Allow larger effects- Report full posterior, not just significance:
Example results:β_buildup: -0.15 (95% credible interval: [-0.25, -0.05])P(β_buildup < 0 | data) = 99.9%
Interpretation:- 99.9% probability that buildup causes purchase delay- Expected magnitude: 15% decrease in sales during buildup- Could be as low as 5% or as high as 25% (95% credible)This is Bayesian enough for most applied work while maintaining analytical simplicity.
Frequentist vs Bayesian Mindset
Section titled “Frequentist vs Bayesian Mindset”Hypothesis testing mindset (frequentist):
Goal: Test H₀: β = 0 vs H₁: β ≠ 0Result: Reject H₀ at p < 0.05Interpretation: Buildup effect is "statistically significant"Bayesian mindset:
Goal: Estimate P(β | data) - full posterior distributionResult: β ~ Normal(-0.15, 0.05²)Interpretation: - Best estimate: -15% effect - Uncertainty: ±5% (standard error) - Probability of delay: P(β < 0) = 99.9% - Probability of large delay: P(β < -0.10) = 84%Key differences:
- Frequentist: Binary decision (reject/fail to reject)
- Bayesian: Probabilistic statements about parameters
- Frequentist: p-value (probability of data given H₀)
- Bayesian: Posterior probability (probability of hypothesis given data)
Summary: Which Approach to Choose?
Section titled “Summary: Which Approach to Choose?”| Your Goal | Recommended Approach | Bayesian Purity |
|---|---|---|
| Hypothesis testing (does effect exist?) | Step function + informative prior | Pragmatically Bayesian ✓ |
| Pattern discovery (what is the shape?) | Multiple dummies (unrestricted) | Moderately Bayesian ✓✓ |
| Smooth estimation with flexibility | Hierarchical prior or random walk | Purely Bayesian ✓✓✓ |
| Maximum simplicity | Step function + weakly informative prior | Frequentist-flavored |
Bottom line: All approaches are valid. The choice depends on:
- Your research question (hypothesis testing vs pattern discovery)
- Available domain knowledge (strong prior beliefs vs agnostic)
- Computational constraints (simple YAML config vs custom PyMC code)
- Philosophical preference (pragmatic vs purist Bayesian)
(model-diagnostics)=
Model Diagnostics
Section titled “Model Diagnostics”1. Posterior Predictive Checks
Section titled “1. Posterior Predictive Checks”Goal: Verify the model accurately captures the buildup pattern in actual data.
import arviz as az
# Generate posterior predictive sampleswith model: ppc = pm.sample_posterior_predictive(trace)
# Plot observed vs predictedaz.plot_ppc(az.from_pymc3(posterior_predictive=ppc, model=model))What to look for:
- Observed data (blue) should fall within posterior predictive distribution (orange)
- Check buildup weeks specifically - does model capture the dip?
2. ELPD Comparison
Section titled “2. ELPD Comparison”Goal: Compare models with/without buildup variables.
AMMM computes Expected Log Pointwise Predictive Density (ELPD) and writes
artefacts under 50_diagnostics/ (for example ELPD.txt and
ELPD_summary.csv).
Procedure:
- Run baseline model (no buildup variables)
- Run model with buildup variables
- Compare ELPD scores
# Baselinepython runme.py --config config_no_buildup.yml# ELPD: -1250.4
# With builduppython runme.py --config config_with_buildup.yml# ELPD: -1205.8
# Difference: 44.6 (buildup model is better)Rule of Thumb:
- ΔELPD > 10: Strong evidence for buildup model
- ΔELPD > 4: Moderate evidence
- ΔELPD < 4: Weak evidence
3. Multicollinearity (VIF) Checks
Section titled “3. Multicollinearity (VIF) Checks”Goal: Ensure buildup dummies don’t create collinearity issues.
AMMM computes Variance Inflation Factor (VIF) in pre-diagnostics:
python runme.py --config config_with_buildup.yml# Check results/10_pre_diagnostics/vif_summary.csvWhat to check:
variable,VIFmedia_google,2.1media_facebook,1.8bfcm_week_minus_4,1.5bfcm_week_minus_3,1.6bfcm_week_minus_2,1.7bfcm_week_minus_1,1.8bfcm_event,2.3Rule of Thumb:
- VIF < 5: No collinearity concern
- VIF 5-10: Moderate collinearity (monitor)
- VIF > 10: High collinearity (problematic)
Common Issue: If you have multiple overlapping events (e.g., BFCM + Christmas within 4 weeks), VIF may be high.
Solution:
- Aggregate overlapping events into single periods
- Use hierarchical priors to pool similar events
4. Coefficient Stability
Section titled “4. Coefficient Stability”Goal: Verify buildup coefficients are stable across model specifications.
Procedure:
- Add buildup variables one at a time
- Check if coefficients change drastically
# Model A: Only week -1 and event# β_week_minus_1 = -0.18, β_event = +1.75
# Model B: All buildup weeks# β_week_minus_1 = -0.15, β_event = +1.80
# Stable? Yes (coefficients changed <20%)Unstable coefficients suggest collinearity or overfitting.
(common-pitfalls)=
Common Pitfalls
Section titled “Common Pitfalls”1. Overfitting with Too Many Event Variables
Section titled “1. Overfitting with Too Many Event Variables”Problem: If you have 52 weeks of data and 20 event dummies, you’re fitting too many parameters.
Symptom:
- Wide credible intervals
- Unstable coefficients
- Poor out-of-sample predictive performance
Solution:
- Group similar events (e.g., all “seasonal sales” into one category)
- Use hierarchical priors to pool events
- Increase data collection (more years)
2. Confounding with Media Spend Patterns
Section titled “2. Confounding with Media Spend Patterns”Problem: If you always increase Google Ads during BFCM buildup, the model can’t separate Google effect from buildup effect.
Symptom:
- Buildup coefficient is positive (opposite of expected)
- Media coefficient for Google is lower than other models
Solution:
- Check correlation between buildup dummies and media spend
- If correlation > 0.7, consider interaction terms or reduce media spend variation during events
Diagnostic:
# Check correlationcorr_matrix = data[['bfcm_week_minus_1', 'media_google']].corr()print(corr_matrix)
# If corr > 0.7:print("WARNING: High correlation between buildup and Google spend")3. Prophet vs PyMC Control Variables
Section titled “3. Prophet vs PyMC Control Variables”Problem: Buildup dummies in PyMC config do not automatically transfer to Prophet (used for multi-period optimisation forecasting).
Current State of Prophet Integration:
AMMM uses Prophet for seasonality decomposition and forecasting, but custom event handling is limited:
For model fitting (PyMC):
- Use
extra_features_colsin YAML config (as shown in this guide) - Event variables (BFCM buildup, etc.) are modeled as PyMC control variables
For forecasting/optimisation (Prophet):
- Prophet uses built-in country holidays via config:
prophet:include_holidays: trueholiday_country: "UK" # or your country
- Custom holidays from
holidays.xlsxare currently not passed to Prophet - The file is read by AMMM but not used to add custom events to Prophet forecasts
Impact:
When using multi-period optimisation (--multiperiod):
- Prophet forecasts will not account for your custom BFCM buildup effects
- Only country holidays (e.g., UK Bank Holidays) are included in forecasts
- Event buildup patterns learned in PyMC are not propagated to Prophet
Workaround:
Model custom events (like BFCM) only as PyMC control variables for now:
- This captures the effect in model fitting and attribution
- For multi-period optimisation, be aware that Prophet forecasts will not include these patterns
- Consider this when interpreting optimisation results for periods with known events
Future Enhancement:
If custom Prophet holiday support is needed, the code in src/prepro/seas.py::prophet_decomp() would need to be extended to:
- Accept the
holidays.xlsxDataFrame - Pass it to Prophet via
Prophet(holidays=dt_holidays) - Support
lower_window/upper_windowfor event buildup patterns
Recommendation:
For most use cases, the current approach is sufficient:
- PyMC captures event effects in historical data (what happened)
- Prophet captures seasonality and trend (business-as-usual patterns)
- If you need event-aware optimisation, consider scenario-based planning instead
4. Forgetting to Update for New Years
Section titled “4. Forgetting to Update for New Years”Problem: Hard-coded dates in your data preparation script become outdated.
Solution: Create a centralized events configuration file.
Example: events_calendar.json
{ "BFCM": { "2022": {"event_week": ["2022-11-21", "2022-11-27"]}, "2023": {"event_week": ["2023-11-20", "2023-11-26"]}, "2024": {"event_week": ["2024-11-25", "2024-12-01"]}, "2025": {"event_week": ["2025-11-24", "2025-11-30"]} }, "Christmas": { "2022": {"event_week": ["2022-12-19", "2022-12-25"]}, "2023": {"event_week": ["2023-12-18", "2023-12-24"]}, "2024": {"event_week": ["2024-12-23", "2024-12-29"]} }}Load and apply programmatically:
import json
with open('events_calendar.json', 'r') as f: events = json.load(f)
data = create_bfcm_dummies(data, events['BFCM'])data = create_christmas_dummies(data, events['Christmas'])5. Mixing Weekly and Daily Granularity
Section titled “5. Mixing Weekly and Daily Granularity”Problem: Your data is weekly but BFCM spans Friday-Monday (4 days).
Solution:
- Option A: Keep weekly data, treat entire week as “event week” (recommended for simplicity)
- Option B: Switch to daily data for finer granularity (but increases model complexity)
For most use cases, weekly granularity is sufficient since:
- MMM is about medium-term trends (weeks/months), not daily spikes
- Adstock effects smooth out daily fluctuations
- Weekly data has better signal-to-noise ratio
(references)=
References
Section titled “References”Academic Literature
Section titled “Academic Literature”-
Almon, S. (1965). “The Distributed Lag Between Capital Appropriations and Expenditures.” Econometrica, 33(1), 178-196.
- Foundational paper on distributed lag models
-
Greene, W. H. (2003). Econometric Analysis (5th ed.). Prentice Hall.
- Chapter 17: Models with Lagged Variables
-
Jin, Y., Wang, Y., Sun, Y., Chan, D., & Koehler, J. (2017). “Bayesian Methods for Media Mix Modeling with Carryover and Shape Effects.” Google Research.
- Modern MMM with event effects
-
Box, G. E. P., Jenkins, G. M., Reinsel, G. C., & Ljung, G. M. (2015). Time Series Analysis: Forecasting and Control (5th ed.). Wiley.
- Chapter on intervention analysis (related to event modelling)
AMMM Documentation
Section titled “AMMM Documentation”- Guide: Configuration (see
extra_features_cols/ control variables) - Explanation: Model Components (baseline + controls)
- Guide: Multi-Period Optimisation
- Explanation: Methodology (Prophet time features + MMM regression)
Online Resources
Section titled “Online Resources”- PyMC Documentation: Distributed Lag Models
- Facebook Prophet: Holiday Effects
- Cross Validated: Event Study Methodology
Appendix: Full Working Example
Section titled “Appendix: Full Working Example”Complete Python Script
Section titled “Complete Python Script”#!/usr/bin/env python3"""Create BFCM buildup dummies for AMMM input data.
Usage: python create_bfcm_dummies.py --input raw_data.csv --output data_with_bfcm.csv"""
import pandas as pdimport numpy as npimport argparsefrom datetime import timedelta
# BFCM event dates (Monday of event week)BFCM_EVENTS = { 2022: pd.Timestamp('2022-11-21'), 2023: pd.Timestamp('2023-11-20'), 2024: pd.Timestamp('2024-11-25'), 2025: pd.Timestamp('2025-11-24'),}
def create_bfcm_dummies(data: pd.DataFrame, date_col: str = 'date') -> pd.DataFrame: """ Create BFCM buildup and event dummy variables.
Args: data: DataFrame with date column date_col: Name of date column
Returns: DataFrame with added dummy columns """ # Ensure date column is datetime data[date_col] = pd.to_datetime(data[date_col])
# Initialize dummy columns dummy_cols = [ 'bfcm_week_minus_4', 'bfcm_week_minus_3', 'bfcm_week_minus_2', 'bfcm_week_minus_1', 'bfcm_event', ] for col in dummy_cols: data[col] = 0
# Create dummies for each year for year, event_start in BFCM_EVENTS.items(): # Calculate week boundaries (7-day periods starting on Monday) weeks = { 'bfcm_week_minus_4': (event_start - timedelta(weeks=4), event_start - timedelta(weeks=4) + timedelta(days=6)), 'bfcm_week_minus_3': (event_start - timedelta(weeks=3), event_start - timedelta(weeks=3) + timedelta(days=6)), 'bfcm_week_minus_2': (event_start - timedelta(weeks=2), event_start - timedelta(weeks=2) + timedelta(days=6)), 'bfcm_week_minus_1': (event_start - timedelta(weeks=1), event_start - timedelta(weeks=1) + timedelta(days=6)), 'bfcm_event': (event_start, event_start + timedelta(days=6)), }
# Set dummies for dummy_col, (start, end) in weeks.items(): mask = data[date_col].between(start, end) data.loc[mask, dummy_col] = 1
if mask.sum() > 0: print(f"{year} {dummy_col}: {start.date()} to {end.date()} ({mask.sum()} rows)")
# Validation: Ensure no overlaps dummy_sum = data[dummy_cols].sum(axis=1) if (dummy_sum > 1).any(): print("WARNING: Multiple dummies active in same row!") print(data[dummy_sum > 1][['date'] + dummy_cols])
return data
def main(): parser = argparse.ArgumentParser(description='Create BFCM buildup dummies') parser.add_argument('--input', required=True, help='Input CSV file') parser.add_argument('--output', required=True, help='Output CSV file') parser.add_argument('--date-col', default='date', help='Name of date column') args = parser.parse_args()
# Load data print(f"Loading data from {args.input}...") data = pd.read_csv(args.input) print(f"Loaded {len(data)} rows")
# Create dummies print("\nCreating BFCM buildup dummies...") data = create_bfcm_dummies(data, date_col=args.date_col)
# Save print(f"\nSaving to {args.output}...") data.to_csv(args.output, index=False) print("Done!")
# Summary dummy_cols = ['bfcm_week_minus_4', 'bfcm_week_minus_3', 'bfcm_week_minus_2', 'bfcm_week_minus_1', 'bfcm_event'] print("\nSummary:") for col in dummy_cols: n_active = (data[col] == 1).sum() print(f" {col}: {n_active} weeks")
if __name__ == '__main__': main()Complete YAML Config
Section titled “Complete YAML Config”# AMMM Configuration with BFCM Buildup Effects# Data configurationdate_col: datekpi_col: revenueraw_data_granularity: weekly
# Media channels (simple list - priors set globally below)media: - spend_col: google_spend - spend_col: facebook_spend
# Control variables (BFCM buildup + any baseline controls)extra_features_cols: - temperature # Example baseline control - bfcm_week_minus_4 - bfcm_week_minus_3 - bfcm_week_minus_2 - bfcm_week_minus_1 - bfcm_event
# Global priors for all model parameterscustom_priors: # Media channel coefficients beta_channel: dist: Normal kwargs: mu: 0.0 sigma: 2.0
# Adstock decay parameter alpha: dist: Beta kwargs: alpha: 2 beta: 2
# Saturation lambda parameter lam: dist: Gamma kwargs: alpha: 2 beta: 0.0001
# Control variable coefficients (uniform for all, or vectorized) gamma_control: dist: TruncatedNormal kwargs: mu: 0.0 # Option 1: Uniform prior for all controls sigma: 0.5 # Option 2: Vectorized sigma (must match extra_features_cols order) # sigma: [0.5, 0.5, 0.5, 0.5, 0.5, 1.0] # Wider for event lower: -2.0 upper: 2.0
# Sampling configurationdraws: 2000tune: 1000chains: 4target_accept: 0.95Key Changes from Old Syntax:
- Media: Simple list of
spend_colonly (no per-channel priors) - Controls: Use
extra_features_colslist (notcontrol:blocks) - Priors: All priors in
custom_priorssection - Sampler: Use
draws,tune,chains(notmcmc_*)
End of Guide
For questions or issues, consult the AMMM GitHub repository or contact the development team.