deepcausalmmm.postprocess.optimization
Budget Optimization for Marketing Mix Modeling.
This module provides budget optimization capabilities using response curves from DeepCausalMMM. It uses constrained optimization to find the optimal allocation of marketing budget across channels to maximize predicted response.
The optimizer uses Hill equation saturation curves fitted by ResponseCurveFit to predict channel responses at different spend levels and finds the allocation that maximizes total response subject to budget and business constraints.
Functions
|
Convenience function to optimize budget directly from curve parameters DataFrame. |
Classes
|
Optimize marketing budget allocation using response curves. |
|
Result from budget optimization. |
- class deepcausalmmm.postprocess.optimization.OptimizationResult(success: bool, allocation: Dict[str, float], predicted_response: float, by_channel: DataFrame, message: str = '', method: str = 'trust-constr')[source]
Result from budget optimization.
- by_channel
Detailed results by channel with spend, response, and ROI
- Type:
pd.DataFrame
Examples
>>> result = optimizer.optimize() >>> if result.success: ... print(f"Optimal allocation: {result.allocation}") ... print(f"Expected response: {result.predicted_response:,.0f}") ... print(result.by_channel)
- class deepcausalmmm.postprocess.optimization.BudgetOptimizer(budget: float, channels: List[str], response_curves: Dict[str, Dict], *, num_weeks: int = 52, method: str = 'trust-constr')[source]
Optimize marketing budget allocation using response curves.
Uses constrained optimization (trust-constr, SLSQP, or differential evolution) with Hill transformation curves from ResponseCurveFit to find optimal spend allocation that maximizes total response subject to business constraints.
- Parameters:
budget (float) – Total budget to allocate across all channels
channels (List[str]) – List of channel names to include in optimization
response_curves (Dict[str, Dict]) – Response curve parameters by channel from ResponseCurveFit. Each channel dict should contain: ‘top’, ‘bottom’, ‘saturation’, ‘slope’
num_weeks (int, default=52) – Number of weeks for planning horizon (annual by default)
method (str, default='trust-constr') – Optimization method: ‘trust-constr’, ‘SLSQP’, ‘differential_evolution’, ‘hybrid’
- constraints_df
DataFrame with channel-level constraints (lower, upper bounds)
- Type:
pd.DataFrame or None
Examples
>>> # After fitting response curves with ResponseCurveFit >>> curves = { ... 'TV': {'top': 1000000, 'bottom': 0, 'saturation': 50000, 'slope': 1.5}, ... 'Search': {'top': 800000, 'bottom': 0, 'saturation': 30000, 'slope': 2.0}, ... 'Social': {'top': 600000, 'bottom': 0, 'saturation': 20000, 'slope': 1.8} ... } >>> >>> optimizer = BudgetOptimizer( ... budget=1000000, ... channels=['TV', 'Search', 'Social'], ... response_curves=curves, ... num_weeks=52 ... ) >>> >>> # Optional: Set channel-specific constraints >>> optimizer.set_constraints({ ... 'TV': {'lower': 50000, 'upper': 500000}, ... 'Search': {'lower': 100000, 'upper': 400000} ... }) >>> >>> # Run optimization >>> result = optimizer.optimize() >>> >>> # View results >>> if result.success: ... print("Optimal Allocation:") ... for channel, spend in result.allocation.items(): ... print(f" {channel}: ${spend:,.0f}") ... print(f"\nTotal Response: {result.predicted_response:,.0f}") ... print(f"\nDetailed Results:\n{result.by_channel}")
Notes
The optimizer maximizes total response using the Hill equation:
\[response = bottom + (top - bottom) * \frac{spend^{slope}}{saturation^{slope} + spend^{slope}}\]Where: - top: Maximum response (saturation level) - bottom: Minimum response (typically 0) - saturation: Spend level at half-maximum response - slope: Steepness of the response curve
The optimization problem is:
\[ \begin{align}\begin{aligned}\max_{x_1, ..., x_n} \sum_{i=1}^{n} response_i(x_i)\\s.t. \sum_{i=1}^{n} x_i = budget\\lower_i \leq x_i \leq upper_i \quad \forall i\end{aligned}\end{align} \]- __init__(budget: float, channels: List[str], response_curves: Dict[str, Dict], *, num_weeks: int = 52, method: str = 'trust-constr')[source]
Initialize BudgetOptimizer with budget, channels, and response curves.
- set_constraints(constraints: Dict[str, Dict[str, float]]) None[source]
Set spend constraints for channels.
- Parameters:
constraints (Dict[str, Dict[str, float]]) – Channel constraints: {‘channel’: {‘lower’: min_spend, ‘upper’: max_spend}}
Examples
>>> optimizer.set_constraints({ ... 'TV': {'lower': 50000, 'upper': 500000}, ... 'Search': {'lower': 100000, 'upper': 400000}, ... 'Social': {'lower': 25000, 'upper': 300000} ... })
Notes
Channels not specified in constraints get default bounds: [0, budget]
Upper bounds are automatically capped at total budget
If lower > upper, lower is reset to 0
Upper bounds cannot be 0 (would make channel unusable)
- optimize() OptimizationResult[source]
Run optimization to find optimal budget allocation.
- Returns:
Optimization results including allocation, predicted response, and details
- Return type:
Examples
>>> result = optimizer.optimize() >>> if result.success: ... print("Optimization successful!") ... print(f"Predicted response: {result.predicted_response:,.0f}") ... for channel, spend in result.allocation.items(): ... roi = result.by_channel[result.by_channel['channel']==channel]['roi'].iloc[0] ... print(f"{channel}: ${spend:,.0f} (ROI: {roi:.2f})") ... else: ... print(f"Optimization failed: {result.message}")
- compare_scenarios(scenarios: Dict[str, Dict[str, float]]) DataFrame[source]
Compare different budget allocation scenarios.
- Parameters:
scenarios (Dict[str, Dict[str, float]]) – Dictionary of scenarios: {‘scenario_name’: {‘channel’: spend, …}}
- Returns:
Comparison of scenarios with predicted responses and ROIs
- Return type:
pd.DataFrame
Examples
>>> scenarios = { ... 'Current': {'TV': 400000, 'Search': 350000, 'Social': 250000}, ... 'Optimized': result.allocation, ... 'Heavy TV': {'TV': 600000, 'Search': 250000, 'Social': 150000} ... } >>> comparison = optimizer.compare_scenarios(scenarios) >>> print(comparison)
- deepcausalmmm.postprocess.optimization.optimize_budget_from_curves(budget: float, curve_params: DataFrame, *, channel_col: str = 'channel', num_weeks: int = 52, constraints: Dict[str, Dict[str, float]] | None = None, method: str = 'trust-constr') OptimizationResult[source]
Convenience function to optimize budget directly from curve parameters DataFrame.
This function is useful when you have response curve parameters in a DataFrame (e.g., from ResponseCurveFit fitted on multiple channels) and want to quickly run optimization without manually setting up the BudgetOptimizer.
- Parameters:
budget (float) – Total budget to allocate
curve_params (pd.DataFrame) – DataFrame with response curve parameters. Required columns: channel, top, bottom, saturation, slope
channel_col (str, default='channel') – Name of the channel column in curve_params
num_weeks (int, default=52) – Number of weeks for planning horizon
constraints (Dict[str, Dict[str, float]], optional) – Channel-specific constraints: {‘channel’: {‘lower’: min, ‘upper’: max}}
method (str, default='trust-constr') – Optimization method
- Returns:
Optimization results
- Return type:
Examples
>>> # After fitting curves for multiple channels >>> curves_df = pd.DataFrame({ ... 'channel': ['TV', 'Search', 'Social'], ... 'top': [1000000, 800000, 600000], ... 'bottom': [0, 0, 0], ... 'saturation': [50000, 30000, 20000], ... 'slope': [1.5, 2.0, 1.8] ... }) >>> >>> result = optimize_budget_from_curves( ... budget=1000000, ... curve_params=curves_df, ... constraints={'TV': {'lower': 100000, 'upper': 600000}} ... ) >>> print(result.allocation)