Skip to content

Multi-Period Budget Optimization - User Guide

Multi-period budget optimization extends AMMM’s budget allocation capabilities to plan across multiple time periods (e.g., 13 weeks, 6 months) rather than optimizing for a single aggregated period. This enables:

  • Time-varying budget allocation - Allocate different budgets to different periods based on expected effectiveness
  • Seasonality integration - Automatically adjust for seasonal variations in channel effectiveness
  • Realistic planning - Align with business planning cycles (quarterly, annual)
  • Constraint flexibility - Set per-period budget limits while optimizing total allocation

How Multi-Period Optimisation Works (Implementation)

Section titled “How Multi-Period Optimisation Works (Implementation)”

Multi-period optimisation allocates budgets S_{p,i} across periods p=1..P and channels i=1..n to maximise total expected contribution:

\max_{\{S_{p,i}\}} \sum_{p=1}^{P}\sum_{i=1}^{n} w_{p,i}\,C_i(S_{p,i})

Where:

  • C_i(·) is a per-channel response curve fitted from the MMM (sigmoid saturation by default),
  • w_{p,i} is an optional seasonality multiplier (defaults to 1 when seasonality is disabled).

The optimiser enforces:

  • Total budget across all periods and channels: Σ_{p,i} S_{p,i} = B
  • Per-channel bounds per period: S_{p,i}^{min} ≤ S_{p,i} ≤ S_{p,i}^{max}
  • Optional per-period budget limits: B_p^{min} ≤ Σ_i S_{p,i} ≤ B_p^{max}
  • Optional ramp constraints between consecutive periods (absolute and/or percentage)

Solver selection (sequential vs simultaneous)

Section titled “Solver selection (sequential vs simultaneous)”

AMMM automatically chooses a more robust strategy for larger problems:

  • Sequential optimisation (default for larger problems): optimises one period at a time (greedy) and naturally enforces ramp constraints against the previous period. This is much more stable for larger horizons.
  • Simultaneous optimisation (smaller problems): solves all P×n variables at once using SciPy’s constrained optimiser (SLSQP by default; may switch to trust-constr for very large problems).

Adstock-aware multi-period optimisation (advanced)

Section titled “Adstock-aware multi-period optimisation (advanced)”

The codebase includes an adstock-aware objective (carryover state dynamics), but it is not exposed via runme.py multi-period mode today. Treat it as an advanced/experimental option requiring careful constraint tuning.

Use multi-period optimization when:

  • You need to plan budgets for multiple weeks/months ahead
  • Your business has significant seasonality (e.g., retail, travel)
  • You want to optimize across a planning horizon (e.g., Q1, full year)
  • You need to respect time-varying budget constraints

Use single-period optimization when:

  • You’re optimizing for a single aggregated time period
  • You want quick “what-if” scenarios with fixed total budgets
  • Seasonality is not a major concern

Basic Multi-Period Optimization (13 weeks)

Section titled “Basic Multi-Period Optimization (13 weeks)”
import src as ammm
from src.driver import MMMBaseDriverV2
# Fit model (standard pipeline)
driver = MMMBaseDriverV2('config.yml', 'data.csv', 'holidays.xlsx', 'results')
driver.main()
# Run 13-week multi-period optimization
ammm.optimize_marketing_budget(
model=driver.model,
data=driver.processed_data,
config=driver.config,
results_dir=driver.results_dir,
n_time_periods=13, # 13 weeks
multiperiod_mode=True, # Enable multi-period
use_seasonality=True, # Apply seasonal adjustments
frequency='W' # Weekly planning
)

Output:

  • CSV file: results/multiperiod_optimization_results.csv
  • Console summary with total budget, contribution, and ROI
ammm.optimize_marketing_budget(
model=driver.model,
data=driver.processed_data,
config=driver.config,
results_dir='results',
n_time_periods=13,
multiperiod_mode=True,
use_seasonality=True,
frequency='W',
start_date='2025-01-06' # Q1 start
)
ammm.optimize_marketing_budget(
model=driver.model,
data=driver.processed_data,
config=driver.config,
results_dir='results',
n_time_periods=13,
total_budget=12_500_000, # £12.5M for Q1
multiperiod_mode=True,
use_seasonality=True,
frequency='W',
start_date='2025-01-06'
)
ammm.optimize_marketing_budget(
model=driver.model,
data=driver.processed_data,
config=driver.config,
results_dir='results',
n_time_periods=12,
multiperiod_mode=True,
use_seasonality=True,
frequency='M', # Monthly frequency
start_date='2025-01-01'
)

Example 4: With Per-Period Budget Constraints

Section titled “Example 4: With Per-Period Budget Constraints”
# Direct call to multi-period function for advanced options
from src.driver.opt import optimize_multiperiod_budget
results_df = optimize_multiperiod_budget(
model=driver.model,
data=driver.processed_data,
config=driver.config,
results_dir='results',
n_periods=13,
total_budget=12_500_000,
period_budget_limits=(800_000, 1_200_000), # £800K-£1.2M per week
use_seasonality=True,
frequency='W',
start_date='2025-01-06'
)

Main Function: optimize_marketing_budget()

Section titled “Main Function: optimize_marketing_budget()”
ParameterTypeDefaultDescription
modelBaseMMMRequiredFitted MMM model
dataDataFrameRequiredHistorical data
configDictRequiredConfiguration dictionary
results_dirstrRequiredOutput directory
n_time_periodsint1Number of periods to optimize
multiperiod_modeboolFalseEnable multi-period optimization
use_seasonalityboolFalseApply seasonal adjustments
frequencystr’M’Time frequency (‘W’, ‘M’, ‘D’)
start_datestrNonePlanning start date (YYYY-MM-DD)
total_budgetfloatNoneTotal budget (auto-calculated if None)

Advanced Function: optimize_multiperiod_budget()

Section titled “Advanced Function: optimize_multiperiod_budget()”

Additional parameters for fine-grained control:

ParameterTypeDefaultDescription
period_budget_limitsTuple[float, float]None(min, max) budget per period
lower_bound_pctfloat0.20% below mean for channel lower bounds
upper_bound_pctfloat0.20% above mean for channel upper bounds

CSV File: multiperiod_optimization_results.csv

Section titled “CSV File: multiperiod_optimization_results.csv”
ColumnDescription
periodPeriod index (0-indexed)
period_dateActual date for period start
channelChannel name (or ‘TOTAL’)
budgetAllocated budget for this period/channel
contributionExpected contribution (with seasonal adjustment)
seasonal_multiplierSeasonal effectiveness multiplier (1.0 = baseline)
roiReturn on investment for this period/channel
period,period_date,channel,budget,contribution,seasonal_multiplier,roi
0,2025-01-06,google,12500,60000,0.92,4.8
0,2025-01-06,facebook,28000,420000,0.88,15.0
0,2025-01-06,tv,95000,550000,1.15,5.8
0,2025-01-06,TOTAL,965000,1306000,0.98,1.35
1,2025-01-13,google,13200,63800,0.94,4.8
...
Multi-Period Optimization Summary (13 periods)
Total Budget: 12,500,000.00
Total Contribution: 95,750,000.00
Overall ROI: 7.66×

When you run multi-period optimization with use_seasonality=True, AMMM generates several visualizations to help you understand the results. These are saved in results/png/:

Section titled “Budget vs Contribution Trends (multiperiod_budget_vs_contribution.png)”

What you’re looking at:

This graph shows two metrics over time:

  • Blue bars (Budget): The optimal recommended budget for each specific period
  • Orange line (Contribution): The expected contribution (revenue/conversions) generated in each period

Critical understanding:

The “Budget” shown is NOT your historical or average spending. This is the output of the optimization algorithm - it represents the model’s recommendation for how to allocate your total budget across time to maximize total contribution.

Key features:

  1. Time-Varying Allocation: The budget is not flat across periods. The optimizer allocates more to periods where it expects higher return on investment (ROI). For example, if you see higher budget in periods 4-6, that’s when the model predicts the best effectiveness.

  2. Seasonal Response: The allocation pattern reflects Prophet’s seasonal effectiveness multipliers. Periods with higher seasonal multipliers (e.g., holiday weeks) receive more budget because each £ spent generates more contribution.

  3. Total Per Period: Each bar represents the total recommended marketing spend across all channels for that single period (e.g., week 0, week 1, etc.). This is the sum of all channel budgets for that time period.

  4. Contribution Follows Budget: You’ll typically see the contribution line (orange) peak around the same periods as the budget bars. This shows the optimization is working - allocating more budget where it generates more output.

Example interpretation:

If you see:

  • Period 5: £1.2M budget → £8.5M contribution
  • Period 0: £800K budget → £5.2M contribution

This means:

  • The optimizer recommends spending 50% more in Period 5 (£1.2M vs £800K)
  • Because Period 5 has higher effectiveness (likely higher seasonal multiplier)
  • Result: Period 5 generates 63% more contribution despite only 50% more spend
  • This is the optimization at work - allocating budget to high-ROI periods

Seasonal Patterns (multiperiod_seasonal_patterns.png):

  • Shows how seasonal multipliers vary over time for each channel
  • Multipliers >1.0 indicate periods of above-average effectiveness
  • Helps explain why budget allocation varies across periods

Budget Allocation Heatmap (multiperiod_budget_heatmap.png):

  • Channel-by-period heatmap showing recommended spend
  • Identify which channels get more budget during specific periods
  • Useful for spotting channel-specific seasonal patterns

Total Budget: 12,500,000.00 Total Contribution: 95,750,000.00 Overall ROI: 7.66×

## Prophet Seasonality Integration
**Period Comparison (`multiperiod_period_comparison.png`):**
- Side-by-side comparison of budget, contribution, and ROI by period
- Quickly identify the most efficient periods
**Contribution Over Time (`multiperiod_contribution_over_time.png`):**
- Stacked area chart showing each channel's contribution by period
- See which channels dominate during different time periods
**Using these visualizations:**
1. **Validate the optimization** - Does the budget allocation make business sense given your seasonality?
2. **Identify peak periods** - When should you concentrate spend?
3. **Channel timing** - Which channels work best in which periods?
4. **Explain to stakeholders** - These visuals make the \"why\" behind budget allocation clear
## Prophet Seasonality Integration
============================================================
Total Budget: 12,500,000.00
Total Contribution: 95,750,000.00
Overall ROI: 7.66×

Multi-period optimization uses Prophet’s seasonal decomposition from your fitted MMM model to adjust channel effectiveness over time.

Seasonal Components Used:

  • Yearly seasonality - Annual patterns (e.g., holiday peaks, summer lulls)
  • Weekly seasonality - Day-of-week patterns (e.g., weekend vs weekday)
  • Both combined - Additive effect of yearly + weekly

Example:

  • Week of December 15 (pre-Christmas Friday):
    • Yearly: +0.5 (holiday shopping season)
    • Weekly: +0.2 (Friday/weekend effect)
    • Combined: +0.7
    • Multiplier: ~1.07 (7% effectiveness boost)

Configuring Prophet Seasonality in Your Model

Section titled “Configuring Prophet Seasonality in Your Model”

Prophet seasonality is configured when fitting your MMM model. Check your model config:

# In your config.yml
prophet:
yearly_seasonality: true # Enable yearly patterns
weekly_seasonality: true # Enable weekly patterns
daily_seasonality: false # Usually false for weekly/monthly data
# Optional: Custom seasonality
seasonalities:
- name: 'monthly'
period: 30.5
fourier_order: 5
  • 1.0 - Baseline effectiveness (no seasonal adjustment)
  • >1.0 - Channel more effective (e.g., 1.08 = 8% boost)
  • <1.0 - Channel less effective (e.g., 0.92 = 8% reduction)

Example: If Google Ads normally generates £5 contribution per £1 spend:

  • Week with multiplier 1.08: £5.40 contribution per £1 spend
  • Week with multiplier 0.92: £4.60 contribution per £1 spend

Inspect what seasonality your model has:

# After model fitting
prophet_model = driver.model.prophet_model
forecast = prophet_model.predict(prophet_model.history)
print(forecast.columns)
# Output: ['ds', 'trend', 'yhat', 'yearly', 'weekly', 'holidays', ...]
# Plot seasonal components
prophet_model.plot_components(forecast)

Multi-period optimization with seasonality is particularly powerful for planning around major retail events like Black Friday, Christmas, Prime Day, or other promotional periods. This section explains how AMMM handles holiday events and provides guidance based on marketing science research.

Holiday effects appear in Prophet’s holidays component when:

  1. Holidays file is configured - Your holidays.xlsx contains the event dates
  2. Events fall within planning window - The holiday date is within your optimization horizon
  3. Historical data shows patterns - Prophet learns the effect from your training data

Example:

Planning window: 13 weeks starting Oct 17, 2025
Black Friday: Nov 28, 2025 (6 weeks ahead)
Result: Black Friday IS captured (within window)
Planning window: 13 weeks starting Oct 17, 2025
Christmas: Dec 25, 2025 (10 weeks ahead)
Result: Christmas IS captured (within window)
Planning window: 13 weeks starting Oct 17, 2025
Easter 2026: April 20, 2026 (26 weeks ahead)
Result: Easter NOT captured (beyond window) ⚠️

To include more holidays: Extend your planning window

# 26 weeks captures Q4 holidays + Q1
n_time_periods=26

Add events to your holidays.xlsx file:

ds,holiday,country,year
2024-11-29,Black_Friday,US,2024
2025-11-28,Black_Friday,US,2025
2026-11-27,Black_Friday,US,2026
2024-12-25,Christmas,US,2024
2025-12-25,Christmas,US,2025

Prophet will learn the average effect of each holiday from your historical data and project it forward.

Verify Prophet has captured your holidays:

# After model fitting
prophet_model = driver.model.prophet_model
forecast = prophet_model.predict(prophet_model.history)
# Check if holidays component exists and has variation
if 'holidays' in forecast.columns:
print(f"Holiday effect range: {forecast['holidays'].min():.2f} to {forecast['holidays'].max():.2f}")
# Show which dates have holiday effects
holiday_dates = forecast[forecast['holidays'] != 0][['ds', 'holidays']]
print(holiday_dates)

The Black Friday Planning Problem: Buildup vs Event Allocation

Section titled “The Black Friday Planning Problem: Buildup vs Event Allocation”

The Strategic Question: Should you allocate more budget to the weeks before Black Friday or during Black Friday week itself?

The Short Answer: Marketing science suggests weighted toward the buildup (2-3 weeks before), but the optimal split depends on your brand. Let your MMM discover the answer empirically.

Principle: Advertising has lagged effects. Spend in weeks 1-3 before Black Friday continues to influence purchases during the event.

Research:

  • Naik, Mantrala & Sawyer (1998) - “Planning Media Schedules in the Presence of Dynamic Advertising Quality”

    • Pulsing strategies (concentrated bursts before events) outperform continuous spending
    • Pre-event concentration builds awareness that converts during the event
  • Sethuraman, Tellis & Briesch (2011) - Meta-analysis of advertising elasticity

    • Advertising effects peak 0-1 weeks after exposure but persist for several weeks
    • Carryover effects mean early spending has compounding impact

Implication for AMMM: If your MMM shows strong carryover parameters (high adstock), the optimization will naturally shift budget earlier to capture these compounding effects.

Consumer behaviour research shows:

3-4 weeks before Black Friday:

  • Research phase - Consumers browse deals, create wish lists
  • Ad effectiveness: Brand awareness advertising has high ROI
  • Channel focus: Upper-funnel (display, video, social brand)

1-2 weeks before:

  • Consideration phase - Comparing options, checking reviews
  • Ad effectiveness: Persuasion advertising critical
  • Channel focus: Mid-funnel (search, comparison shopping, retargeting)

Black Friday week:

  • Conversion phase - Executing planned purchases
  • Ad effectiveness: Reminder advertising, but decisions largely made
  • Channel focus: Lower-funnel (branded search, remarketing)

Research:

  • Google/Ipsos (2019) - “Black Friday Shopping behaviour”
    • 72% of Black Friday shoppers research 2+ weeks in advance
    • 54% create shopping lists 1-2 weeks before
    • Only 28% make spontaneous same-day decisions

Implication: The majority of purchasing decisions are made before Black Friday, suggesting buildup spending is critical.

During Black Friday itself:

  • Extreme competition for attention (every brand advertising heavily)
  • Diminishing returns kick in faster due to saturation
  • Cost inflation (higher CPMs/CPCs during peak periods)

Research:

  • Bruce, Foutz & Kolsarici (2012) - “Dynamic Effectiveness of Advertising and Word of Mouth in Sequential Distribution of Short Lifecycle Products”, Journal of Marketing Research
    • Advertising effectiveness varies significantly based on competitive intensity
    • Early timing in product lifecycle (or event window) can establish stronger market position
    • Word-of-mouth effects amplify early advertising investments

Implication: Your advertising may face less competition and achieve stronger effectiveness in the buildup weeks compared to the highly saturated Black Friday period itself.

Based on marketing literature and industry practice:

Week -4 to -3: 15-20% of total budget (awareness building)
Week -2 to -1: 30-35% of total budget (peak consideration)
Week 0 (BF): 25-30% of total budget (conversion + share of voice)
Week +1: 10-15% of total budget (Cyber Monday + stragglers)

However: This is a starting point. Your MMM will discover the optimal pattern for YOUR brand from YOUR data.

Your multi-period optimization will empirically determine the best allocation by:

  1. Prophet learns lag structures from historical data:

    • If you historically spent more in buildup weeks and saw strong Black Friday sales
    • Prophet encodes that lag relationship in its forecast
  2. Response curves capture dynamics:

    • The saturation curves show how spending in week T affects outcomes in weeks T, T+1, T+2
    • Multi-period optimization accounts for these cross-period effects
  3. Seasonal multipliers show when channels are most effective:

    • If buildup weeks have higher multipliers than Black Friday week
    • Optimization allocates more budget to those higher-ROI weeks

After running your multi-period optimization, validate the results:

import pandas as pd
import matplotlib.pyplot as plt
# Load results
df = pd.read_csv('results/csv/multiperiod_optimization_results.csv')
# Extract Black Friday window (adjust period numbers to your dates)
bf_window = df[(df['period'] >= 4) & (df['period'] <= 8)]
# Check budget allocation pattern
budget_by_period = bf_window.groupby('period')['budget'].sum()
print("\nBudget allocation around Black Friday:")
print(budget_by_period)
# Check seasonal multipliers
multipliers = bf_window[bf_window['channel']=='google'][['period', 'period_date', 'seasonal_multiplier']]
print("\nSeasonal effectiveness:")
print(multipliers)
# Visualize
plt.figure(figsize=(10, 6))
budget_by_period.plot(kind='bar')
plt.title('Budget Allocation: Black Friday Window')
plt.xlabel('Week')
plt.ylabel('Total Budget')
plt.show()

Questions to ask:

  • Is budget ramping up in weeks leading to Black Friday?
  • Where is the peak allocation - before or during the event?
  • How do seasonal multipliers change across the window?

Scenario: Retail brand planning from mid-October through Q1

# Capture full Black Friday season + post-holiday period
results = ammm.optimize_marketing_budget(
model=driver.model,
data=driver.processed_data,
config=driver.config,
results_dir='results',
n_time_periods=26, # Oct through April
multiperiod_mode=True,
use_seasonality=True,
frequency='W',
start_date='2025-10-17' # Mid-October
)

Expected seasonal pattern:

  • Weeks 0-5 (Oct-Nov): Baseline/ramping (multipliers ~0.95-1.05)
  • Weeks 6-7 (Black Friday/Cyber Monday): Peak event (multipliers >1.10)
  • Weeks 8-10 (Dec - Christmas): Sustained high (multipliers 1.05-1.15)
  • Weeks 11-12 (Post-Christmas): Drop-off (multipliers ~0.90-0.95)
  • Weeks 13-26 (Jan-Apr): Q1 baseline (multipliers 0.85-1.00)

Optimization will:

  • Front-load budget to Q4 (higher seasonal multipliers)
  • Concentrate spend 2-3 weeks before Black Friday (based on learned carryover)
  • Reduce budget post-holidays (lower multipliers)

Different channels may have different optimal timing:

Search (Google, Bing):

  • Peak effectiveness: During event (high-intent queries spike)
  • Strategy: Moderate buildup, heavy during Black Friday

Social (Facebook, TikTok, Instagram):

  • Peak effectiveness: 2-3 weeks before (awareness and consideration)
  • Strategy: Heavy buildup, maintain during event

TV/Display:

  • Peak effectiveness: 3-4 weeks before (brand awareness, long lead)
  • Strategy: Very heavy buildup, reduce during event

Your MMM learns this automatically from historical data and will allocate accordingly.

The same principles apply to other retail events:

Prime Day (Amazon):

  • Shorter customer journey (impulse/deal-driven)
  • Suggest: 60% buildup (1-2 weeks), 40% during event

Christmas:

  • Very long planning window (starts October)
  • Suggest: Steady buildup from Oct, peak mid-December

Back to School:

  • Medium planning window (3-4 weeks)
  • Suggest: 70% buildup, 30% during peak weeks

Let your MMM’s optimization discover the optimal timing for each event based on your specific customer behaviour.

Peer-Reviewed Academic Research:

  1. Naik, Mantrala & Sawyer (1998) - “Planning Media Schedules in the Presence of Dynamic Advertising Quality”, Marketing Science
  2. Sethuraman, Tellis & Briesch (2011) - “How Well Does Advertising Work?”, Journal of Marketing Research
  3. Tellis & Weiss (1995) - “Does TV Advertising Really Affect Sales?”, Journal of Marketing
  4. Bruce, Foutz & Kolsarici (2012) - “Dynamic Effectiveness of Advertising and Word of Mouth in Sequential Distribution of Short Lifecycle Products”, Journal of Marketing Research

Industry Research (Non-Peer-Reviewed):

  • Google/Ipsos (2019) - “Black Friday Shopping Behaviour” - Consumer research study
  • Google Retail Insights - Annual Black Friday shopper behaviour studies
  • Facebook Holiday Marketing Guide - Customer journey research
  • Kantar - Seasonal advertising effectiveness studies

Key Takeaway: Multi-period optimization with seasonality lets your MMM empirically determine optimal timing based on YOUR data, rather than relying solely on industry averages.


Cause: Your MMM model doesn’t have Prophet seasonality fitted.

Solution: Ensure Prophet is configured in your model config:

baseline_model: 'prophet' # Use Prophet for baseline/trend

Workaround: Run without seasonality:

use_seasonality=False

Issue: Optimization fails with “Infeasible constraints”

Section titled “Issue: Optimization fails with “Infeasible constraints””

Cause: Per-period budget limits are too restrictive given total budget.

Example:

  • Total budget: £500K
  • Per-period limits: (£200K, £300K) per week
  • 4 weeks: Minimum needed = 4 × £200K = £800K (exceeds £500K!)

Solution: Adjust constraints:

period_budget_limits=(100_000, 200_000) # More flexible

Or increase total budget.


Issue: Results show same budget for all periods

Section titled “Issue: Results show same budget for all periods”

Cause: Seasonality not enabled or no seasonal variation in data.

Solutions:

  1. Enable seasonality: use_seasonality=True
  2. Check if Prophet has seasonal components (see “Checking Seasonal Patterns” above)
  3. If data truly has no seasonality, this is expected behaviour

Issue: Budget allocation seems unrealistic

Section titled “Issue: Budget allocation seems unrealistic”

Cause: Budget bounds may be too wide or too narrow.

Solution: Adjust bounds:

from src.driver.opt import optimize_multiperiod_budget
results = optimize_multiperiod_budget(
...,
lower_bound_pct=0.10, # Channels can go 10% below historical mean
upper_bound_pct=0.30 # Channels can go 30% above historical mean
)

Cause: Large number of periods × channels creates many variables.

Example: 52 weeks × 20 channels = 1,040 optimization variables

Solutions:

  1. Reduce planning horizon: n_periods=13 instead of 52
  2. Use faster frequency: monthly instead of weekly
  3. Simplify constraints (remove per-period limits)

Issue: Optimization fails with “Positive directional derivative” or “failed to converge”

Section titled “Issue: Optimization fails with “Positive directional derivative” or “failed to converge””

Error message:

RuntimeError: Multi-period budget optimization failed to converge.
Solver message: Positive directional derivative for linesearch

Root cause: Over-constrained optimization problem.

What this means:

The combination of features you’ve enabled creates too many competing constraints for the solver to satisfy simultaneously. The optimizer is “trapped in a corner” where it cannot improve the objective without violating at least one constraint.

Common scenarios:

Scenario 1: All Features Enabled (Most Common)

Section titled “Scenario 1: All Features Enabled (Most Common)”
Terminal window
# This often fails for 8+ weeks:
python runme.py --multiperiod --multiperiod-weeks 8 \
--ramp-abs 15000 --ramp-pct 0.20

Why:

  • Absolute ramps: ~45 upper/lower bounds
  • Percentage ramps: ~45 more upper/lower bounds
  • Channel bounds: ~30 min/max constraints
  • Total: many constraints relative to variables

If you enable adstock-aware optimisation via the low-level API, it adds additional state dynamics and can make the problem significantly harder to solve.

Terminal window
# Very restrictive ramp limits:
python runme.py --multiperiod --ramp-abs 5000 --ramp-pct 0.10

Why:

  • 10% ramp on a £100K/week channel = ±£10K
  • If optimal solution needs £20K jump, ramp prevents it
  • Solver cannot find feasible path
Terminal window
# Absolute ramp too loose, percentage too tight:
--ramp-abs 50000 # ±£50K allowed
--ramp-pct 0.05 # ±5% allowed

For a £30K/week channel:

  • Absolute allows: ±£50K (very loose)
  • Percentage allows: ±£1.5K (very tight)
  • Percentage always wins → very restrictive

Solutions (ranked by effectiveness):

Section titled “Solution 1: Use Single Ramp Type ✅ RECOMMENDED”

Choose either absolute OR percentage, not both:

Terminal window
# Option A: Absolute only
python runme.py --multiperiod --multiperiod-weeks 4 --ramp-abs 20000
# Option B: Percentage only
python runme.py --multiperiod --multiperiod-weeks 4 --ramp-pct 0.25

Impact: Removes ~45 constraints, typically resolves convergence issues.

Solution 2: Avoid adstock-aware optimisation (advanced)

Section titled “Solution 2: Avoid adstock-aware optimisation (advanced)”

If you are using the low-level, adstock-aware multi-period optimiser, disable it and retry. In runme.py multi-period mode, adstock-aware optimisation is currently not enabled, so this typically only applies to advanced usage.

Terminal window
# 4 weeks instead of 8:
python runme.py --multiperiod --multiperiod-weeks 4

Impact: Fewer variables (periods × channels) and fewer inter-period constraints.

Trade-off: Shorter planning window may not capture full seasonal patterns.

Terminal window
# Widen ramp limits:
python runme.py --multiperiod --ramp-abs 25000 --ramp-pct 0.35

Or modify code (src/driver/opt.py line ~607):

# Default is ±50%
lower_bound_pct: float = 0.50, # Allow 50% below mean
upper_bound_pct: float = 0.50, # Allow 50% above mean

Impact: Gives solver more “wiggle room” to find feasible solutions.

Remove advanced features temporarily to isolate the issue:

Terminal window
# Minimal config (seasonality only):
python runme.py --multiperiod --multiperiod-weeks 4
# Add a single ramp constraint:
python runme.py --multiperiod --multiperiod-weeks 4 --ramp-abs 20000
# Add a second ramp type (use with care):
python runme.py --multiperiod --multiperiod-weeks 4 --ramp-abs 20000 --ramp-pct 0.25

Test convergence at each step. Identifies which feature combination causes failure.


The optimization problem has:

  • Variables: Budget allocations (n_periods × n_channels)
  • Constraints: Rules the solution must satisfy

Constraint-to-variable ratio:

  • < 1.0: Under-constrained (many feasible solutions) ✅ Easy
  • 1.0-1.5: Well-constrained (goldilocks zone) ✅ Good
  • 1.5-2.5: Moderately over-constrained ⚠ Challenging
  • > 2.5: Heavily over-constrained ❌ Often fails

Example calculation:

15 channels × 4 periods = 60 variables

Constraints:

  • Budget equality: 1
  • Channel bounds: 2 × 15 = 30
  • Absolute ramps: 2 × 3 × 15 = 90
  • Percentage ramps: 2 × 3 × 15 = 90
  • Total: 211 constraints

Ratio: 211 / 60 = 3.5× → Will likely fail

(Advanced) If using adstock-aware state dynamics, this adds additional coupling between periods and can make the problem harder to solve.

Fix: Remove both ramp types → 76 constraints → 1.3× ratio → Should converge


  1. Start simple, add incrementally

    Terminal window
    # Week 1: Test basic
    python runme.py --multiperiod --multiperiod-weeks 4
    # Week 2: Add a single ramp constraint
    python runme.py --multiperiod --multiperiod-weeks 4 --ramp-abs 20000
    # Week 3: Add a second ramp type (optional)
    python runme.py --multiperiod --multiperiod-weeks 4 --ramp-abs 20000 --ramp-pct 0.25
  2. Choose features for your use case

    • Accuracy priority: Seasonality with relaxed constraints (no ramps)
    • Operational realism: Single ramp type + seasonality
    • Maximum control (advanced): Both ramps + per-period budget limits (requires direct Python API)
  3. Monitor solver output

    [!] CONSTRAINT COMPLEXITY ANALYSIS
    Variables: 120 (8 periods × 15 channels)
    Constraints: 256
    Ratio: 2.1× (>1.5 = challenging, >2.5 = very difficult)
    ⚠ WARNING: Problem is moderately constrained
    Convergence may be slow or fail

    If you see ratio >2.0, expect convergence issues.

  4. Test before production

    • Run with --multiperiod-weeks 4 first
    • Verify convergence with small problem
    • Scale up gradually

If you’ve tried all solutions and still cannot converge:

  1. Disable multi-period mode entirely

    Terminal window
    # Fall back to single-period optimization
    python runme.py # No --multiperiod flag
  2. Use scenario planning instead

    Terminal window
    # Test multiple budget levels without optimization
    python runme.py --scenarios "-10,-5,0,5,10"
  3. Manual budget allocation

    • Export response_curve_fit_combined.csv
    • Manually allocate budget using response curves
    • Not optimal but guaranteed feasible
  4. Report to developers

    • Open GitHub issue with your configuration
    • Include error message and --multiperiod flags used
    • Developers may improve solver for your scenario

Currently all channels use the same bound percentages (±20%). To customize per-channel:

# Directly call core function with custom bounds
from src.core.opt import optimize_multiperiod_budget_distribution
budget_ranges = {
'google': (5000, 25000), # Google: £5K-£25K per period
'facebook': (10000, 50000), # Facebook: £10K-£50K per period
'tv': (50000, 200000) # TV: £50K-£200K per period
}
optimized_budget = optimize_multiperiod_budget_distribution(
method='sigmoid',
total_budget=12_500_000,
budget_ranges=budget_ranges,
parameters=sigmoid_params,
channels=list(budget_ranges.keys()),
n_periods=13,
seasonal_effects=seasonal_effects
)

To force equal budget across all periods:

period_budget = total_budget / n_periods
period_budget_limits=(period_budget, period_budget) # Force exact amount

Weekly Planning (more granular):

  • Pros: Accounts for within-month variation, precise allocation
  • Cons: More variables (slower), requires weekly data

Monthly Planning (aggregated):

  • Pros: Faster optimization, simpler to execute
  • Cons: Misses intra-month patterns, less flexible
  1. Start with fewer periods - Test with 4-8 periods before running full year
  2. Validate seasonality - Check Prophet components make sense for your business
  3. Compare with/without seasonality - See impact of seasonal adjustments
  4. Monitor constraint feasibility - Ensure constraints don’t over-restrict optimization
  5. Cross-validate results - Compare multi-period results with historical performance

Multi-period optimization is integrated into runme.py and can be enabled via CLI:

Terminal window
# Enable multi-period mode (13 weeks, with seasonality)
python runme.py --multiperiod
# Or modify runme.py directly for custom settings

See runme.py for full integration example.

See test_multiperiod.py for detailed examples testing:

  • Basic multi-period optimization
  • With seasonality adjustments
  • With per-period budget constraints
  • Validation of results

Run tests:

Terminal window
python test_multiperiod.py

For issues or questions:

  1. Check the Troubleshooting section above
  2. Review GitHub Issues
  3. Create new issue with reproducible example