Marketing Mix Modeling (MMM) Example

This comprehensive example demonstrates how to use Atlas for Marketing Mix Modeling, a common use case for budget optimization across marketing channels.

Overview

Marketing Mix Modeling helps businesses understand:

  • How different marketing channels contribute to sales

  • Optimal budget allocation across channels

  • Diminishing returns and saturation effects

  • Synergies between channels

  • Time-lagged effects of marketing spend

Complete MMM Implementation

Step 1: Setup and Data Preparation

import numpy as np
import pandas as pd
import xarray as xr
from datetime import datetime, timedelta
from atlas import (
    ModelWrapper, 
    OptimizerFactory,
    ConfigurationManager
)
from atlas.strategies import MediaMixOptimizationStrategy
import matplotlib.pyplot as plt
import seaborn as sns

# Set style for visualizations
plt.style.use('seaborn-v0_8-darkgrid')
sns.set_palette("husl")

Step 2: Load and Prepare Marketing Data

# Load historical marketing data
# In practice, this would come from your data warehouse
def generate_sample_mmm_data():
    """Generate realistic marketing mix data for demonstration."""
    np.random.seed(42)
    
    # Time periods (weekly data for 2 years)
    dates = pd.date_range('2022-01-01', '2023-12-31', freq='W')
    n_periods = len(dates)
    
    # Marketing channels
    channels = ['tv', 'digital_search', 'digital_display', 'social', 'radio', 'print']
    
    # Generate spend data with realistic patterns
    data = pd.DataFrame({'date': dates})
    
    # TV: High spend with seasonality
    data['tv_spend'] = (
        100000 + 50000 * np.sin(np.arange(n_periods) * 2 * np.pi / 52) +
        np.random.normal(0, 10000, n_periods)
    ).clip(min=0)
    
    # Digital Search: Growing trend
    data['digital_search_spend'] = (
        50000 + 1000 * np.arange(n_periods) +
        np.random.normal(0, 5000, n_periods)
    ).clip(min=0)
    
    # Digital Display: Stable with campaigns
    data['digital_display_spend'] = (
        30000 + 20000 * (np.random.random(n_periods) > 0.7) +
        np.random.normal(0, 3000, n_periods)
    ).clip(min=0)
    
    # Social: Growing with high volatility
    data['social_spend'] = (
        20000 + 500 * np.arange(n_periods) +
        np.random.normal(0, 8000, n_periods)
    ).clip(min=0)
    
    # Radio: Declining trend
    data['radio_spend'] = (
        40000 - 300 * np.arange(n_periods) +
        np.random.normal(0, 3000, n_periods)
    ).clip(min=10000)
    
    # Print: Seasonal (high in Q4)
    data['print_spend'] = (
        15000 + 10000 * ((data['date'].dt.quarter == 4).astype(int)) +
        np.random.normal(0, 2000, n_periods)
    ).clip(min=0)
    
    # Generate impressions (with diminishing returns)
    for channel in channels:
        spend_col = f'{channel}_spend'
        data[f'{channel}_impressions'] = (
            1000 * np.sqrt(data[spend_col]) +
            np.random.normal(0, 100, n_periods)
        ).clip(min=0)
    
    # Generate sales with channel contributions
    base_sales = 500000
    channel_effects = {
        'tv': 0.3,
        'digital_search': 0.5,
        'digital_display': 0.2,
        'social': 0.15,
        'radio': 0.1,
        'print': 0.05
    }
    
    # Calculate sales with carryover effects
    data['sales'] = base_sales
    
    for channel, effect in channel_effects.items():
        spend_col = f'{channel}_spend'
        # Add immediate effect
        data['sales'] += effect * np.sqrt(data[spend_col])
        # Add carryover effect (30% of previous week)
        carryover = 0.3 * effect * np.sqrt(data[spend_col].shift(1).fillna(0))
        data['sales'] += carryover
    
    # Add seasonality and noise
    data['sales'] += 50000 * np.sin(np.arange(n_periods) * 2 * np.pi / 52)
    data['sales'] += np.random.normal(0, 20000, n_periods)
    
    # Add external factors
    data['competitor_spend'] = np.random.uniform(200000, 400000, n_periods)
    data['macro_index'] = 100 + np.random.normal(0, 5, n_periods)
    
    return data

# Load data
marketing_data = generate_sample_mmm_data()
print(marketing_data.head())
print(f"\nData shape: {marketing_data.shape}")
print(f"Date range: {marketing_data['date'].min()} to {marketing_data['date'].max()}")

Step 3: Create Marketing Mix Model

class MarketingMixModel(ModelWrapper):
    """
    Marketing Mix Model with saturation curves and carryover effects.
    """
    
    def __init__(self, 
                 historical_data: pd.DataFrame,
                 carryover_rates: dict = None,
                 saturation_params: dict = None):
        """
        Initialize MMM model.
        
        Args:
            historical_data: Historical marketing and sales data
            carryover_rates: Adstock/carryover rates by channel
            saturation_params: Saturation curve parameters by channel
        """
        super().__init__()
        self.historical_data = historical_data
        self.channels = ['tv', 'digital_search', 'digital_display', 
                        'social', 'radio', 'print']
        
        # Default carryover rates (percentage of effect carrying to next period)
        self.carryover_rates = carryover_rates or {
            'tv': 0.3,
            'digital_search': 0.1,
            'digital_display': 0.15,
            'social': 0.05,
            'radio': 0.2,
            'print': 0.25
        }
        
        # Default saturation parameters (Hill transformation)
        self.saturation_params = saturation_params or {
            'tv': {'alpha': 2.5, 'gamma': 0.8},
            'digital_search': {'alpha': 2.0, 'gamma': 0.9},
            'digital_display': {'alpha': 2.2, 'gamma': 0.85},
            'social': {'alpha': 1.8, 'gamma': 0.95},
            'radio': {'alpha': 2.3, 'gamma': 0.7},
            'print': {'alpha': 2.0, 'gamma': 0.6}
        }
        
        # Fit response curves
        self._fit_response_curves()
    
    def _fit_response_curves(self):
        """Fit response curves from historical data."""
        self.response_curves = {}
        
        for channel in self.channels:
            spend = self.historical_data[f'{channel}_spend'].values
            # Simplified: use sqrt transformation
            # In practice, you'd fit actual response curves
            self.response_curves[channel] = {
                'coefficient': np.corrcoef(
                    np.sqrt(spend), 
                    self.historical_data['sales']
                )[0, 1] * 1000
            }
    
    def _apply_saturation(self, spend: float, channel: str) -> float:
        """Apply Hill saturation transformation."""
        params = self.saturation_params[channel]
        alpha = params['alpha']
        gamma = params['gamma']
        
        # Hill transformation: spend^alpha / (spend^alpha + gamma^alpha)
        return (spend ** alpha) / (spend ** alpha + gamma ** alpha)
    
    def _apply_carryover(self, spend_series: np.ndarray, channel: str) -> np.ndarray:
        """Apply adstock/carryover transformation."""
        carryover = self.carryover_rates[channel]
        result = np.zeros_like(spend_series)
        
        for t in range(len(spend_series)):
            # Current period effect
            result[t] = spend_series[t]
            # Add carryover from previous periods
            for lag in range(1, min(t + 1, 13)):  # Max 13 weeks carryover
                result[t] += spend_series[t - lag] * (carryover ** lag)
        
        return result
    
    def predict(self, budget_allocation: xr.Dataset) -> xr.DataArray:
        """
        Predict sales given budget allocation.
        
        Args:
            budget_allocation: Dataset with budget by channel and time
            
        Returns:
            Predicted sales
        """
        # Extract dimensions
        time_periods = budget_allocation.coords['time']
        n_periods = len(time_periods)
        
        # Initialize predictions
        predictions = np.zeros(n_periods)
        base_sales = 500000  # Base sales without marketing
        
        # Process each channel
        for channel in self.channels:
            if channel in budget_allocation.data_vars:
                # Get spend series
                spend = budget_allocation[channel].values
                
                # Apply saturation
                saturated_spend = np.array([
                    self._apply_saturation(s, channel) for s in spend
                ])
                
                # Apply carryover
                effective_spend = self._apply_carryover(saturated_spend, channel)
                
                # Calculate contribution
                coef = self.response_curves[channel]['coefficient']
                contribution = coef * effective_spend
                
                predictions += contribution
        
        # Add base sales and seasonality
        week_of_year = pd.to_datetime(time_periods.values).isocalendar().week
        seasonality = 50000 * np.sin(2 * np.pi * week_of_year / 52)
        predictions += base_sales + seasonality
        
        # Return as xarray
        return xr.DataArray(
            predictions,
            coords={'time': time_periods},
            dims=['time'],
            name='predicted_sales'
        )
    
    def get_channel_contributions(self, budget_allocation: xr.Dataset) -> xr.Dataset:
        """Calculate individual channel contributions."""
        contributions = {}
        
        for channel in self.channels:
            if channel in budget_allocation.data_vars:
                # Create single-channel budget
                single_channel = budget_allocation.copy()
                for ch in self.channels:
                    if ch != channel:
                        single_channel[ch] = 0
                
                # Get contribution
                contribution = self.predict(single_channel)
                contributions[channel] = contribution - 500000  # Remove base
        
        return xr.Dataset(contributions)
    
    def calculate_roi(self, budget_allocation: xr.Dataset) -> dict:
        """Calculate ROI by channel."""
        contributions = self.get_channel_contributions(budget_allocation)
        roi = {}
        
        for channel in self.channels:
            if channel in budget_allocation.data_vars:
                total_spend = float(budget_allocation[channel].sum())
                total_contribution = float(contributions[channel].sum())
                
                if total_spend > 0:
                    roi[channel] = total_contribution / total_spend
                else:
                    roi[channel] = 0
        
        return roi

Step 4: Set Up Optimization

# Create model instance
mmm_model = MarketingMixModel(marketing_data)

# Define optimization constraints
def create_mmm_constraints(total_budget: float = 1_000_000):
    """Create realistic MMM optimization constraints."""
    return {
        'total_budget': total_budget,
        'channel_bounds': {
            'tv': (50_000, 400_000),           # TV needs minimum for effectiveness
            'digital_search': (100_000, 500_000), # High-performing channel
            'digital_display': (20_000, 200_000),
            'social': (30_000, 300_000),
            'radio': (10_000, 100_000),         # Declining channel
            'print': (5_000, 50_000)            # Minimal investment
        },
        'business_rules': {
            # Digital should be at least 40% of budget
            'min_digital_share': 0.4,
            # No single channel over 40%
            'max_channel_share': 0.4,
            # Traditional media (TV + Radio + Print) max 50%
            'max_traditional_share': 0.5
        },
        'ratio_constraints': {
            # Search should be at least 50% of display
            ('digital_search', 'digital_display'): (0.5, None),
            # Social shouldn't exceed search
            ('social', 'digital_search'): (None, 1.0)
        }
    }

# Create optimization strategy
optimization_strategy = MediaMixOptimizationStrategy(
    reach_frequency_targets={'reach': 0.75, 'frequency': 3.0},
    seasonality_factors={'Q1': 0.8, 'Q2': 1.0, 'Q3': 0.9, 'Q4': 1.3},
    competitive_response=True
)

# Create optimizer
optimizer = OptimizerFactory.create(
    optimizer_type='optuna',
    model=mmm_model,
    strategy=optimization_strategy,
    config={
        'n_trials': 1000,
        'n_jobs': -1,
        'sampler': 'TPE',
        'pruner': 'MedianPruner'
    }
)

Step 5: Run Optimization for Different Scenarios

# Scenario 1: Current period optimization
current_budget = {
    'tv': 150_000,
    'digital_search': 200_000,
    'digital_display': 80_000,
    'social': 100_000,
    'radio': 50_000,
    'print': 20_000
}

print("Current Budget Allocation:")
for channel, amount in current_budget.items():
    print(f"  {channel}: ${amount:,.0f}")
print(f"  Total: ${sum(current_budget.values()):,.0f}")

# Run optimization
constraints = create_mmm_constraints(total_budget=1_000_000)
optimal_result = optimizer.optimize(
    initial_budget=current_budget,
    constraints=constraints,
    time_periods=['2024-W01']  # Next week
)

print("\nOptimal Budget Allocation:")
for channel, amount in optimal_result.optimal_budget.items():
    print(f"  {channel}: ${amount:,.0f}")
print(f"  Total: ${sum(optimal_result.optimal_budget.values()):,.0f}")
print(f"\nExpected Sales Increase: ${optimal_result.improvement:,.0f}")

# Scenario 2: Multi-period optimization (Quarter)
quarterly_periods = pd.date_range('2024-01-01', '2024-03-31', freq='W')
quarterly_budget = 12_000_000  # $12M for Q1

quarterly_result = optimizer.optimize_multiperiod(
    periods=quarterly_periods,
    total_budget=quarterly_budget,
    constraints=constraints,
    objective='maximize_sales'
)

# Visualize quarterly allocation
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
fig.suptitle('Quarterly Budget Optimization Results', fontsize=16)

# Plot 1: Budget allocation over time
ax1 = axes[0, 0]
allocation_df = quarterly_result.to_dataframe()
allocation_df.plot(kind='area', ax=ax1)
ax1.set_title('Weekly Budget Allocation by Channel')
ax1.set_xlabel('Week')
ax1.set_ylabel('Budget ($)')
ax1.legend(loc='best')

# Plot 2: Channel mix comparison
ax2 = axes[0, 1]
current_mix = pd.Series(current_budget)
optimal_mix = pd.Series(optimal_result.optimal_budget)
comparison_df = pd.DataFrame({
    'Current': current_mix,
    'Optimal': optimal_mix
})
comparison_df.plot(kind='bar', ax=ax2)
ax2.set_title('Current vs Optimal Channel Mix')
ax2.set_ylabel('Budget ($)')
ax2.set_xticklabels(ax2.get_xticklabels(), rotation=45)

# Plot 3: ROI by channel
ax3 = axes[1, 0]
current_roi = mmm_model.calculate_roi(
    xr.Dataset({ch: xr.DataArray([amt]) for ch, amt in current_budget.items()})
)
optimal_roi = mmm_model.calculate_roi(
    xr.Dataset({ch: xr.DataArray([amt]) for ch, amt in optimal_result.optimal_budget.items()})
)
roi_comparison = pd.DataFrame({
    'Current ROI': current_roi,
    'Optimal ROI': optimal_roi
})
roi_comparison.plot(kind='bar', ax=ax3)
ax3.set_title('ROI Comparison by Channel')
ax3.set_ylabel('ROI (Sales/Spend)')
ax3.set_xticklabels(ax3.get_xticklabels(), rotation=45)

# Plot 4: Saturation curves
ax4 = axes[1, 1]
spend_range = np.linspace(0, 500_000, 100)
for channel in ['tv', 'digital_search', 'social']:
    response = [mmm_model._apply_saturation(s, channel) * 
                mmm_model.response_curves[channel]['coefficient'] 
                for s in spend_range]
    ax4.plot(spend_range, response, label=channel)
ax4.set_title('Channel Response Curves')
ax4.set_xlabel('Spend ($)')
ax4.set_ylabel('Response')
ax4.legend()

plt.tight_layout()
plt.show()

Step 6: Advanced Analysis

# Marginal ROI Analysis
def calculate_marginal_roi(model, current_budget, increment=10_000):
    """Calculate marginal ROI for each channel."""
    marginal_roi = {}
    
    for channel in model.channels:
        # Current performance
        current_pred = model.predict(
            xr.Dataset({ch: xr.DataArray([current_budget.get(ch, 0)]) 
                       for ch in model.channels})
        )
        
        # Performance with increment
        incremented_budget = current_budget.copy()
        incremented_budget[channel] = incremented_budget.get(channel, 0) + increment
        
        incremented_pred = model.predict(
            xr.Dataset({ch: xr.DataArray([incremented_budget.get(ch, 0)]) 
                       for ch in model.channels})
        )
        
        # Marginal ROI
        marginal_sales = float(incremented_pred - current_pred)
        marginal_roi[channel] = marginal_sales / increment
    
    return marginal_roi

marginal_roi = calculate_marginal_roi(mmm_model, current_budget)
print("\nMarginal ROI Analysis (per $10k increase):")
for channel, roi in sorted(marginal_roi.items(), key=lambda x: x[1], reverse=True):
    print(f"  {channel}: ${roi:.2f} per dollar")

# Budget Optimization with Different Objectives
objectives = {
    'maximize_sales': {'weight': 1.0, 'target': 'sales'},
    'maximize_efficiency': {'weight': 1.0, 'target': 'roi'},
    'balanced': {'weight': 0.7, 'target': 'sales', 
                 'weight_roi': 0.3, 'target_roi': 'efficiency'}
}

results_by_objective = {}
for obj_name, obj_config in objectives.items():
    result = optimizer.optimize(
        initial_budget=current_budget,
        constraints=constraints,
        objective=obj_config
    )
    results_by_objective[obj_name] = result

# Compare results
print("\nOptimization Results by Objective:")
comparison_data = []
for obj_name, result in results_by_objective.items():
    total_spend = sum(result.optimal_budget.values())
    expected_sales = result.optimal_value
    overall_roi = expected_sales / total_spend if total_spend > 0 else 0
    
    comparison_data.append({
        'Objective': obj_name,
        'Total Spend': total_spend,
        'Expected Sales': expected_sales,
        'Overall ROI': overall_roi
    })

comparison_df = pd.DataFrame(comparison_data)
print(comparison_df.to_string(index=False))

# Scenario Analysis
scenarios = {
    'recession': {
        'total_budget': 700_000,
        'constraints': {
            'max_traditional_share': 0.3,  # Focus on measurable channels
            'min_digital_share': 0.6
        }
    },
    'growth': {
        'total_budget': 1_500_000,
        'constraints': {
            'min_awareness_channels': 0.4,  # TV + Radio for awareness
            'allow_experimental': True
        }
    },
    'competitive_response': {
        'total_budget': 1_000_000,
        'constraints': {
            'min_search_share': 0.3,  # Defend search position
            'max_channel_volatility': 0.2  # Limit big changes
        }
    }
}

scenario_results = {}
for scenario_name, scenario_config in scenarios.items():
    constraints = create_mmm_constraints(
        total_budget=scenario_config['total_budget']
    )
    constraints.update(scenario_config.get('constraints', {}))
    
    result = optimizer.optimize(
        initial_budget=current_budget,
        constraints=constraints
    )
    scenario_results[scenario_name] = result

# Visualize scenario comparison
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))

# Budget allocation by scenario
scenario_budgets = pd.DataFrame({
    scenario: result.optimal_budget 
    for scenario, result in scenario_results.items()
})
scenario_budgets.plot(kind='bar', ax=ax1)
ax1.set_title('Budget Allocation by Scenario')
ax1.set_xlabel('Channel')
ax1.set_ylabel('Budget ($)')
ax1.legend(title='Scenario')

# Expected outcomes by scenario
outcomes = pd.DataFrame({
    'Expected Sales': {
        scenario: result.optimal_value 
        for scenario, result in scenario_results.items()
    },
    'Total Budget': {
        scenario: sum(result.optimal_budget.values())
        for scenario, result in scenario_results.items()
    }
})
outcomes.plot(kind='bar', ax=ax2)
ax2.set_title('Expected Outcomes by Scenario')
ax2.set_xlabel('Scenario')
ax2.set_ylabel('Value ($)')

plt.tight_layout()
plt.show()

Step 7: Generate Executive Report

class MMM_ReportGenerator:
    """Generate executive reports for MMM optimization results."""
    
    def __init__(self, model, optimization_results, current_budget):
        self.model = model
        self.results = optimization_results
        self.current_budget = current_budget
    
    def generate_executive_summary(self):
        """Generate executive summary of optimization results."""
        optimal_budget = self.results.optimal_budget
        
        # Calculate key metrics
        current_total = sum(self.current_budget.values())
        optimal_total = sum(optimal_budget.values())
        
        # Predict performance
        current_sales = float(self.model.predict(
            xr.Dataset({ch: xr.DataArray([self.current_budget.get(ch, 0)]) 
                       for ch in self.model.channels})
        ))
        optimal_sales = float(self.model.predict(
            xr.Dataset({ch: xr.DataArray([optimal_budget.get(ch, 0)]) 
                       for ch in self.model.channels})
        ))
        
        sales_increase = optimal_sales - current_sales
        sales_increase_pct = (sales_increase / current_sales) * 100
        
        # ROI comparison
        current_roi = current_sales / current_total if current_total > 0 else 0
        optimal_roi = optimal_sales / optimal_total if optimal_total > 0 else 0
        roi_improvement = optimal_roi - current_roi
        
        # Channel shifts
        channel_changes = {}
        for channel in self.model.channels:
            current = self.current_budget.get(channel, 0)
            optimal = optimal_budget.get(channel, 0)
            change_amt = optimal - current
            change_pct = (change_amt / current * 100) if current > 0 else 0
            channel_changes[channel] = {
                'current': current,
                'optimal': optimal,
                'change_amt': change_amt,
                'change_pct': change_pct
            }
        
        # Generate report
        report = f"""
# Marketing Mix Optimization - Executive Summary

## Key Findings

**Expected Sales Improvement**: ${sales_increase:,.0f} (+{sales_increase_pct:.1f}%)
**ROI Improvement**: {roi_improvement:.2f} ({current_roi:.2f} β†’ {optimal_roi:.2f})
**Budget Maintained**: ${optimal_total:,.0f}

## Recommended Actions

### Immediate Changes (High Impact)
"""
        
        # Sort by absolute change
        sorted_changes = sorted(
            channel_changes.items(), 
            key=lambda x: abs(x[1]['change_amt']), 
            reverse=True
        )
        
        for channel, change in sorted_changes[:3]:
            if change['change_amt'] > 0:
                action = "INCREASE"
                emoji = "πŸ“ˆ"
            else:
                action = "DECREASE"
                emoji = "πŸ“‰"
            
            report += f"""
{emoji} **{action} {channel.replace('_', ' ').title()}**
   - Current: ${change['current']:,.0f}
   - Recommended: ${change['optimal']:,.0f}
   - Change: ${change['change_amt']:,.0f} ({change['change_pct']:+.1f}%)
"""
        
        # Add strategic insights
        report += """
## Strategic Insights

### Channel Performance
"""
        marginal_roi = calculate_marginal_roi(self.model, self.current_budget)
        top_performers = sorted(
            marginal_roi.items(), 
            key=lambda x: x[1], 
            reverse=True
        )[:3]
        
        report += "**Highest Marginal ROI Channels**:\n"
        for channel, roi in top_performers:
            report += f"- {channel.replace('_', ' ').title()}: ${roi:.2f} per dollar\n"
        
        # Add risks and recommendations
        report += """

### Risk Considerations

1. **Implementation Timeline**: Changes should be phased over 2-3 weeks to monitor impact
2. **Market Conditions**: Results assume stable competitive environment
3. **Measurement**: Set up proper tracking before implementation

### Next Steps

1. Review and approve budget reallocation
2. Set up A/B tests for major changes
3. Implement tracking for ROI validation
4. Schedule follow-up optimization in 4 weeks

---
*Report generated: {date}*
        """.format(date=datetime.now().strftime("%Y-%m-%d %H:%M"))
        
        return report

# Generate report
report_gen = MMM_ReportGenerator(mmm_model, optimal_result, current_budget)
executive_summary = report_gen.generate_executive_summary()
print(executive_summary)

# Save report
with open('mmm_optimization_report', 'w') as f:
    f.write(executive_summary)

Step 8: Continuous Optimization

class MMM_ContinuousOptimizer:
    """Continuous optimization with performance tracking."""
    
    def __init__(self, model, optimizer, tracking_config):
        self.model = model
        self.optimizer = optimizer
        self.tracking_config = tracking_config
        self.optimization_history = []
    
    def run_weekly_optimization(self, current_performance, market_conditions):
        """Run weekly optimization with latest data."""
        # Update model with latest performance
        self.model.update_with_actuals(current_performance)
        
        # Adjust constraints based on market
        constraints = self._adjust_constraints(market_conditions)
        
        # Run optimization
        result = self.optimizer.optimize(
            initial_budget=current_performance['budget'],
            constraints=constraints
        )
        
        # Track results
        self.optimization_history.append({
            'date': datetime.now(),
            'result': result,
            'market_conditions': market_conditions
        })
        
        return result
    
    def _adjust_constraints(self, market_conditions):
        """Dynamically adjust constraints based on market."""
        base_constraints = create_mmm_constraints()
        
        # Adjust based on competition
        if market_conditions.get('competitor_activity') == 'high':
            base_constraints['business_rules']['min_search_share'] = 0.35
        
        # Adjust for seasonality
        if market_conditions.get('season') == 'holiday':
            base_constraints['channel_bounds']['tv'] = (100_000, 500_000)
        
        return base_constraints
    
    def generate_performance_dashboard(self):
        """Generate performance tracking dashboard."""
        if not self.optimization_history:
            return "No optimization history available"
        
        # Create visualizations
        fig, axes = plt.subplots(2, 2, figsize=(15, 10))
        fig.suptitle('MMM Optimization Performance Dashboard', fontsize=16)
        
        # Extract historical data
        dates = [h['date'] for h in self.optimization_history]
        predicted_sales = [h['result'].optimal_value for h in self.optimization_history]
        
        # Plot 1: Predicted sales over time
        axes[0, 0].plot(dates, predicted_sales, marker='o')
        axes[0, 0].set_title('Predicted Sales Trend')
        axes[0, 0].set_xlabel('Date')
        axes[0, 0].set_ylabel('Predicted Sales ($)')
        
        # Plot 2: Budget allocation evolution
        channels = self.model.channels
        allocation_history = pd.DataFrame([
            h['result'].optimal_budget for h in self.optimization_history
        ], index=dates)
        
        allocation_history.plot(ax=axes[0, 1])
        axes[0, 1].set_title('Budget Allocation Evolution')
        axes[0, 1].set_xlabel('Date')
        axes[0, 1].set_ylabel('Budget ($)')
        axes[0, 1].legend(bbox_to_anchor=(1.05, 1), loc='upper left')
        
        # Plot 3: ROI trends
        roi_history = pd.DataFrame([
            self.model.calculate_roi(
                xr.Dataset({ch: xr.DataArray([h['result'].optimal_budget.get(ch, 0)]) 
                           for ch in channels})
            ) for h in self.optimization_history
        ], index=dates)
        
        roi_history.plot(ax=axes[1, 0])
        axes[1, 0].set_title('Channel ROI Trends')
        axes[1, 0].set_xlabel('Date')
        axes[1, 0].set_ylabel('ROI')
        
        # Plot 4: Market conditions impact
        if len(self.optimization_history) > 1:
            conditions = [h['market_conditions'].get('competitor_activity', 'normal') 
                         for h in self.optimization_history]
            condition_impact = pd.Series(predicted_sales, index=dates)
            
            for condition in set(conditions):
                mask = [c == condition for c in conditions]
                axes[1, 1].scatter(
                    [d for d, m in zip(dates, mask) if m],
                    [s for s, m in zip(predicted_sales, mask) if m],
                    label=f'Competition: {condition}',
                    alpha=0.7
                )
            
            axes[1, 1].set_title('Sales vs Market Conditions')
            axes[1, 1].set_xlabel('Date')
            axes[1, 1].set_ylabel('Predicted Sales ($)')
            axes[1, 1].legend()
        
        plt.tight_layout()
        return fig

# Example usage
continuous_optimizer = MMM_ContinuousOptimizer(
    model=mmm_model,
    optimizer=optimizer,
    tracking_config={
        'update_frequency': 'weekly',
        'performance_threshold': 0.95,
        'reoptimize_trigger': 'auto'
    }
)

Key Takeaways

This comprehensive example demonstrates:

  1. Data Preparation: Loading and structuring marketing data for optimization

  2. Model Creation: Building a realistic MMM with saturation and carryover effects

  3. Constraint Definition: Setting up business-realistic constraints

  4. Optimization Execution: Running single and multi-period optimizations

  5. Results Analysis: Calculating ROI, marginal returns, and scenario analysis

  6. Visualization: Creating insightful charts for decision-making

  7. Reporting: Generating executive-friendly summaries

  8. Continuous Optimization: Setting up ongoing optimization processes

Next Steps

To adapt this example for your use case:

  1. Replace sample data with your actual marketing data

  2. Customize the model with your specific response curves

  3. Adjust constraints based on your business rules

  4. Add additional channels or modify existing ones

  5. Integrate with your data pipeline for automation

For more advanced features, explore: