Budget Allocation Risk Assessment with PyMC-Marketing#

This notebook is centered around evaluating the risks tied to different budget allocations across various marketing channels. You’ll discover how to create an optimal budget allocation that aligns with your specific risk tolerance. This knowledge will empower you to make well-informed decisions regarding your budget distribution.

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.

Expected Outcomes#

Upon completion of this notebook, readers will acquire a comprehensive understanding of how to evaluate the risks associated with various budget allocations and how to develop an optimal budget allocation based on specified risk tolerance criteria.

Preliminary Setup#

Consistent with previous notebooks in the PyMC-Marketing series, this document relies on a specific set of libraries. Below are the necessary imports required for executing the code snippets presented hereafter.

import warnings

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

from pymc_marketing.mmm import MMM

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"

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.”

Loading a Pre-Trained Model#

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

mmm = MMM.load("model.nc")

Formulating the Budget Allocation Challenge#

As in earlier notebooks, it is essential to delineate the budget allocation challenge. Specifically, we must define the duration of our budget allocation and the permissible expenditure per time unit. Our model utilizes weekly data; therefore, we will maintain the same temporal granularity.

In this example, we aim to distribute a budget across two channels over the course of eight weeks, with a weekly budget of 3 Million. Consequently, the total budget available for allocation amounts to 24 Million.

num_periods = (
    8  # Number of time periods to allocate the budget (weeks for this example)
)
time_unit_budget = 3  # Imagine is 3K or 3M (per week in this case)

# Define your channels
channels = ["x1", "x2"]

print(f"Total budget to allocate: {num_periods * time_unit_budget}M")
Total budget to allocate: 24M

Based on our intuition, we were thinking of distributing this million into 2.5 Million for Google (\(x2\)) and 0.5 Million for Facebook (\(x1\)). Using this allocation, we can compute the response distribution and plot it.

initial_allocation = {"x1": 0.5, "x2": 2.5}
initial_posterior_response = mmm.sample_response_distribution(
    allocation_strategy=initial_allocation,
    time_granularity="weekly",
    num_periods=num_periods,
    noise_level=0.01,
)

fig, ax = mmm.plot_budget_allocation(
    samples=initial_posterior_response, figsize=(12, 8)
)
ax.set_title("Response vs spent per channel", fontsize=18, fontweight="bold");
Sampling: [y]
../../_images/0fbe865b4717651d25c3995d428c9fda1d9107f97e0157f9b8d55cc3675dc774.png
# Plot the response distribution by Arviz
az.plot_posterior(
    initial_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    hdi_prob=0.95,
)
plt.title("Response Distribution at 95% HDI (highest density interval)");
../../_images/b70dca065cad6788d72514e73b1384ed8c8495be084b475f1aab0941a0f7e81d.png

This is great, apparently we could get 4.4M units sold given our 3M budget which is mostly allocated to \(X2\).

Could we do better? The usual approach is to allocate the budget to maximize the response. We can use the optimize_budget method to do so, here we will compute the response given several budget combinations, and we’ll prefer the one that maximizes the response. It’s important to note that this example doesn’t use any bounds, or constraints, so the optimizer will seek to use the entire budget.

model_granularity = "weekly"
allocation_strategy, optimization_result = mmm.optimize_budget(
    budget=time_unit_budget,
    num_periods=num_periods,
)

naive_posterior_response = mmm.sample_response_distribution(
    allocation_strategy=allocation_strategy,
    time_granularity=model_granularity,
    num_periods=num_periods,
    noise_level=0.01,
)

print(f"Budget allocation: {naive_posterior_response.allocation.to_numpy()}")
print(
    f"Total Allocated Budget: {np.sum(naive_posterior_response.allocation.to_numpy())}M"
)

fig, ax = mmm.plot_budget_allocation(samples=naive_posterior_response, figsize=(12, 8))
ax.set_title("Response vs spent per channel", fontsize=18, fontweight="bold");
Sampling: [y]
Budget allocation: [1.56120876 1.43879124]
Total Allocated Budget: 3.0M
../../_images/4a1c2014d0dc37a136fbf34628c5bf8af3a85ab63d0d0f5d6fff9d88636a67a9.png

Great! Looks like we could get 5M units sold given our 3M budget, meaning the optimizer found a better allocation which maximizes the response for the same budget. We could follow the same approach and plot the two distributions to compare them.

# Plot the response distribution by Arviz
fig, ax = plt.subplots()
az.plot_posterior(
    naive_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    hdi_prob=0.95,
    color="blue",
    label="Optimized allocation",
    ax=ax,
)
az.plot_posterior(
    initial_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    hdi_prob=0.95,
    color="red",
    label="Guessed allocation",
    ax=ax,
)
plt.title("Response Distribution at 95% HDI (highest density interval)")
plt.legend()
plt.show()
../../_images/ff2b4bb208bb92302474a7a104272c249c87aa1c23ab282afd5f5893671bccf0.png

This makes everything clear, the optimized allocation has a higher mean. But looks like the optimized allocation has a higher risk, as the distribution is wider, respect to the initial guessed allocation.

Based on this, using the optimized allocation its very likely to get a respose of 5M units sold but also the budget could bring as low as 4.5M units sold or as big as 5.6M units sold. On the other hand, using the guessed allocation its very likely to get a respose of 4.4M units sold but also the budget could bring as low as 3.7M units sold or as big as 5.4M units sold.

Here the answer looks clear, the optimization brings a higher mean, but with lower risk. But that would be always the case?

During this notebook will give you the tools to answer this question. If you face a situation where the best bet is not the safer bet, which one would you prefer? Higher mean, but with more risk? Or lower mean, but with less risk? A safer bet or a riskier bet?

This is where risk assessment comes into play, we can use different risk assessment criteria to help us decide which allocation is better based on our risk tolerance.

Introduction to Risk Assessment#

The budget_optimizer module encompasses various risk assessment criteria that facilitate the evaluation of risks associated with different budget allocations.

Utilization of the ut class allows for the computation of risks linked to various budget allocations. Should the need arise to implement a customized risk assessment criterion, one can develop an individual function and incorporate it into the optimize_budget method as necessary. Subsequently, guidance on creating a personalized risk assessment criterion will be provided.

from pymc_marketing.mmm import utility as ut

Optimizing Budget Allocation Using Mean Tightness Score (MTS)#

This section focuses on the optimization of marketing budget allocation while incorporating risk considerations. Specifically, we employ the Mean Tightness Score (MTS) as the utility function to ensure that our budget plan effectively minimizes potential losses within a defined HDI (highest density interval).

Overview of the Process#

We invoke mmm.optimize_budget to ascertain the optimal allocation of the marketing budget across various channels over specified time periods, mirroring the approach undertaken in the prior section.

The parameters remain consistent with those from the preceding section, with the addition of the utility_function parameters. In this instance:

  • utility_function: This parameter is assigned to mean_tightness_score.

The Mean Tightness Score represents a risk-adjusted metric that harmonizes the mean return with the tail variability within a distribution. This metric is computed as follows:

\[ Mean\ Tightness\ Score = \mu - \alpha \cdot Tail\ Distance \]

In this formula, \(\mu\) signifies the mean of the sample returns, \(Tail\ Distance\) represents the tail distance metric, and \(\alpha\) denotes the risk tolerance parameter.

ut.mean_tightness_score?
Signature: ut.mean_tightness_score(alpha: float = 0.5, confidence_level: float = 0.75) -> collections.abc.Callable[[pytensor.tensor.variable.TensorVariable, pytensor.tensor.variable.TensorVariable], float]
Docstring:
Calculate the mean tightness score.

The mean tightness score is a risk metric that balances the mean return and the tail variability.
It is calculated as:

.. math::
    Mean\ Tightness\ Score = \mu - \alpha \cdot Tail\ Distance

where:
    - :math:`\mu` is the mean of the sample returns.
    - :math:`Tail\ Distance` is the tail distance metric.
    - :math:`\alpha` is the risk tolerance parameter.

alpha (Risk Tolerance Parameter): This parameter controls the trade-off.
    - Higher :math:`\alpha` increases sensitivity to variability, making the metric value higher for spread dist
    - Lower :math:`\alpha` decreases sensitivity to variability, making the metric value lower for spread dist

Parameters
----------
alpha : float, optional
    Risk tolerance parameter (default is 0.5).
confidence_level : float, optional
    Confidence level for the quantiles (default is 0.75).
    Confidence level must be between 0 and 1.

Returns
-------
UtilityFunctionType
    A function that calculates the mean tightness score given samples and budgets.
File:      ~/Documents/GitHub/pymc-marketing/pymc_marketing/mmm/utility.py
Type:      function
mts_budget_allocation, mts_optimizer_result = mmm.optimize_budget(
    budget=time_unit_budget,
    num_periods=num_periods,
    utility_function=ut.mean_tightness_score(alpha=0.9),
)

mts_posterior_response = mmm.sample_response_distribution(
    allocation_strategy=mts_budget_allocation,
    time_granularity=model_granularity,
    num_periods=num_periods,
    noise_level=0.01,
)

print(f"Budget allocation: {mts_posterior_response.allocation.to_numpy()}")
print(
    f"Total Allocated Budget: {np.sum(mts_posterior_response.allocation.to_numpy())}M"
)

fig, ax = mmm.plot_budget_allocation(samples=mts_posterior_response, figsize=(12, 8))
ax.set_title("Response vs spent per channel", fontsize=18, fontweight="bold");
Sampling: [y]
Budget allocation: [1.63155458 1.36844542]
Total Allocated Budget: 3.0M
../../_images/d7a53397ee57782ea5618858ec991f442908b5af0ccc67c5cff0c29e417fdf9f.png

The majority of the budget is allocated to \(X1\). This allocation has been determined to minimize potential risk. Essentially, this approach indicates that we are prepared to accept lower returns if those returns are characterized by a higher degree of certainty. This is evident in the response distribution plot, which should exhibit a tight distribution with narrow tails.

fig, ax = plt.subplots()
az.plot_dist(
    naive_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    color="blue",
    label="Non-risk optimized allocation",
    ax=ax,
    rug=True,
)
az.plot_dist(
    mts_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    color="orange",
    label="MTS optimized allocation",
    ax=ax,
    rug=True,
)
plt.axvline(
    x=naive_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    .mean()
    * mmm.target_transformer["scaler"].scale_.item(),
    color="blue",
    linestyle="--",
)
plt.axvline(
    x=mts_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    .mean()
    * mmm.target_transformer["scaler"].scale_.item(),
    color="orange",
    linestyle="--",
)
plt.title("Response Distribution at 95% HDI (highest density interval)")
plt.legend()
plt.show()
../../_images/743aa3c531cb5d7541ad0dc6939fd4dc218754969737e1eaaf256d2dcc1ae834.png

Looks like budgets didn’t change much, but the risk is a bit lower compared to the non-risk optimized allocation, if we observe the distributions the density is narrower for the MTS optimized allocation, even when the MTS optimized allocation has almost the same mean.

This strategy is logical, as \(X1\) demonstrates a response with reduced uncertainty, whereas \(X2\) is associated with greater uncertainty. Consequently, the optimizer allocates a larger portion of the budget to \(X1\), as it represents a more secure investment option.

mmm.plot_direct_contribution_curves(show_fit=True, xlim_max=1.5);
../../_images/395d529dd4a0dcf4ffcc4bea5340492663b529bf86f8d32f7b21ef37a6124585.png

We can exhibit this behavior on a more evident way; if we want to maximize a response that is less certain, we should get the opposite scenario. Let’s set the mean tightness score with a lower alpha parameter, meaning, we have a higher risk tolerance.

mts_budget_allocation_high_risk, mts_optimizer_result_high_risk = mmm.optimize_budget(
    budget=time_unit_budget,
    num_periods=num_periods,
    utility_function=ut.mean_tightness_score(alpha=0.3),
)

mts_posterior_response_high_risk = mmm.sample_response_distribution(
    allocation_strategy=mts_budget_allocation_high_risk,
    time_granularity=model_granularity,
    num_periods=num_periods,
    noise_level=0.01,
)

print(f"Budget allocation: {mts_posterior_response_high_risk.allocation.to_numpy()}")
print(
    f"Total Allocated Budget: {np.sum(mts_posterior_response_high_risk.allocation.to_numpy())}M"
)

fig, ax = mmm.plot_budget_allocation(
    samples=mts_posterior_response_high_risk, figsize=(12, 8)
)
ax.set_title("Response vs spent per channel", fontsize=18, fontweight="bold");
Sampling: [y]
Budget allocation: [1.56291798 1.43708202]
Total Allocated Budget: 3.0M
../../_images/68e65949b812f1e226908a3d87caadd598a4e619ba441c68d5c0e8845c39630a.png

We are spending more in \(X2\), and less in \(X1\) compared to the previous allocation. Let’s see the response distribution plot, again to compare it with the previous one.

fig, ax = plt.subplots()
az.plot_dist(
    mts_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    color="orange",
    label="MTS optimized allocation with low risk",
    ax=ax,
    rug=True,
)
az.plot_dist(
    mts_posterior_response_high_risk.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    color="red",
    label="MTS optimized allocation with high risk",
    ax=ax,
    rug=True,
)
plt.axvline(
    x=mts_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    .mean()
    * mmm.target_transformer["scaler"].scale_.item(),
    color="orange",
    linestyle="--",
)
plt.axvline(
    x=mts_posterior_response_high_risk.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    .mean()
    * mmm.target_transformer["scaler"].scale_.item(),
    color="red",
    linestyle="--",
)
plt.title("Response Distribution at 95% HDI (highest density interval)")
plt.legend()
plt.show()
../../_images/f358b062b79876a84d3abad4edc4edfe0d340cc79d64e6ccfa082989acd97ac8.png

As expected, the distribution has bigger tails now, and the mean is higher as well. We got bigger returns, but with more risk. The extra risk is coming from the additional budget allocated to \(X2\).

Optimizing Budget Allocation through ROAS and Value at Risk (VaR)#

In order to enhance decision-making regarding budget allocation, we can integrate various risk assessment criteria to develop a more sophisticated utility function. In this context, we will utilize the Return on Advertising Spend (ROAS) associated with each allocation, alongside the Value at Risk (VaR) as our risk assessment criterion. This approach will facilitate the identification of the allocation that maximizes ROAS while concurrently minimizing potential risks.

Value at Risk is a statistical method employed to quantify the risk of financial loss within a portfolio or investment. Within the realm of marketing, it assists in understanding the potential worst-case loss (ROAS) associated with a particular budget allocation, evaluated at a specified HDI (highest density interval). By minimizing VaR, we aim to select an allocation that ensures, even in adverse scenarios, the ROAS remains as elevated as feasibly possible.

def value_at_roas(confidence_level=0.9):
    """Calculate the Value at Risk (VaR) based on the ROAS distribution."""

    def _value_at_roas(samples, budgets):
        roas_samples = ut._calculate_roas_distribution_for_allocation(samples, budgets)
        return ut.value_at_risk(confidence_level=confidence_level)(
            samples=roas_samples, budgets=budgets
        )

    return _value_at_roas


mts_roas_budget_allocation, mts_roas_optimizer_result = mmm.optimize_budget(
    budget=time_unit_budget,
    num_periods=num_periods,
    utility_function=value_at_roas(confidence_level=0.9),
)

mts_roas_posterior_response = mmm.sample_response_distribution(
    allocation_strategy=mts_roas_budget_allocation,
    time_granularity=model_granularity,
    num_periods=num_periods,
    noise_level=0.01,
)

print(f"Budget allocation: {mts_roas_posterior_response.allocation.to_numpy()}")
print(
    f"Total Allocated Budget: {np.sum(mts_roas_posterior_response.allocation.to_numpy())}M"
)

fig, ax = mmm.plot_budget_allocation(
    samples=mts_roas_posterior_response, figsize=(12, 8)
)
ax.set_title("Response vs spent per channel", fontsize=18, fontweight="bold");
Sampling: [y]
Budget allocation: [1.74304018 1.25695982]
Total Allocated Budget: 3.0M
../../_images/5b96ace73ba9c2b3cb199ca2c070a15cb635e136672bb5adf849017dbcbe7342.png

The optimizer is once again allocating the budget to \(X1\). However, this decision is informed by the expectation that the current combination will yield a higher Return on Advertising Spend (ROAS), while also presenting a small lower risk profile compared to the previous allocation.

fig, ax = plt.subplots()
az.plot_dist(
    mts_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    color="green",
    label="MTS optimized allocation with low risk",
    ax=ax,
    rug=True,
)
az.plot_dist(
    mts_roas_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    color="red",
    label="MTS ROAS optimized allocation",
    ax=ax,
    rug=True,
)
plt.axvline(
    x=mts_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    .mean()
    * mmm.target_transformer["scaler"].scale_.item(),
    color="green",
    linestyle="--",
)
plt.axvline(
    x=mts_roas_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    .mean()
    * mmm.target_transformer["scaler"].scale_.item(),
    color="red",
    linestyle="--",
)
plt.legend()
plt.show()
../../_images/2c275fe7b9de75b923f57ed31ed7b1313d46b4f4fd0d04260a1edae3822e9b05.png

Custom Risk Assessment Criterion#

We have the capacity to establish a bespoke risk assessment criterion by formulating a function that inputs the samples and assets and outputs a scalar value to be optimized. In this context, our objective is to maximize the value at risk, with particular consideration given to the diversification ratio.

We aim to favor allocation strategies that exhibit the highest mean tightness score, while simultaneously ensuring a high level of diversification across marketing channels. As we already possess a foundational understanding of value at risk, we will concentrate our efforts on portfolio entropy.

(
    ut.portfolio_entropy(samples=None, budgets=np.array([0.1, 9.9])).eval(),
    ut.portfolio_entropy(samples=None, budgets=np.array([5, 5])).eval(),
)
(array(0.05600153), array(0.69314718))

We can see that the portfolio entropy is higher when the budget is allocated evenly, meaning that the diversification is higher.

Now, we can create our own risk assessment criterion by combining the value at risk and the portfolio entropy. In this case, we’ll compute the mean tightness score and will multiply the response by the entropy in the portafolio. This will moderate our score, and we’ll prefer the allocation that has the highest score, but with a high diversification between the marketing channels.

def mts_with_diversification(alpha, confidence_level):
    def _mts_with_diversification(samples, budgets):
        return ut.mean_tightness_score(alpha, confidence_level)(samples, budgets) * (
            1 + ut.portfolio_entropy(samples=None, budgets=budgets)
        )

    return _mts_with_diversification
mts_diversification_budget_allocation, mts_diversification_optimizer_result = (
    mmm.optimize_budget(
        budget=time_unit_budget,
        num_periods=num_periods,
        utility_function=mts_with_diversification(alpha=0.9, confidence_level=0.7),
    )
)

mts_diversification_posterior_response = mmm.sample_response_distribution(
    allocation_strategy=mts_diversification_budget_allocation,
    time_granularity=model_granularity,
    num_periods=num_periods,
    noise_level=0.01,
)

print(
    f"Budget allocation: {mts_diversification_posterior_response.allocation.to_numpy()}"
)
print(
    f"Total Allocated Budget: {np.sum(mts_diversification_posterior_response.allocation.to_numpy())}M"
)

fig, ax = mmm.plot_budget_allocation(
    samples=mts_diversification_posterior_response, figsize=(12, 8)
)
ax.set_title("Response vs spent per channel", fontsize=18, fontweight="bold");
Sampling: [y]
Budget allocation: [1.58163404 1.41836596]
Total Allocated Budget: 3.0M
../../_images/2c2658b8a8460874b041717359c9653412898f0da2d1a285e1b6f0beb3dd5cfd.png

We can see that the optimizer is allocating the budget more evenly between the two channels (\(X1\) and \(X2\)), they are spending almost the same amount. This allocation is more balanced than the previous ones. Nevertheless, the total allocated budget is more balanced, the risk is higher, in response terms.

fig, ax = plt.subplots()
az.plot_dist(
    mts_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    color="green",
    label="MTS optimized allocation with low risk",
    ax=ax,
    rug=True,
)
az.plot_dist(
    mts_diversification_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    color="red",
    label="MTS with diversification",
    ax=ax,
    rug=True,
)
plt.title("Response Distribution");
../../_images/baeba8a158f28c31d86a8bb78cbff5fd8b62d7efe093434344310d869ccb4856.png
# Plot all the response distributions one next to each other in the same figure
fig, ax = plt.subplots(figsize=(12, 7))
# remove the descriptions in the plot of mean and interval
az.plot_dist(
    naive_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    ax=ax,
    color="pink",
    label="Non-Risk optimized allocation",
    rug=True,
)
az.plot_dist(
    mts_diversification_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    ax=ax,
    color="red",
    label="MTS with diversification optimized allocation",
    rug=True,
)
az.plot_dist(
    mts_posterior_response_high_risk.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    ax=ax,
    color="blue",
    label="MTS with high risk optimized allocation",
    rug=True,
)
az.plot_dist(
    mts_roas_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    ax=ax,
    color="black",
    label="MTS ROAS optimized allocation",
    rug=True,
)
az.plot_dist(
    mts_posterior_response.channel_contributions.sum(dim="channel")
    .mean(dim="date")
    .values.flatten()
    * mmm.target_transformer["scaler"].scale_.item(),
    ax=ax,
    color="green",
    label="MTS optimized allocation with low risk",
    rug=True,
)
ax.set_title("Response Distribution at 95% HDI (highest density interval)");
../../_images/537045252a803877a75abb764ec353967388193efca311d1d0745156fda2e1c1.png

Great, here its clear how different strategies can lead to similar results, but with different risk profiles. Some distributions are more narrow, and some are more spread based on the risk tolerance.

Conclusion#

In this notebook, we have examined the methodology for assessing the risk associated with various budget allocations, utilizing distinct strategies. We have also demonstrated how to generate an optimal budget allocation that aligns with a specified risk tolerance criterion. Three separate risk assessment metrics were employed: the Mean Tightness Score (MTS), Value at Risk (VaR), and a custom criterion that integrates both the mean tightness score and the diversification ratio.

Next Steps#

It is essential to recognize that not all risk assessment criteria are compatible with the output without appropriate transformations. For instance, to compute the VaR, we analyzed the Return on Advertising Spend (ROAS) distribution; utilizing the response distribution directly would not adhere to the assumptions inherent in the VaR formula, potentially resulting in inconsistent or nonsensical outcomes.

The next step is for you to develop your own risk assessment criterion and apply it to optimize your budget allocation.

%load_ext watermark
%watermark -n -u -v -iv -w -p pytensor
Last updated: Fri Jan 17 2025

Python implementation: CPython
Python version       : 3.10.16
IPython version      : 8.31.0

pytensor: 2.26.4

arviz         : 0.20.0
matplotlib    : 3.10.0
pymc_marketing: 0.10.0
numpy         : 1.26.4

Watermark: 2.5.0