Evaluación de Riesgo de Asignación de Presupuesto con PyMC-Marketing#
Este cuaderno se centra en evaluar los riesgos asociados a diferentes asignaciones presupuestarias en varios canales de marketing. Descubrirá cómo crear una asignación presupuestaria óptima que se alinee con su tolerancia al riesgo específica. Este conocimiento le permitirá tomar decisiones bien fundamentadas respecto a la distribución de su presupuesto.
Conocimientos Previos#
El cuaderno asume que el lector tiene conocimiento de las funcionalidades esenciales de PyMC-Marketing. Si no está familiarizado, el «Cuaderno de Ejemplo de MMM» sirve como un excelente punto de partida, ofreciendo una introducción completa a los modelos de mezcla de medios en este contexto.
Resultados Esperados#
Al completar este cuaderno, los lectores adquirirán una comprensión integral de cómo evaluar los riesgos asociados con diversas asignaciones presupuestarias y cómo desarrollar una asignación presupuestaria óptima basada en criterios de tolerancia al riesgo especificados.
Configuración preliminar#
Consistente con los cuadernos anteriores de la serie PyMC-Marketing, este documento se basa en un conjunto específico de bibliotecas. A continuación se presentan las importaciones necesarias para ejecutar los fragmentos de código que se presentan a continuación.
import warnings
import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from xarray import DataArray
from pymc_marketing.mmm.budget_optimizer import optimizer_xarray_builder
from pymc_marketing.mmm.builders.yaml import build_mmm_from_yaml
from pymc_marketing.mmm.multidimensional import (
MultiDimensionalBudgetOptimizerWrapper,
)
from pymc_marketing.paths import data_dir
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"
/Users/imrisofer/miniconda3/envs/pymc-marketing-dev/lib/python3.12/site-packages/pymc_extras/model/marginal/graph_analysis.py:10: FutureWarning: `pytensor.graph.basic.io_toposort` was moved to `pytensor.graph.traversal.io_toposort`. Calling it from the old location will fail in a future release.
from pytensor.graph.basic import io_toposort
/Users/imrisofer/projects/pymc-marketing/pymc_marketing/mmm/multidimensional.py:218: FutureWarning: This functionality is experimental and subject to change. If you encounter any issues or have suggestions, please raise them at: https://github.com/pymc-labs/pymc-marketing/issues/new
warnings.warn(warning_msg, FutureWarning, stacklevel=1)
/Users/imrisofer/projects/pymc-marketing/pymc_marketing/mmm/time_slice_cross_validation.py:32: UserWarning: The pymc_marketing.mmm.builders module is experimental and its API may change without warning.
from pymc_marketing.mmm.builders.yaml import build_mmm_from_yaml
La expectativa es que un modelo ya ha sido entrenado utilizando las funcionalidades proporcionadas en versiones anteriores de la biblioteca PyMC-Marketing. Por lo tanto, los procesos de generación de datos y entrenamiento se replicarán en un cuaderno diferente. Se aconseja a aquellos que no estén familiarizados con estos procedimientos que consulten el «Cuaderno de Ejemplo de MMM.»
Cargando un modelo preentrenado#
Para utilizar un modelo guardado, cárguelo en una nueva instancia de la clase MMM utilizando el método load a continuación.
data_path = data_dir / "multidimensional_mock_data.csv"
data_df = pd.read_csv(data_path, parse_dates=["date"], index_col=0)
data_df.head()
| date | y | x1 | x2 | event_1 | event_2 | dayofyear | t | geo | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 2018-04-02 | 3984.662237 | 159.290009 | 0.0 | 0.0 | 0.0 | 92 | 0 | geo_a |
| 1 | 2018-04-09 | 3762.871794 | 56.194238 | 0.0 | 0.0 | 0.0 | 99 | 1 | geo_a |
| 2 | 2018-04-16 | 4466.967388 | 146.200133 | 0.0 | 0.0 | 0.0 | 106 | 2 | geo_a |
| 3 | 2018-04-23 | 3864.219373 | 35.699276 | 0.0 | 0.0 | 0.0 | 113 | 3 | geo_a |
| 4 | 2018-04-30 | 4441.625278 | 193.372577 | 0.0 | 0.0 | 0.0 | 120 | 4 | geo_a |
x_train = data_df.drop(columns=["y"])
y_train = data_df["y"]
mmm = build_mmm_from_yaml(
X=x_train,
y=y_train,
config_path=data_dir / "config_files" / "multi_dimensional_example_model.yml",
)
optimizable_model = MultiDimensionalBudgetOptimizerWrapper(
model=mmm, start_date="2021-09-06", end_date="2021-11-29"
)
Formulando el Desafío de Asignación del Presupuesto#
Como en cuadernos anteriores, es esencial delinear el desafío de la asignación del presupuesto. Específicamente, debemos definir la duración de nuestra asignación presupuestaria y el gasto permitido por unidad de tiempo. Nuestro modelo utiliza datos semanales; por lo tanto, mantendremos la misma granularidad temporal.
En este ejemplo, nuestro objetivo es distribuir un presupuesto entre dos canales a lo largo de ocho semanas, con un presupuesto semanal de 3 millones. En consecuencia, el presupuesto total disponible para asignación asciende a 24 millones.
num_periods = optimizable_model.num_periods
time_unit_budget = 3_000 # Imagine is 3K or 3M (per week in this case)
# Define your channels
channels = ["x1", "x2"]
geos = ["geo_a", "geo_b"]
print(f"Total budget to allocate: {num_periods * time_unit_budget:,.0f}")
Total budget to allocate: 39,000
Basado en nuestra intuición, estábamos pensando en distribuir este presupuesto en 80% millones para Google (\(x2\)) y 20% millones para Facebook (\(x1\)). Usando esta asignación, podemos calcular la distribución de respuestas y graficarla.
initial_budget = optimizer_xarray_builder(
np.array(
[
[time_unit_budget * 0.15, time_unit_budget * 0.65],
[time_unit_budget * 0.05, time_unit_budget * 0.15],
]
),
channel=channels,
geo=geos,
)
initial_posterior_response = optimizable_model.sample_response_distribution(
allocation_strategy=initial_budget,
include_carryover=True,
include_last_observations=False,
)
fig, ax = optimizable_model.plot.budget_allocation(
samples=initial_posterior_response, figsize=(12, 8)
)
# Plot the response distribution by Arviz
az.plot_posterior(
initial_posterior_response.total_media_contribution_original_scale.values,
hdi_prob=0.95,
)
plt.title("Response Distribution at 95% HDI (highest density interval)");
Esto es excelente, aparentemente podríamos vender 123,226 unidades dado nuestro presupuesto total de 39,000 (3,000 Diarios) que está mayormente asignado a \(X2\).
¿Podríamos hacerlo mejor? El enfoque habitual es asignar el presupuesto para maximizar la respuesta. Podemos utilizar el método optimize_budget para ello; aquí calcularemos la respuesta dada varias combinaciones de presupuesto, y preferiremos la que maximice la respuesta. Es importante señalar que este ejemplo no utiliza ningún límite ni restricciones, por lo que el optimizador buscará utilizar el presupuesto completo.
allocation_strategy, optimization_result = optimizable_model.optimize_budget(
budget=time_unit_budget,
)
naive_posterior_response = optimizable_model.sample_response_distribution(
allocation_strategy=allocation_strategy,
include_carryover=True,
include_last_observations=False,
)
print(
f"Budget allocation: {naive_posterior_response.allocation.to_numpy().astype(int)}"
)
print(
f"Total Allocated Budget: {np.sum(naive_posterior_response.allocation.to_numpy()):,.0f}"
)
fig, ax = optimizable_model.plot.budget_allocation(
samples=naive_posterior_response, figsize=(12, 8)
)
# Plot the response distribution by Arviz
fig, ax = plt.subplots()
az.plot_posterior(
naive_posterior_response.total_media_contribution_original_scale.values,
hdi_prob=0.95,
color="blue",
label="Optimized allocation",
ax=ax,
)
az.plot_posterior(
initial_posterior_response.total_media_contribution_original_scale.values,
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()
¡Genial! Parece que podríamos vender 145,255 unidades dado nuestro presupuesto de 3,000 unidades de tiempo, lo que significa que el optimizador encontró una mejor asignación que maximiza la respuesta para el mismo presupuesto. Podríamos seguir el mismo enfoque y trazar las dos distribuciones para compararlas.
Esto aclara todo, la asignación optimizada tiene una media más alta. Pero parece que la asignación optimizada tiene un mayor riesgo, ya que la distribución es más amplia, en comparación con la asignación inicial estimada.
Basado en esto, utilizando la asignación optimizada es muy probable obtener una respuesta de 145,255 unidades vendidas, pero también el presupuesto podría llevar a vender tan solo 125,000 unidades o hasta 162,000 unidades vendidas. Por otro lado, utilizando la asignación estimada es muy probable obtener una respuesta de 123,000 unidades vendidas, pero también el presupuesto podría llevar a vender tan solo 117,000 unidades o hasta 133,000 unidades vendidas.
Durante este cuaderno, le proporcionará las herramientas para responder a esta pregunta. Si se enfrenta a una situación en la que la mejor apuesta no es la más segura, ¿cuál preferiría? ¿Mayor media, pero con más riesgo? ¿O menor media, pero con menos riesgo? ¿Una apuesta más segura o una apuesta más arriesgada?
Aquí es donde entra en juego la evaluación de riesgos; podemos utilizar diferentes criterios de evaluación de riesgos para ayudarnos a decidir cuál asignación es mejor según nuestra tolerancia al riesgo.
Introducción a la Evaluación de Riesgos#
El módulo budget_optimizer abarca diversos criterios de evaluación de riesgos que facilitan la evaluación de los riesgos asociados con diferentes asignaciones presupuestarias.
La utilización de la clase ut permite el cálculo de riesgos vinculados a diversas asignaciones presupuestarias. Si surge la necesidad de implementar un criterio de evaluación de riesgos personalizado, se puede desarrollar una función individual e incorporarla al método optimize_budget según sea necesario. Posteriormente, se proporcionará orientación sobre cómo crear un criterio de evaluación de riesgos personalizado.
from pymc_marketing.mmm import utility as ut
Optimización de la Asignación del Presupuesto Utilizando el Puntaje de Ajuste Medio (MTS)#
Esta sección se centra en la optimización de la asignación del presupuesto de marketing, incorporando consideraciones de riesgo. Específicamente, empleamos el Puntaje de Ajuste Medio (MTS) como la función de utilidad para garantizar que nuestro plan de presupuesto minimice efectivamente las pérdidas potenciales dentro de un HDI (intervalo de mayor densidad) definido.
Descripción general del proceso#
Invocamos mmm.optimize_budget para determinar la asignación óptima del presupuesto de marketing a través de varios canales durante períodos de tiempo específicos, reflejando el enfoque adoptado en la sección anterior.
Los parámetros se mantienen consistentes con los de la sección anterior, con la adición de los parámetros de utility_function. En este caso:
utility_function: Este parámetro se asigna amean_tightness_score.
El Puntaje Medio de Ajuste representa una métrica ajustada al riesgo que armoniza el retorno medio con la variabilidad en la cola dentro de una distribución. Esta métrica se calcula de la siguiente manera:
En esta fórmula, \(\mu\) significa la media de los retornos de la muestra, \(Tail\ Distance\) representa la métrica de distancia de cola, y \(\alpha\) denota el parámetro de tolerancia al riesgo.
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 (MTS).
MTS balances the posterior mean against a symmetric, quantile-based tail spread and
returns a dimensionless, normalized score:
.. math::
\mathrm{MTS}(X; \alpha, p) = 1 - \alpha \frac{T_p(X)}{\mu}
where:
- :math:`\mu` is the posterior mean of the samples.
- :math:`T_p(X) = |Q_p - \mu| + |\mu - Q_{1-p}|` is a symmetric tail distance.
Larger :math:`T_p` indicates a more dispersed posterior and thus a lower score.
This formulation makes the following properties explicit:
- :math:`\alpha` controls risk aversion: increasing :math:`\alpha` increases the
penalty on dispersion, so the score decreases for more spread posteriors (all else equal).
- With :math:`\alpha = 0`, the score is identically 1 for any samples (no preference signal).
- For fixed :math:`X` and :math:`p`, the score is linear and non-increasing in :math:`\alpha`.
- For fixed :math:`X` and :math:`\alpha`, the score is non-increasing in :math:`p`
(since :math:`Q_p - Q_{1-p}` widens as :math:`p` moves away from 0.5).
Parameters
----------
alpha : float, optional
Risk-aversion weight. Larger values increase the penalty from tail spread (default 0.5).
confidence_level : float, optional
Quantile probability :math:`p \in (0, 1)` used to compute :math:`T_p`.
Typical choices are :math:`p \in [0.6, 0.9]` (default 0.75).
Returns
-------
UtilityFunctionType
A function that calculates the normalized mean tightness score given samples and budgets.
Raises
------
ValueError
If ``confidence_level`` is not between 0 and 1.
File: ~/projects/pymc-marketing/pymc_marketing/mmm/utility.py
Type: function
mts_budget_allocation, mts_optimizer_result, callback_results = (
optimizable_model.optimize_budget(
budget=time_unit_budget,
utility_function=ut.mean_tightness_score(alpha=0.05, confidence_level=0.94),
callback=True,
minimize_kwargs={"options": {"maxiter": 2_000, "ftol": 1e-18}},
)
)
mts_posterior_response = optimizable_model.sample_response_distribution(
allocation_strategy=mts_budget_allocation,
include_carryover=True,
include_last_observations=False,
)
# Print budget allocation by channel
print("Budget allocation by channel:")
for channel in channels:
print(
f" {channel}: {mts_posterior_response.allocation.sel(channel=channel).astype(int).sum():,}"
)
print(
f"Total Allocated Budget: {np.sum(mts_posterior_response.allocation.to_numpy()):,.0f}"
)
Sampling: [y]
Budget allocation by channel:
x1: 2,172
x2: 826
Total Allocated Budget: 3,000
mts_optimizer_result
[autoreload of cutils_ext failed: Traceback (most recent call last):
File "/Users/imrisofer/miniconda3/envs/pymc-marketing-dev/lib/python3.12/site-packages/IPython/extensions/autoreload.py", line 325, in check
superreload(m, reload, self.old_objects)
File "/Users/imrisofer/miniconda3/envs/pymc-marketing-dev/lib/python3.12/site-packages/IPython/extensions/autoreload.py", line 580, in superreload
module = reload(module)
^^^^^^^^^^^^^^
File "/Users/imrisofer/miniconda3/envs/pymc-marketing-dev/lib/python3.12/importlib/__init__.py", line 130, in reload
raise ModuleNotFoundError(f"spec not found for the module {name!r}", name=name)
ModuleNotFoundError: spec not found for the module 'cutils_ext'
]
message: Optimization terminated successfully
success: True
status: 0
fun: -0.9943168701477988
x: [ 2.103e+03 4.132e+02 7.062e+01 4.135e+02]
nit: 237
jac: [ 1.055e-07 3.468e-06 7.318e-06 3.526e-06]
nfev: 775
njev: 237
multipliers: [ 1.441e-07]
fig, ax = plt.subplots(figsize=(12, 7))
az.plot_dist(
naive_posterior_response.total_media_contribution_original_scale.values,
# hdi_prob=0.85,
color="blue",
label="Optimized allocation",
# kind="hist",
rug=True,
ax=ax,
)
az.plot_dist(
mts_posterior_response.total_media_contribution_original_scale.values,
# hdi_prob=0.85,
color="red",
label="Mean tightness score allocation",
# kind="hist",
rug=True,
ax=ax,
)
plt.legend()
plt.title("Comparison of Allocation Strategies");
La mayor parte del presupuesto se destina a \(X1\). Esta asignación se ha determinado para minimizar el riesgo potencial. Esencialmente, este enfoque indica que estamos dispuestos a aceptar rendimientos más bajos si esos rendimientos se caracterizan por un mayor grado de certeza. Esto es evidente en el gráfico de distribución de respuestas, que debería mostrar una distribución ajustada con colas estrechas.
Ahora el cambio en el presupuesto es bastante significativo, pero el riesgo es menor en comparación con la asignación no optimizada por riesgo. Si observamos las distribuciones, la densidad es más estrecha para la asignación optimizada por MTS.
Esta estrategia es lógica, ya que \(X1\) demuestra una respuesta con menor incertidumbre, mientras que \(X2\) está asociada con una mayor incertidumbre. En consecuencia, el optimizador asigna una mayor parte del presupuesto a \(X1\), ya que representa una opción de inversión más segura.
curve = mmm.saturation.sample_curve(mmm.idata.posterior, max_value=5)
fig, axes = mmm.plot.saturation_curves(
curve,
original_scale=True,
n_samples=15,
hdi_probs=0.94,
subplot_kwargs={"figsize": (12, 8), "ncols": 2},
rc_params={
"xtick.labelsize": 10,
"ytick.labelsize": 10,
"axes.labelsize": 10,
"axes.titlesize": 10,
},
)
# Iterate over all channel-geo combinations
subplot_idx = 0
for channel in channels:
for geo in geos:
# Make sure we're accessing the correct axis object
ax = axes.flat[subplot_idx] if isinstance(axes, np.ndarray) else axes
# Get the budget value for this specific channel-geo combination
budget_value = mts_posterior_response.allocation.sel(
channel=channel, geo=geo
).item()
# Add vertical line with a label
ax.axvline(
x=budget_value,
color="red",
linestyle="--",
label=f"{channel}-{geo}: {budget_value:.1f}",
)
subplot_idx += 1
# Ensure we're working with actual axes objects, not numpy arrays
for i in range(len(channels) * len(geos)):
ax = axes.flat[i] if isinstance(axes, np.ndarray) else axes
if hasattr(ax, "title"):
ax.title.set_fontsize(10)
if hasattr(fig, "_suptitle") and fig._suptitle is not None:
fig._suptitle.set_fontsize(12)
plt.tight_layout()
plt.show()
Podemos exhibir este comportamiento de una manera más evidente; si queremos maximizar una respuesta que es menos cierta, deberíamos obtener el escenario opuesto. Establezcamos la puntuación de rigidez media con un parámetro alfa más alto, lo que significa que tenemos una mayor tolerancia al riesgo.
(
mts_budget_allocation_high_risk,
mts_optimizer_result_high_risk,
callback_results_high_risk,
) = optimizable_model.optimize_budget(
budget=time_unit_budget,
utility_function=ut.mean_tightness_score(alpha=0.95, confidence_level=0.94),
callback=True,
minimize_kwargs={"options": {"maxiter": 2_000, "ftol": 1e-18}},
)
mts_posterior_response_high_risk = optimizable_model.sample_response_distribution(
allocation_strategy=mts_budget_allocation_high_risk,
include_carryover=True,
include_last_observations=False,
)
for channel in channels:
print(
f" {channel}: {mts_posterior_response_high_risk.allocation.sel(channel=channel).astype(int).sum():,}"
)
print(
f"Total Allocated Budget: {np.sum(mts_posterior_response_high_risk.allocation.to_numpy()):,.0f}"
)
Sampling: [y]
x1: 2,216
x2: 782
Total Allocated Budget: 3,000
Estamos gastando más en \(X2\) y menos en \(X1\) en comparación con la asignación anterior. Veamos el gráfico de distribución de respuestas, nuevamente para compararlo con el anterior.
fig, ax = plt.subplots()
az.plot_dist(
mts_posterior_response.total_media_contribution_original_scale.values,
color="orange",
label="MTS optimized allocation with low risk",
ax=ax,
rug=True,
)
az.plot_dist(
mts_posterior_response_high_risk.total_media_contribution_original_scale.values,
color="red",
label="MTS optimized allocation with high risk",
ax=ax,
rug=True,
)
plt.axvline(
x=mts_posterior_response.total_media_contribution_original_scale.values.mean(),
color="orange",
linestyle="--",
)
plt.axvline(
x=mts_posterior_response_high_risk.total_media_contribution_original_scale.values.mean(),
color="red",
linestyle="--",
)
plt.title("Response Distribution at 95% HDI (highest density interval)")
plt.legend()
plt.show()
Como era de esperar, la distribución ahora tiene colas más grandes, y la media también es más alta. Obtuvimos mayores rendimientos, pero con más riesgo. El riesgo adicional proviene del presupuesto extra asignado a \(X2\).
curve = mmm.saturation.sample_curve(mmm.idata.posterior, max_value=5)
fig, axes = mmm.plot.saturation_curves(
curve,
original_scale=True,
n_samples=15,
hdi_probs=0.94,
subplot_kwargs={"figsize": (12, 8), "ncols": 2},
rc_params={
"xtick.labelsize": 10,
"ytick.labelsize": 10,
"axes.labelsize": 10,
"axes.titlesize": 10,
},
)
# Iterate over all channel-geo combinations
subplot_idx = 0
for channel in channels:
for geo in geos:
# Make sure we're accessing the correct axis object
ax = axes.flat[subplot_idx] if isinstance(axes, np.ndarray) else axes
# Get the budget value for this specific channel-geo combination
budget_value = mts_posterior_response_high_risk.allocation.sel(
channel=channel, geo=geo
).item()
# Add vertical line with a label
ax.axvline(
x=budget_value,
color="red",
linestyle="--",
label=f"{channel}-{geo}: {budget_value:.1f}",
)
subplot_idx += 1
# Ensure we're working with actual axes objects, not numpy arrays
for i in range(len(channels) * len(geos)):
ax = axes.flat[i] if isinstance(axes, np.ndarray) else axes
if hasattr(ax, "title"):
ax.title.set_fontsize(10)
if hasattr(fig, "_suptitle") and fig._suptitle is not None:
fig._suptitle.set_fontsize(12)
plt.tight_layout()
plt.show()
Truco
Si observa esta distribución, probablemente notará que es similar a la optimización de riesgo inicial no consciente. Esto se debe a que, a medida que alpha se aproxima a 1, su perfil de riesgo es alto, y la optimización comienza a acercarse a una simple mean(posterior).
fig, ax = plt.subplots(figsize=(12, 7))
az.plot_dist(
naive_posterior_response.total_media_contribution_original_scale.values,
# hdi_prob=0.85,
color="blue",
label="Optimized allocation",
# kind="hist",
rug=True,
ax=ax,
)
az.plot_dist(
mts_posterior_response_high_risk.total_media_contribution_original_scale.values,
color="red",
label="MTS optimized allocation with high risk",
ax=ax,
rug=True,
)
plt.legend()
plt.title("Comparison of Allocation Strategies");
Optimización de la Asignación del Presupuesto a través del ROAS y el Valor en Riesgo (VaR)#
Para mejorar la toma de decisiones respecto a la asignación del presupuesto, podemos integrar diversos criterios de evaluación de riesgos para desarrollar una función de utilidad más sofisticada. En este contexto, utilizaremos el Retorno sobre la Inversión en Publicidad (ROAS) asociado a cada asignación, junto con el Valor en Riesgo (VaR) como nuestro criterio de evaluación de riesgos. Este enfoque facilitará la identificación de la asignación que maximiza el ROAS mientras minimiza simultáneamente los riesgos potenciales.
El Valor en Riesgo es un método estadístico utilizado para cuantificar el riesgo de pérdida financiera dentro de un portafolio o inversión. En el ámbito del marketing, ayuda a comprender la posible pérdida en el peor de los casos (ROAS) asociada con una asignación presupuestaria particular, evaluada en un HDI específico (intervalo de densidad más alta). Al minimizar el VaR, nuestro objetivo es seleccionar una asignación que garantice, incluso en escenarios adversos, que el ROAS se mantenga tan elevado como sea factiblemente posible.
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, callback_results_roas = (
optimizable_model.optimize_budget(
budget=time_unit_budget,
utility_function=value_at_roas(confidence_level=0.75),
callback=True,
minimize_kwargs={"options": {"maxiter": 2_000, "ftol": 1e-18}},
)
)
mts_roas_posterior_response = optimizable_model.sample_response_distribution(
allocation_strategy=mts_roas_budget_allocation,
include_carryover=True,
include_last_observations=False,
)
for channel in channels:
print(
f" {channel}: {mts_roas_posterior_response.allocation.sel(channel=channel).astype(int).sum():,}"
)
print(
f"Total Allocated Budget: {np.sum(mts_roas_posterior_response.allocation.to_numpy()):,.0f}"
)
Sampling: [y]
x1: 1,596
x2: 1,402
Total Allocated Budget: 3,000
mts_roas_optimizer_result
message: Optimization terminated successfully
success: True
status: 0
fun: -46.47735372469248
x: [ 7.955e+02 6.997e+02 8.011e+02 7.037e+02]
nit: 116
jac: [ 1.359e-02 1.428e-02 1.359e-02 1.428e-02]
nfev: 497
njev: 116
multipliers: [ 1.366e-02]
El optimizador está nuevamente asignando el presupuesto a \(X1\) en su mayoría, sin embargo, \(X2\) está recibiendo más dinero esta vez. Sin embargo, esta decisión se basa en la expectativa de que la combinación actual generará un mayor Retorno sobre el Gasto Publicitario (ROAS), al mismo tiempo que presenta un perfil de riesgo ligeramente más bajo en comparación con la asignación anterior.
fig, ax = plt.subplots()
az.plot_dist(
mts_posterior_response.total_media_contribution_original_scale.values,
color="green",
label="MTS optimized allocation with low risk",
ax=ax,
rug=True,
)
az.plot_dist(
mts_roas_posterior_response.total_media_contribution_original_scale.values,
color="red",
label="MTS ROAS optimized allocation",
ax=ax,
rug=True,
)
az.plot_dist(
naive_posterior_response.total_media_contribution_original_scale.values,
# hdi_prob=0.85,
color="blue",
label="Non-risk optimized allocation",
# kind="hist",
rug=True,
ax=ax,
)
plt.axvline(
x=mts_posterior_response.total_media_contribution_original_scale.values.mean(),
color="green",
linestyle="--",
)
plt.axvline(
x=mts_roas_posterior_response.total_media_contribution_original_scale.values.mean(),
color="red",
linestyle="--",
)
plt.axvline(
x=naive_posterior_response.total_media_contribution_original_scale.values.mean(),
color="blue",
linestyle="--",
)
plt.legend()
plt.show()
Criterio de Evaluación de Riesgos Personalizado#
Tenemos la capacidad de establecer un criterio de evaluación de riesgos a medida mediante la formulación de una función que introduce las muestras y activos y produce un valor escalar a optimizar. En este contexto, nuestro objetivo es maximizar el valor en riesgo, prestando especial atención al ratio de diversificación.
Nuestro objetivo es favorecer estrategias de asignación que exhiban el puntaje medio de ajuste más alto, al mismo tiempo que garantizamos un alto nivel de diversificación a través de los canales de marketing. Dado que ya poseemos una comprensión fundamental del valor en riesgo, concentraremos nuestros esfuerzos en la entropía de la cartera.
(
ut.portfolio_entropy(
samples=None, budgets=DataArray([0.1, 9.9], dims=("channel",))
).eval(),
ut.portfolio_entropy(
samples=None,
budgets=DataArray([5, 5], dims=("channel",)),
).eval(),
)
(array(0.05600153), array(0.69314718))
Podemos ver que la entropía de la cartera es mayor cuando el presupuesto se asigna de manera uniforme, lo que significa que la diversificación es mayor.
Ahora, podemos crear nuestro propio criterio de evaluación de riesgos combinando el valor en riesgo y la entropía de la cartera. En este caso, calcularemos la puntuación media de ajuste y multiplicaremos la respuesta por la entropía en la cartera. Esto moderará nuestra puntuación, y preferiremos la asignación que tenga la puntuación más alta, pero con una alta diversificación entre los canales de marketing.
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,
callback_results_diversification,
) = optimizable_model.optimize_budget(
budget=time_unit_budget,
utility_function=mts_with_diversification(alpha=0.9, confidence_level=0.7),
callback=True,
minimize_kwargs={"options": {"maxiter": 2_000, "ftol": 1e-18}},
)
mts_diversification_posterior_response = optimizable_model.sample_response_distribution(
allocation_strategy=mts_diversification_budget_allocation,
include_carryover=True,
include_last_observations=False,
)
for channel in channels:
print(
f" {channel}: {mts_diversification_posterior_response.allocation.sel(channel=channel).astype(int).sum():,}"
)
print(
f"Total Allocated Budget: {np.sum(mts_diversification_posterior_response.allocation.to_numpy()):,.0f}"
)
Sampling: [y]
x1: 1,798
x2: 1,199
Total Allocated Budget: 3,000
mts_diversification_optimizer_result
message: Optimization terminated successfully
success: True
status: 0
fun: -2.257048234413834
x: [ 8.999e+02 6.027e+02 8.996e+02 5.978e+02]
nit: 90
jac: [ 4.223e-05 2.117e-04 4.199e-05 2.144e-04]
nfev: 241
njev: 90
multipliers: [ 5.749e-05]
fig, ax = optimizable_model.plot.budget_allocation(
samples=mts_diversification_posterior_response, figsize=(12, 8)
)
Podemos ver que el optimizador está asignando el presupuesto de manera más equitativa entre los dos canales (\(X1\) y \(X2\)), están gastando casi la misma cantidad. Esta asignación es más equilibrada que las anteriores. Sin embargo, el presupuesto total asignado es más equilibrado, el riesgo es mayor, en términos de respuesta.
fig, ax = plt.subplots()
az.plot_dist(
mts_posterior_response.total_media_contribution_original_scale.values,
color="green",
label="MTS optimized allocation with low risk",
ax=ax,
rug=True,
)
az.plot_dist(
mts_diversification_posterior_response.total_media_contribution_original_scale.values,
color="red",
label="MTS with diversification",
ax=ax,
rug=True,
)
plt.title("Response Distribution");
# 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.total_media_contribution_original_scale.values,
ax=ax,
color="pink",
label="Non-Risk optimized allocation",
rug=True,
)
az.plot_dist(
mts_diversification_posterior_response.total_media_contribution_original_scale.values,
ax=ax,
color="red",
label="MTS with diversification optimized allocation",
rug=True,
)
az.plot_dist(
mts_posterior_response_high_risk.total_media_contribution_original_scale.values,
ax=ax,
color="blue",
label="MTS with high risk optimized allocation",
rug=True,
)
az.plot_dist(
mts_roas_posterior_response.total_media_contribution_original_scale.values,
ax=ax,
color="black",
label="MTS ROAS optimized allocation",
rug=True,
)
az.plot_dist(
mts_posterior_response.total_media_contribution_original_scale.values,
ax=ax,
color="green",
label="MTS optimized allocation with low risk",
rug=True,
)
ax.set_title("Response Distribution at 95% HDI (highest density interval)");
Excelente, aquí está claro cómo diferentes estrategias pueden llevar a resultados similares, pero con diferentes perfiles de riesgo. Algunas distribuciones son más estrechas, y algunas son más amplias según la tolerancia al riesgo.
Conclusión#
En este cuaderno, hemos examinado la metodología para evaluar el riesgo asociado con diversas asignaciones presupuestarias, utilizando estrategias distintas. También hemos demostrado cómo generar una asignación presupuestaria óptima que se alinee con un criterio de tolerancia al riesgo específico. Se emplearon tres métricas de evaluación de riesgos separadas: el Puntaje de Ajuste Medio (MTS), el Valor en Riesgo (VaR) y un criterio personalizado que integra tanto el puntaje de ajuste medio como el ratio de diversificación.
Próximos pasos#
Es esencial reconocer que no todos los criterios de evaluación de riesgos son compatibles con la salida sin las transformaciones adecuadas. Por ejemplo, para calcular el VaR, analizamos la distribución del Retorno sobre el Gasto en Publicidad (ROAS); utilizar la distribución de respuesta directamente no se ajustaría a las suposiciones inherentes a la fórmula del VaR, lo que podría resultar en resultados inconsistentes o sin sentido.
El siguiente paso es que desarrolle su propio criterio de evaluación de riesgos y lo aplique para optimizar la asignación de su presupuesto.
%load_ext watermark
%watermark -n -u -v -iv -w -p pytensor
Last updated: Thu Jan 15 2026
Python implementation: CPython
Python version : 3.12.12
IPython version : 9.6.0
pytensor: 2.35.1
arviz : 0.22.0
pymc_marketing: 0.17.0
numpy : 2.3.4
pandas : 2.3.3
matplotlib : 3.10.7
Watermark: 2.5.0