Budget Allocation with PyMC-Marketing#

The purpose of this notebook is to explore the recently included function in the PyMC-Marketing library that focuses on budget allocation. This function’s underpinnings are based on the methodologies inspired by Bolt work in his article, “Budgeting with Bayesian Models”.

Prerequisite Knowledge#

The notebook assumes the reader has knowledge of the essential functionalities of PyMC-Marketing. If one is unfamiliar, the “MMM Example Notebook” serves as an excellent starting point, offering a comprehensive introduction to media mix models in this context.

Context#

The emphasis of this notebook is on enhancing marketing budgets. Contrarily to broader issues addressed in prior notebooks, our primary aim here is to unravel specialized knowledge on budget allocation tactics using the functionality.

Objectives#

To elucidate more efficient ways of resource allocation across diverse media channels. To deliver data-driven, actionable insights for budgeting decisions.

Introducing the budget allocator#

This notebook instigates an examination of the function within the PyMC-Marketing library, which addresses these challenges using Bayesian models. The function intends to provide:

  1. Quantitative measures of the effectiveness of different media channels.

  2. Probabilistic ROI estimates under a range of budget scenarios.

What to Anticipate#

Upon completing this notebook, readers should get a comprehensive understanding of the budget allocation function. They will then be equipped to incorporate this analytic tool into their marketing analytics routines for data-driven decision-making.

Installing PyMC-Marketing#

Before delving into the specifics of budget allocation, the initial step is to install the PyMC-Marketing library and ascertain its version. This step will confirm support for the budget allocation function. The following pip command can be run on your Jupyter Notebook:

Basic Setup#

Like previous notebooks revolving around PyMC-Marketing, this relies on a specific library set. Here are the requisite imports necessary for executing the provided code snippets subsequently.

import warnings

import arviz as az
import matplotlib.pyplot as plt
import numpy as np

from pymc_marketing.mmm.budget_optimizer import calculate_expected_contribution
from pymc_marketing.mmm.delayed_saturated_mmm import DelayedSaturatedMMM

warnings.filterwarnings("ignore")

az.style.use("arviz-darkgrid")
plt.rcParams["figure.figsize"] = [12, 7]
plt.rcParams["figure.dpi"] = 100

%load_ext autoreload
%autoreload 2
%config InlineBackend.figure_format = "retina"

These imports and configurations form the fundamental setup necessary for the entire span of this notebook.

The expectation is that a model has already been trained using the functionalities provided in prior versions of the PyMC-Marketing library. Thus, the data generation and training processes will be replicated in a different notebook. Those unfamiliar with these procedures are advised to refer to the “MMM Example Notebook.”

Employing ModelBuilder: A Feature in PyMC-Marketing#

The ModelBuilder feature, introduced in version 0.2.0 of PyMC-Marketing, empowers users to easily save and load pre-trained models. The capability to load a pre-existing model is especially advantageous for accelerating analyses, mainly when dealing with expansive data sets or intricate models.

Saving Model#

Once the model has been trained, it is easy to save for later use. An example of the “.save” method is demonstrated below to store the model at a designated location.

mmm.save("/content/budget_optimizer_model.nc")

Loading a Pre-Trained Model#

To utilize a saved model, load it into a new instance of the DelayedSaturatedMMM class using the load method below.

name = "/pymc-marketing/data/budget_optimizer_model.nc"
mmm = DelayedSaturatedMMM.load(name)

For more details on the save() and load() methods, consult the pymc-marketing documentation on Model Deployment.

Problem Statement#

Before jumping into the data, let’s first define the business problem we are trying to solve.In a progressively competitive scenario, marketers are tasked with distributing a predetermined marketing budget across various channels for optimizing Return on Investment (ROI). Consider a forthcoming quarter wherein a marketing team must decide the division of its operations between two advertising channels, represented as x1 and x2. These could effectively symbolize any medium, such as TV, digital advertising, print, etc.

The task lies in making decisions that invoke data, comply with factual evidence, and align with business logic. For instance, how can one incorporate prior information like budget restrictions, platform trends, constraints, or even distinctive features of each channel into the decision-making process?

Introducing Budget Allocation Function#

The updated budget allocation function in PyMC-Marketing aims to tackle this issue by offering a Bayesian framework for optimal allocation. This enables marketers to:

  • Integrate the outcomes of Media Mix Modeling (MMM), quantifying each channel’s effectiveness in metrics like ROI, incremental sales, etc.

  • Merge this empirical data with prior business knowledge and logic for making holistic and robust decisions.

By utilizing this function, marketers can guarantee that the budget spread not only obeys the mathematical rigor furnished by the MMM outcomes but also incorporates business-specific factors, thereby achieving a balanced and optimized budget plan.

Getting started#

Media Mix Modeling (MMM) acts as a dependable method to estimate the contribution of each channel (e.g., x1, x2) to a target variable like sales or any variable. The function plot_direct_contribution_curves() allows for visualization of this direct channel impact. However, it is crucial to remember that this only unveils the “observable space” for values of X (spend) and Y (contribution).

response_curve_fig = mmm.plot_direct_contribution_curves()
../../_images/a8b466a0e838abcad1622f2be18e9b9da7c66ecda93b5da5a7121e04db948c7f.png

The observable space only encompasses our data points and does not illustrate what transpires beyond those points. As a result, it is not assured that the maximum contribution point for each channel lies within this observable range.

Understanding Saturation Functions: Sigmoid and Michaelis-Menten#

What do we mean by saturation functions? We assume the effect of spend on sales is not linear and saturates at some point. Two prevalent functions deployed to comprehend and estimate the saturation effects in advertising channels are the sigmoid and the Michaelis-Menten functions.

Sigmoid Function#

The sigmoid function is formulated as: $\( \frac{\alpha - \alpha \cdot \exp(-\lambda x)}{1 + \exp(-\lambda x)} \)$ Key Elements:

  • α (Alpha): Denotes the Asymptotic Maximum or Ceiling Value. It is the point that the function approaches as the input x becomes immense.

  • λ (Lambda): Influences the steepness of the curve. A more substantial value of λ renders the curve steeper, while a lesser value makes it more even-paced.

Michaelis-Menten Function#

The Michaelis-Menten function is formulated as: $\( \frac{\alpha \times x}{\lambda + x} \)$ Key Elements:

  • α (Alpha or Vmax): It represents the maximum contribution (y) a channel can make, also recognized as the plateau point.

  • λ (k): Denotes the elbow on the function in x, signifying the moment when the curve adjusts its direction.

Which Function to Use?#

The preference between the sigmoid and Michaelis-Menten functions ought to be steered by the data’s goodness of fit. But it really comes down to your assumptions about where the peak might be and the speed at which it saturates the curve.

To determine which function fits your points better visually, specify the parameter show_fit=True in the same preceding function to obtain a plot.

sigmoid_response_curve_fig = mmm.plot_direct_contribution_curves(show_fit=True)
../../_images/658e879bdd2418bf8a2505ed7205b3d77aaec3d86b2255720cbfeea074dbcd89.png

Handling non-fit errors:#

In some isolated cases, when X is very small the scipy optimizer may not find the optimal setting. In those cases, you can use the functions separately by directly importing the module from utils.

from pymc_marketing.mmm.utils import (
        estimate_menten_parameters,
        estimate_sigmoid_parameters,
    )

estimate_menten_parameters(x=df.X.to_numpy(), y=df.y.to_numpy())
estimate_sigmoid_parameters(x=df.X.to_numpy(), y=df.y.to_numpy())

Use the parameter lam_initial_estimate or alpha_initial_estimate to play with the initial estimates given to the optimizer, better prior initial values should make the optimizer find the answer quickly. Otherwise, you can use the maxfev parameter to increase the number of iterations by curve_fit before giving up.

The plot by default shows the sigmoid function fit with the quantiles estimated by the posterior distribution of the channel. If you need to change the fit method, you can use the method parameter.

sigmoid_response_curve_fig = mmm.plot_direct_contribution_curves(
    show_fit=True, method="michaelis-menten"
)
../../_images/5a3e34b36f66663eb9827403f845a579547e32e91c2e5792c9b4e3b50acf6fc2.png

Additionally, the plot will consistently return the value of alpha (Ceiling). This means you can visualize the maximum possible estimated value that your target variable will achieve.

Curve Fit#

Libraries such as Scipy offer functionalities to fit these functions to the data and extract these statistics. We use curve_fit from Scipy to ascertain the optimal fit for each saturation function, the method used by them is based on least-squares.

Estimating Curve Fit parameters#

Estimating curve-fit parameters, particularly α and λ, is pivotal for optimizing marketing budget allocation. These parameters determine the shape of the saturation curves for each marketing channel and function as the input to the budget_allocator function.

The PyMC-Marketing library proffers a straightforward method to calculate these parameters for each saturation curve. You can employ the following code to estimate parameters for the Sigmoid and Michaelis-Menten curves.

sigmoid_params = mmm.compute_channel_curve_optimization_parameters_original_scale(
    method="sigmoid"
)

menten_params = mmm.compute_channel_curve_optimization_parameters_original_scale(
    method="michaelis-menten"
)

What You Get#

After running the code above, you will receive a dictionary containing α and λ for each channel. For instance, for the Sigmoid & Michaelis Menten curve, you might get:

sigmoid_params, menten_params
({'x1': array([2.40931096, 6.11360947]),
  'x2': array([1.92067375, 3.02135665])},
 {'x1': array([3.1334831 , 0.24731411]),
  'x2': array([2.6305693 , 0.50773317])})

Once these parameters are obtained, if desired, you can recreate the curves for each channel independently. More crucially, these parameter values are indispensable when using the budget_allocator function, which leverages this information to optimize your marketing budget across distinct channels. This section is fundamental to budget optimization.

Example Use-Cases#

The budget_allocator function within PyMC-Marketing boasts a myriad of applications that can solve various business predicaments. Here, we present five critical use cases that exemplify its utility in real-world marketing scenarios.

What are we optimizing?#

Before jumping into the examples, we need to understand the basis of our optimizer.

We aim to optimize the allocation of budgets across multiple channels to maximize the overall contribution to key performance indicators (KPIs), such as sales or conversions. Each channel has its own sigmoid or michaelis-menten curve, representing the relationship between the amount spent and the resultant performance.

These curves vary in characteristics: some channels saturate quickly, meaning that additional spending yields diminishing returns, while others may offer more linear growth in contribution with increased spending.

To solve this optimization problem, we employ the Sequential Least Squares Quadratic Programming (SLSQP) algorithm, a gradient-based optimization technique. SLSQP is well-suited for this application as it allows for the imposition of both equality and inequality constraints, ensuring that the budget allocation adheres to business rules or limitations.

The algorithm works by iteratively approximating the objective function and constraints using quadratic functions and solving the resulting sub-problems to find a local minimum. This enables us to effectively navigate the multidimensional space of budget allocations to find the most efficient distribution of resources.

The optimizer aims to maximize the total contribution from all channels while adhering to the following constraints:

  1. Budget Limitations: The total spending across all channels should not exceed the overall marketing budget.

  2. Channel-specific Constraints: Some channels may have minimum or maximum spending limits.

By leveraging the SLSQP algorithm, we can optimize the multi-channel budget allocation in a rigorous, mathematically sound manner, ensuring that we get the highest possible return on investment.

Maximizing Contribution#

Assume you’re managing the marketing for a retail company with a substantial budget to allocate for advertising across multiple channels. Suppose you’re already apportioning funds to channels x1 and x2. Still, you’re contemplating ways to optimize the forthcoming quarter’s outlay to maximize the overall contribution.

Without, you might have considered scattering your money linearly without an MMM model - equal investments in each channel. However, you wish to explore better alternatives now that you possess an MMM model. Given that you lack prior knowledge, you impose the same restrictions on both channels. They must each expend a minimum of 1 million euros and no more than 5 million, equating to your total budget.

total_budget = 5  # Imagine is 5K or 5M
# Define your channels
channels = ["x1", "x2"]
# The initial split per channel
budget_per_channel = total_budget / len(channels)
# Initial budget per channel as dictionary.
initial_budget_dict = {channel: budget_per_channel for channel in channels}
# bounds for each channel
min_budget, max_budget = 1, 5
budget_bounds = {channel: [min_budget, max_budget] for channel in channels}

We can use our function and see the results with this information saved.

result_sigmoid = mmm.optimize_channel_budget_for_maximum_contribution(
    method="sigmoid",  # define saturation function
    total_budget=total_budget,
    parameters=sigmoid_params,
    budget_bounds=budget_bounds,
)

result_sigmoid
estimated_contribution optimal_budget
x1 2.409210 1.762903
x2 1.920457 3.237097
total 4.329667 5.000000

In the same way, you can perform this optimization based on the michaelis-menten function and the estimated curve according to the best parameters found.

result_menten = mmm.optimize_channel_budget_for_maximum_contribution(
    method="michaelis-menten",
    total_budget=total_budget,
    parameters=menten_params,
    budget_bounds=budget_bounds,
)
result_menten
estimated_contribution optimal_budget
x1 2.821852 2.239459
x2 2.221905 2.760541
total 5.043757 5.000000

These results are expected based on the estimates from the curve and our estimated average contribution from the posterior distribution.

However, based on our main assumptions, how can we ensure this result is optimal? How can we compare this outcome to what we would initially have if we followed our first setup?

# Use the function `calculate_expected_contribution` to estimate
# the contribution of your initial budget based on the curve parameters.
initial_contribution = calculate_expected_contribution(
    method="sigmoid", parameters=sigmoid_params, budget=initial_budget_dict
)

# Initial budget & contribution dictionary
initial_scenario = {
    "initial_contribution": initial_contribution,
    "initial_budget": initial_budget_dict,
}

This information will allow you to compare the optimization results against what could have been your initial configuration and budget distribution.

If you want a visualization, you can create a barplot to see the contribution estimate you could obtain with the new budget and the initial one. The plot has confidence intervals, which come from the posterior distribution; these are, by default, the 95th and 5th quantiles. The estimated contribution could be within said range and not be exactly the mean value.

# Use the function `compare_budget_scenearios` to validate
# The estimated contribution from one scenario agains the other
figure_ = mmm.plot_budget_scenearios(
    base_data=initial_scenario, method="sigmoid", scenarios_data=[result_sigmoid]
)
../../_images/9f91e088d6228e9fd6dd8f923f309c045d8b6f350ec28fb66d2e30b34f00debd.png

While our budget distribution changes, the contribution estimate remains the same for this budget. Could we be spending more than needed? Furthermore, how can we identify the optimal spent amount if this situation arises?

One approach to explore this could be to create various scenarios and observe how our estimated contribution fluctuates according to each scenario.

Creating Budget Scenarios#

Envision the subsequent situation: You’re managing marketing operations for a rapidly growing retail company. The management team has allocated a considerable advertising budget and anticipates substantial results in the next quarter. However, given the uncertainty in economic trends, you are tasked with designing a budget allocation strategy capable of accommodating various scenarios.

Before the advent of a robust MMM model, your approach might have been simplistic yet naive: distribute the funds equally across the two primary channels, x1 and x2. This would result in a linear, evenly split distribution of 2.5 million euros each, given a total budget of 5 million euros.

Nevertheless, with the MMM model now at your disposal, you realize a more sophisticated approach is feasible. You’re eager to investigate how this budget could be optimized across multiple scenarios:

  1. Status Quo Scenario: What if the market stays stable? What is the best allocation?

  2. Growth Scenario: What if the market suddenly becomes more favorable? How should the extra budget be allocated?

  3. Recession Scenario: What if there is a market downturn and the budget gets cut by 40%?

Given that you are treading into uncertain waters, you set certain constraints. Each channel must have a minimum spending of 1 million euros, ensuring base-level visibility. The maximum cap is ±5 million euros (depending on your scenario), respecting the total budget.

# Initialize two variables to save the results and base conditions for each scenario.
scenarios_result = []
scenarios_base = []

for scenario in np.array([0.6, 0.8, 1.2, 1.8]):
    scenarios_result.append(
        mmm.optimize_channel_budget_for_maximum_contribution(
            method="sigmoid",  # define saturation function
            total_budget=total_budget * scenario,
            parameters=sigmoid_params,
            budget_bounds={
                channel: [1, total_budget * scenario] for channel in channels
            },
        ).to_dict()
    )

    scenarios_base.append(
        {
            "initial_contribution": calculate_expected_contribution(
                method="sigmoid",  # define saturation function
                parameters=sigmoid_params,
                budget={
                    channel: total_budget * scenario / len(channels)
                    for channel in channels
                },
            ),
            "initial_budget": {
                channel: total_budget * scenario / len(channels) for channel in channels
            },
        }
    )

We now have two lists containing the expected results with and without model optimization. This will return the same plot as before but based on each of the scenarios.

# Use the function `compare_budget_scenearios` to validate
# The estimated contribution from one scenario agains the other
_figure = mmm.plot_budget_scenearios(
    base_data=initial_scenario, method="sigmoid", scenarios_data=scenarios_result
)
../../_images/83d16b0eb5fe4e594b759b4185e6eaece3f20138a64c1e855dda6421a438b572.png

The graph indicates that boosting the budget beyond the levels of scenario number 3 induces extremely marginal changes in the potential outcome. Therefore, one can use the budget detailed in scenario three as a cap for our budget.

However, is this the best method to invest our resources? So far, we have considered general constraints for each channel. However, since our curve saturates and beyond a specific point, it does not significantly elevate its contribution. Wouldn’t it be crucial to incorporate these limitations?

Adding Business or Channel Constraints#

There may be instances where, despite the recommendation to invest more in a particular channel than another, it may not be feasible.

Consider this: Given your current budget, you have already maxed out the number of people you can reach within a specific platform. Therefore, further spending will only increase the frequency without adding new reach. In this scenario, does it make sense to raise the budget? This is a classic example of budget limitations based on platform restrictions.

In such cases, a dictionary can be created with limits for each channel, and the optimization function can be reapplied.

platform_base_optimization = mmm.optimize_channel_budget_for_maximum_contribution(
    method="sigmoid",
    total_budget=total_budget,
    parameters=sigmoid_params,
    budget_bounds={"x1": [0, 1.5], "x2": [0, 1.5]},
)

platform_base_optimization
estimated_contribution optimal_budget
x1 2.408810 1.5
x2 1.879786 1.5
total 4.288595 3.0

Utilizing bounds is crucial, as both saturation curves (Sigmoid, Menten) are asymptotic. Therefore, without a constraint on your factors, the optimizer will consistently seek to use 100% of the funds because it will always observe an increase within y (target).

If you have yet to notice, this is why, in the previous example, the optimizer always utilized the whole budget in all scenarios. However, should you provide these limits, the optimizer can verify whether it is necessary to use your entire budget.

Let us examine the subsequent plot closely!

sigmoid_response_curve_fig = mmm.plot_direct_contribution_curves(
    show_fit=True, method="sigmoid", xlim_max=3
)
../../_images/7e50155e38c2157ed3d7697b38a2d9511e5bcebe69dbc7703a7d4253420c9c84.png

Although in principle we assign a limit of 1.5 spending for each channel, we can observe that at least for x1 after spending more than 1.2, we enter the plateau effect of the curve where we get almost zero return in y for each unit extra of x.

platform_base_optimization = mmm.optimize_channel_budget_for_maximum_contribution(
    method="sigmoid",
    total_budget=total_budget,
    parameters=sigmoid_params,
    budget_bounds={"x1": [0, 1.2], "x2": [0, 1.5]},
)

platform_base_optimization
estimated_contribution optimal_budget
x1 2.406174 1.2
x2 1.879786 1.5
total 4.285960 2.7

The end result is a total budget of 2.7 units on X. Is this our most optimal result? How to validate?

_figure = mmm.plot_budget_scenearios(
    base_data=initial_scenario,
    method="sigmoid",
    scenarios_data=[platform_base_optimization],
)
../../_images/fc442569993289987e8fcc13320c981b2922934b2624bb37f7e81cac1adc4630.png

Now, the result is considerably more explicit. Using nearly half of the budget with the current distribution configuration could yield the same number of outcomes as our initial setup, which dispersed the budget evenly across all channels. By default, if you need more prior knowledge about the limits, we recommend using alpha (the plateau) as the limit for each channel.

Benefits and Limitations#

In marketing analytics, curve-fitted Media Mix Models (MMMs) provide enriching insights by simplifying intricate systems, facilitating optimization, aiding scenario planning, and delivering quantifiable metrics for strategy evaluation. Each of these advantages presents compelling reasons to incorporate such models.

Nevertheless, it is pivotal to acknowledge that these models do possess their own set of limitations. The primary ones are the assumptions of time invariance and generalized behavior, signifying an incomplete comprehension of market dynamics. The no-impact variance assumptions are not specific to the curves, as the model does not account for these effects. Hence, curves should always be carefully considered.

Other methods to explore#

Even if the method is promising, use other optimization options which includes the full posterior could be a powerful and interesting solution as it’s described on the following blog “Using bayesian decision making to optimize supply chains”

The current methodology is similar to the ones used on other libraries as Robyn from Meta and Google Lightweight from Google. You can explore the solutions and compare if needed.

Conclusion#

MMM models and methodologies used here are designed to bridge the gap between theoretical rigor and actionable marketing insights. They represent a significant stride towards a more data-driven, analytical approach to marketing budget allocation, which could change how organizations invest in customer acquisition and retention.

Although it is a promising tool, it is essential to highlight that this methodology and software release is still experimental. Like any emerging technology, it comes with inherent limitations and assumptions that users should be aware of. The models can offer actionable insights, but they should be cautiously used and in tandem with various forms of analysis. Context is crucial, and while models aim to encapsulate general trends, they might not account for all nuances.

Consequently, your engagements, feedback, and thoughts are not merely welcomed but actively solicited to make this tool as practical and universally applicable as possible.