Skip to main content

marketing-mix-modeling-mmm-pipeline

End-to-end Marketing Mix Modeling pipeline with adstock, saturation, OLS regression, and budget optimization

Ir para a instalação

Informações da origem

Repositório
reason-machines/marketing-skills
Última atividade na origem
26 de junho de 2026 às 21:34
Idioma detectado do SKILL.md
inglês
Estrelas
10
Forks
1

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
marketing-mix-modeling-mmm-pipeline
description
End-to-end Marketing Mix Modeling pipeline with adstock, saturation, OLS regression, and budget optimization
triggers
["build a marketing mix model","analyze marketing channel effectiveness","optimize marketing budget allocation","calculate marketing ROI by channel","implement adstock transformation","apply saturation curves to marketing spend","decompose sales into marketing contributions","run MMM pipeline"]
# Marketing Mix Modeling Pipeline Skill > Skill by [ara.so](https://ara.so) — Marketing Skills collection. This skill provides expertise in using the Marketing Mix Modeling (MMM) end-to-end pipeline for analyzing marketing channel effectiveness, calculating ROI, and optimizing budget allocation using Python. ## What This Project Does Marketing Mix Modeling decomposes historical sales/revenue into contributions from each marketing channel (TV, search, social, print, OOH) plus baseline organic demand. This pipeline: - Applies **geometric adstock** transformations to model advertising carryover effects - Applies **Hill saturation** curves to model diminishing returns - Fits **OLS regression** to estimate channel contributions - Optimizes budget allocation across channels using `scipy` - Exports results for Power BI dashboard visualization **Key insight**: Adstock must be applied *before* saturation because saturation is level-dependent and saturating raw spend would understate carryover effects. ## 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 ``` **Core dependencies**: - pandas, numpy - statsmodels (OLS regression) - scipy (optimization) - jupyter (notebooks) ## Project Structure ``` data/ dt_simulated_weekly.csv # Raw weekly marketing spend + revenue dt_transformed.csv # After adstock + saturation coefficients.csv # Model coefficients & ROI optimized_budget.csv # Optimization results src/ adstock.py # Geometric adstock function saturation.py # Hill saturation function model.py # OLS regression wrapper optimizer.py # Budget allocation optimizer notebooks/ 01_exploration.ipynb # EDA 02_transformations.ipynb # Apply adstock + saturation 03_model.ipynb # Fit OLS model 04_optimizer.ipynb # Budget optimization ``` ## Key Components ### 1. Geometric Adstock Transformation Models advertising carryover effect where this week's exposure includes decayed impact from previous weeks. ```python # src/adstock.py import numpy as np import pandas as pd def geometric_adstock(x, theta): """ Apply geometric adstock transformation. Parameters: ----------- x : array-like Raw media spend or impressions (time series) theta : float Decay rate (0-1). Higher = longer carryover e.g., 0.5 means 50% of last week's effect carries over Returns: -------- array : Adstocked series """ x = np.array(x) adstocked = np.zeros_like(x, dtype=float) adstocked[0] = x[0] for t in range(1, len(x)): adstocked[t] = x[t] + theta * adstocked[t-1] return adstocked # Usage example import pandas as pd df = pd.read_csv('data/dt_simulated_weekly.csv') df['DATE'] = pd.to_datetime(df['DATE']) # Apply to TV spend with 60% carryover df['tv_adstocked'] = geometric_adstock(df['tv_S'], theta=0.6) # Apply to all channels channels = ['tv_S', 'ooh_S', 'print_S', 'facebook_S', 'search_S'] theta_values = {'tv_S': 0.6, 'ooh_S': 0.5, 'print_S': 0.4, 'facebook_S': 0.3, 'search_S': 0.2} for channel in channels: col_name = f"{channel.replace('_S', '')}_adstocked" df[col_name] = geometric_adstock(df[channel], theta_values[channel]) ``` ### 2. Hill Saturation Transformation Models diminishing returns as spend increases. ```python # src/saturation.py import numpy as np def hill_saturation(x, alpha, gamma): """ Apply Hill saturation curve. Parameters: ----------- x : array-like Adstocked media variable alpha : float Half-saturation point (inflection point of the curve) gamma : float Shape parameter (>0). Higher = steeper curve Returns: -------- array : Saturated series (0-1 scale) """ x = np.array(x) return x**gamma / (alpha**gamma + x**gamma) # Usage example # Apply saturation AFTER adstock alpha_values = {'tv': 0.5, 'ooh': 0.6, 'print': 0.7, 'facebook': 0.4, 'search': 0.3} gamma = 0.5 # Same shape for all channels for channel in ['tv', 'ooh', 'print', 'facebook', 'search']: adstock_col = f"{channel}_adstocked" sat_col = f"{channel}_sat" df[sat_col] = hill_saturation( df[adstock_col], alpha=alpha_values[channel], gamma=gamma ) ``` ### 3. OLS Regression Model Fits revenue decomposition model. ```python # src/model.py or in notebook import statsmodels.api as sm import pandas as pd # Load transformed data df = pd.read_csv('data/dt_transformed.csv') df['DATE'] = pd.to_datetime(df['DATE']) # Clean events column (Robyn data uses string "na" instead of null) df['events'] = df['events'].replace('na', None) # Define model variables media_vars = ['tv_sat', 'ooh_sat', 'print_sat', 'facebook_sat', 'search_sat'] control_vars = ['competitor_sales_B', 'events'] # Create dummy for events df = pd.get_dummies(df, columns=['events'], drop_first=True, dtype=float) event_dummies = [col for col in df.columns if col.startswith('events_')] # Prepare regression X_vars = media_vars + ['competitor_sales_B'] + event_dummies X = df[X_vars].fillna(0) X = sm.add_constant(X) # Add intercept y = df['revenue'] # Fit OLS model = sm.OLS(y, X).fit() print(model.summary()) print(f"\nR-squared: {model.rsquared:.3f}") # Extract coefficients for media channels only coef_df = pd.DataFrame({ 'channel': media_vars, 'coefficient': [model.params[var] for var in media_vars], 'p_value': [model.pvalues[var] for var in media_vars] }) # Calculate proxy ROI (contribution per unit spend) # Need to link back to average spend per channel raw_channels = ['tv_S', 'ooh_S', 'print_S', 'facebook_S', 'search_S'] avg_spend = df[raw_channels].mean() coef_df['avg_weekly_spend'] = avg_spend.values coef_df['avg_contribution'] = coef_df['coefficient'] * df[media_vars].mean().values coef_df['proxy_roi'] = coef_df['avg_contribution'] / coef_df['avg_weekly_spend'] coef_df.to_csv('data/coefficients.csv', index=False) print("\n", coef_df) ``` ### 4. Budget Optimization Maximize revenue under fixed total budget constraint. ```python # src/optimizer.py from scipy.optimize import minimize import numpy as np import pandas as pd def optimize_budget(model, df, media_vars, raw_channels, total_budget): """ Optimize budget allocation across channels. Parameters: ----------- model : statsmodels OLS result Fitted regression model df : DataFrame Original data with transformations media_vars : list Saturated media variable names raw_channels : list Raw spend column names total_budget : float Fixed total weekly budget Returns: -------- dict : Optimized allocation """ # Get transformation parameters (need to store these from step 2) theta_values = {'tv_S': 0.6, 'ooh_S': 0.5, 'print_S': 0.4, 'facebook_S': 0.3, 'search_S': 0.2} alpha_values = {'tv': 0.5, 'ooh': 0.6, 'print': 0.7, 'facebook': 0.4, 'search': 0.3} gamma = 0.5 # Get model coefficients coefs = {var: model.params[var] for var in media_vars} def predict_revenue(budget_allocation): """Predict revenue for given budget allocation""" # budget_allocation is array of spend values per channel revenue = model.params['const'] for i, raw_ch in enumerate(raw_channels): spend = budget_allocation[i] channel_name = raw_ch.replace('_S', '') # Apply adstock (simplified - using only current spend) # In practice, need full time series context adstocked = spend # Simplified # Apply saturation alpha = alpha_values[channel_name] saturated = adstocked**gamma / (alpha**gamma + adstocked**gamma) # Get contribution sat_var = f"{channel_name}_sat" revenue += coefs[sat_var] * saturated return revenue # Objective: maximize revenue = minimize negative revenue def objective(budget_allocation): return -predict_revenue(budget_allocation) # Constraint: sum of budgets = total_budget constraints = {'type': 'eq', 'fun': lambda x: np.sum(x) - total_budget} # Bounds: each channel >= 0 bounds = [(0, total_budget) for _ in raw_channels] # Initial guess: equal allocation x0 = np.array([total_budget / len(raw_channels)] * len(raw_channels)) # Optimize result = minimize( objective, x0, method='SLSQP', bounds=bounds, constraints=constraints, options={'maxiter': 1000} ) if not result.success: print(f"Warning: optimization did not converge - {result.message}") # Format results current_avg = df[raw_channels].mean().values optimized = result.x results_df = pd.DataFrame({ 'channel': [ch.replace('_S', '') for ch in raw_channels], 'current_budget': current_avg, 'optimized_budget': optimized, 'change_pct': ((optimized - current_avg) / current_avg * 100) }) current_revenue = predict_revenue(current_avg) optimized_revenue = predict_revenue(optimized) uplift_pct = (optimized_revenue - current_revenue) / current_revenue * 100 print(f"Current revenue: ${current_revenue:,.0f}") print(f"Optimized revenue: ${optimized_revenue:,.0f}") print(f"Uplift: +{uplift_pct:.1f}%\n") print(results_df) results_df.to_csv('data/optimized_budget.csv', index=False) return results_df # Usage total_weekly_budget = df[raw_channels].sum(axis=1).mean() opt_results = optimize_budget( model=model, df=df, media_vars=media_vars, raw_channels=raw_channels, total_budget=total_weekly_budget ) ``` ## Complete Workflow ### Step 1: Load and Clean Data ```python import pandas as pd import numpy as np df = pd.read_csv('data/dt_simulated_weekly.csv') df['DATE'] = pd.to_datetime(df['DATE']) df['events'] = df['events'].replace('na', None) # Verify data print(df.head()) print(f"Date range: {df['DATE'].min()} to {df['DATE'].max()}") print(f"Rows: {len(df)}") ``` ### Step 2: Apply Transformations ```python from src.adstock import geometric_adstock from src.saturation import hill_saturation # Define channels and parameters channels = ['tv_S', 'ooh_S', 'print_S', 'facebook_S', 'search_S'] theta_map = {'tv_S': 0.6, 'ooh_S': 0.5, 'print_S': 0.4, 'facebook_S': 0.3, 'search_S': 0.2} alpha_map = {'tv': 0.5, 'ooh': 0.6, 'print': 0.7, 'facebook': 0.4, 'search': 0.3} gamma = 0.5 # Apply transformations (order matters: adstock first!) for raw_channel in channels: channel_name = raw_channel.replace('_S', '') # Adstock adstock_col = f"{channel_name}_adstocked" df[adstock_col] = geometric_adstock(df[raw_channel], theta_map[raw_channel]) # Saturation sat_col = f"{channel_name}_sat" df[sat_col] = hill_saturation(df[adstock_col], alpha_map[channel_name], gamma) # Save df.to_csv('data/dt_transformed.csv', index=False)
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub