Skip to main content

marketing-mix-modeling-end-to-end-pipeline

Build end-to-end Marketing Mix Models with adstock, saturation, OLS regression, and budget optimization in Python

Jump to install

Source facts

Repository
reason-machines/marketing-skills
Last source activity
June 26, 2026 at 22:30
Detected SKILL.md language
English
Stars
10
Forks
1

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
marketing-mix-modeling-end-to-end-pipeline
description
Build end-to-end Marketing Mix Models with adstock, saturation, OLS regression, and budget optimization in Python
triggers
["build a marketing mix model","implement MMM with adstock and saturation","optimize marketing budget allocation","calculate channel ROI for marketing spend","apply geometric adstock transformation","run marketing attribution analysis","create budget optimizer for marketing channels","decompose revenue by marketing channel"]
# Marketing Mix Modeling End-to-End Pipeline > Skill by [ara.so](https://ara.so) — Marketing Skills collection. This project provides a complete Marketing Mix Modeling (MMM) pipeline in Python that transforms raw marketing spend data into actionable channel insights and optimized budget allocations. It includes geometric adstock transformation, Hill saturation curves, OLS regression modeling, and constrained budget optimization. ## Installation ```bash # Clone the repository git clone https://github.com/francescaetnom-wq/Marketing-Mix-Modeling-End-to-End-Pipeline.git cd Marketing-Mix-Modeling-End-to-End-Pipeline # Create virtual environment python3 -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate # Install dependencies pip install -r requirements.txt ``` **Key Dependencies:** - `pandas` — data manipulation - `numpy` — numerical operations - `statsmodels` — OLS regression - `scipy` — optimization algorithms - `matplotlib` / `seaborn` — visualization ## Project Structure ``` mmm_project/ ├── data/ │ ├── dt_simulated_weekly.csv # Input: weekly spend & revenue │ ├── dt_transformed.csv # Output: with adstock/saturation │ ├── coefficients.csv # Output: model coefficients │ └── optimized_budget.csv # Output: budget recommendations ├── src/ │ ├── adstock.py # Geometric adstock │ ├── saturation.py # Hill saturation │ ├── model.py # OLS wrapper │ └── optimizer.py # Budget allocation └── notebooks/ ├── 01_exploration.ipynb ├── 02_transformations.ipynb ├── 03_model.ipynb └── 04_optimizer.ipynb ``` ## Core Transformations ### 1. Geometric Adstock Models the carryover effect of advertising — this week's exposure includes decayed contributions from previous weeks. **Implementation (`src/adstock.py`):** ```python import numpy as np import pandas as pd def geometric_adstock(x, theta, L_max=8): """ Apply geometric adstock transformation. Parameters: ----------- x : array-like Raw media spend or impressions theta : float Decay rate (0 to 1). Higher = longer carryover L_max : int Maximum lag window Returns: -------- array : Adstocked signal """ x = np.array(x) adstocked = np.zeros_like(x, dtype=float) for t in range(len(x)): for lag in range(min(t + 1, L_max)): adstocked[t] += x[t - lag] * (theta ** lag) return adstocked # Example usage spend = [100, 150, 120, 90, 110] adstocked_spend = geometric_adstock(spend, theta=0.5, L_max=4) # adstocked_spend accounts for carryover from previous weeks ``` **Apply to DataFrame:** ```python import pandas as pd df = pd.read_csv('data/dt_simulated_weekly.csv') # Define adstock parameters per channel adstock_params = { 'tv_S': 0.7, # High carryover 'search_S': 0.3, # Low carryover 'facebook_S': 0.5, 'print_S': 0.6, 'ooh_S': 0.4 } # Apply adstock to each channel for channel, theta in adstock_params.items(): df[f'{channel}_adstocked'] = geometric_adstock( df[channel].values, theta=theta, L_max=8 ) ``` ### 2. Hill Saturation Models diminishing returns — the first dollar spent is more effective than the millionth. **Implementation (`src/saturation.py`):** ```python import numpy as np def hill_saturation(x, alpha, gamma): """ Apply Hill saturation transformation. Parameters: ----------- x : array-like Input signal (usually adstocked spend) alpha : float Half-saturation point (inflection) gamma : float Shape parameter (> 1 for S-curve) Returns: -------- array : Saturated signal """ x = np.array(x) return (x ** gamma) / (alpha ** gamma + x ** gamma) # Example usage adstocked = np.array([50, 100, 150, 200, 250]) saturated = hill_saturation(adstocked, alpha=100, gamma=2.0) # saturated shows diminishing marginal returns ``` **Apply After Adstock (Order Matters!):** ```python # Saturation parameters per channel saturation_params = { 'tv_S': {'alpha': 150, 'gamma': 2.0}, 'search_S': {'alpha': 80, 'gamma': 1.8}, 'facebook_S': {'alpha': 100, 'gamma': 1.9}, 'print_S': {'alpha': 120, 'gamma': 2.1}, 'ooh_S': {'alpha': 90, 'gamma': 2.0} } # Apply saturation to adstocked columns for channel, params in saturation_params.items(): adstocked_col = f'{channel}_adstocked' df[f'{channel}_transformed'] = hill_saturation( df[adstocked_col].values, alpha=params['alpha'], gamma=params['gamma'] ) # Save transformed data df.to_csv('data/dt_transformed.csv', index=False) ``` ## OLS Regression Model ### Training the Model **Implementation (`src/model.py`):** ```python import pandas as pd import statsmodels.api as sm def fit_mmm_model(df, transformed_channels, controls, target='revenue'): """ Fit OLS regression for MMM. Parameters: ----------- df : DataFrame Input data with transformed channels transformed_channels : list Column names of transformed media variables controls : list Control variables (e.g., competitor_sales_B, events) target : str Revenue or sales column Returns: -------- model : statsmodels RegressionResults """ # Prepare feature matrix X = df[transformed_channels + controls].copy() # Handle categorical controls (one-hot encode events) if 'events' in controls: X = pd.get_dummies(X, columns=['events'], drop_first=True) # Add constant X = sm.add_constant(X) # Target variable y = df[target] # Fit OLS model = sm.OLS(y, X).fit() return model # Example usage df = pd.read_csv('data/dt_transformed.csv') transformed_channels = [ 'tv_S_transformed', 'search_S_transformed', 'facebook_S_transformed', 'print_S_transformed', 'ooh_S_transformed' ] controls = ['competitor_sales_B', 'events'] model = fit_mmm_model(df, transformed_channels, controls, target='revenue') print(model.summary()) print(f"\nR-squared: {model.rsquared:.3f}") ``` ### Extract Coefficients & ROI ```python import pandas as pd def extract_coefficients(model, channel_names): """Extract coefficients and calculate proxy ROI.""" coefs = model.params[channel_names] pvalues = model.pvalues[channel_names] results = pd.DataFrame({ 'channel': channel_names, 'coefficient': coefs.values, 'p_value': pvalues.values }) return results # Get coefficients channel_names = [col for col in model.params.index if '_transformed' in col] coef_df = extract_coefficients(model, channel_names) # Calculate proxy ROI (revenue per unit spend) # Match transformed columns to original spend columns spend_mapping = { 'tv_S_transformed': 'tv_S', 'search_S_transformed': 'search_S', 'facebook_S_transformed': 'facebook_S', 'print_S_transformed': 'print_S', 'ooh_S_transformed': 'ooh_S' } for idx, row in coef_df.iterrows(): channel_transformed = row['channel'] channel_raw = spend_mapping[channel_transformed] # Average contribution / average spend avg_spend = df[channel_raw].mean() avg_contribution = row['coefficient'] * df[channel_transformed].mean() coef_df.loc[idx, 'avg_spend'] = avg_spend coef_df.loc[idx, 'avg_contribution'] = avg_contribution coef_df.loc[idx, 'proxy_roi'] = avg_contribution / avg_spend if avg_spend > 0 else 0 coef_df.to_csv('data/coefficients.csv', index=False) print(coef_df) ``` ## Budget Optimization Find the optimal budget allocation to maximize revenue under a fixed total budget constraint. **Implementation (`src/optimizer.py`):** ```python import numpy as np from scipy.optimize import minimize def optimize_budget(model, df, channel_mapping, total_budget, adstock_params, saturation_params): """ Optimize budget allocation across channels. Parameters: ----------- model : statsmodels RegressionResults Fitted OLS model df : DataFrame Historical data (for baseline calculation) channel_mapping : dict Maps transformed column names to raw spend columns total_budget : float Total budget constraint adstock_params : dict Adstock theta per channel saturation_params : dict Saturation alpha/gamma per channel Returns: -------- dict : Optimized budget allocation """ from adstock import geometric_adstock from saturation import hill_saturation channels = list(channel_mapping.keys()) n_channels = len(channels) # Extract coefficients coefs = {col: model.params[col] for col in channels} def predict_revenue(budget_allocation): """Predict revenue given budget allocation.""" revenue = 0 for i, channel_raw in enumerate(channels): spend = budget_allocation[i] channel_transformed = channel_mapping[channel_raw] # Apply transformations adstocked = geometric_adstock( [spend], theta=adstock_params[channel_raw], L_max=1 # Single period optimization )[0] saturated = hill_saturation( [adstocked], alpha=saturation_params[channel_raw]['alpha'], gamma=saturation_params[channel_raw]['gamma'] )[0] # Multiply by coefficient revenue += saturated * coefs[channel_transformed] return revenue # Objective: negative revenue (minimize = maximize revenue) def objective(budget_allocation): return -predict_revenue(budget_allocation) # Constraints constraints = [ {'type': 'eq', 'fun': lambda x: np.sum(x) - total_budget} # Sum = total ] # Bounds: non-negative spend bounds = [(0, total_budget) for _ in range(n_channels)] # Initial guess: equal split x0 = np.array([total_budget / n_channels] * n_channels) # Optimize result = minimize( objective, x0, method='SLSQP', bounds=bounds, constraints=constraints ) # Format results optimized_budget = { channel: result.x[i] for i, channel in enumerate(channels) } return optimized_budget, -result.fun # Return revenue (negate objective) # Example usage channel_mapping = { 'tv_S': 'tv_S_transformed', 'search_S': 'search_S_transformed', 'facebook_S': 'facebook_S_transformed', 'print_S': 'print_S_transformed', 'ooh_S': 'ooh_S_transformed' } # Calculate current total budget current_budget = df[list(channel_mapping.keys())].sum(axis=1).mean() # Optimize optimized, projected_revenue = optimize_budget( model=model, df=df, channel_mapping=channel_mapping, total_budget=current_budget, adstock_params=adstock_params, saturation_params=saturation_params ) # Compare current vs optimized current_allocation = {ch: df[ch].mean() for ch in channel_mapping.keys()} comparison = pd.DataFrame({ 'channel': list(channel_mapping.keys()), 'current_budget': [current_allocation[ch] for ch in channel_mapping.keys()], 'optimized_budget': [optimized[ch] for ch in channel_mapping.keys()] }) comparison['change_pct'] = ( (comparison['optimized_budget'] - comparison['current_budget']) / comparison['current_budget'] * 100 ) comparison.to_csv('data/optimized_budget.csv', index=False) print(comparison) ``` ## Complete Workflow ```python import pandas as pd import numpy as np from src.adstock import geometric_adstock from src.saturation import hill_saturation from src.model import fit_mmm_model from src.optimizer import optimize_budget # 1. Load data df = pd.read_csv('data/dt_simulated_weekly.csv') df['DATE'] = pd.to_datetime(df['DATE']) # Clean events column (handle literal "na" string) df['events'] = df['events'].replace('na', np.nan) # 2. Define transformation parameters
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub