karpiu
  • Home
  • Quickstart
  • More Examples
    • Attribution
    • Adstock
  • Optimization
    • Target Maximization
    • Net Returns Maximization
  • Previous
  • Next
  • Target Maximization

Target Maximization¶

This section covers budget optimization by maximizing target with the insights from a fitted MMM.

InĀ [1]:
Copied!
import numpy as np
import pandas as pd
import math
import matplotlib.pyplot as plt
import pickle

from karpiu.planning.optim import TargetMaximizer
from karpiu.planning.common import generate_cost_report
from karpiu.plots import ColorConstants

pd.set_option("display.float_format", lambda x: "%.5f" % x)
np.set_printoptions(suppress=True)
pd.set_option("display.max_rows", 50)
import numpy as np import pandas as pd import math import matplotlib.pyplot as plt import pickle from karpiu.planning.optim import TargetMaximizer from karpiu.planning.common import generate_cost_report from karpiu.plots import ColorConstants pd.set_option("display.float_format", lambda x: "%.5f" % x) np.set_printoptions(suppress=True) pd.set_option("display.max_rows", 50)
InĀ [2]:
Copied!
%load_ext autoreload
%autoreload 2
%load_ext autoreload %autoreload 2

Bring back the fitted model from previous Quickstart section.

InĀ [3]:
Copied!
with open("./resource/seasonal/model.pkl", "rb") as f:
    mmm = pickle.load(f)
with open("./resource/seasonal/model.pkl", "rb") as f: mmm = pickle.load(f)
InĀ [4]:
Copied!
budget_start = pd.to_datetime("2021-01-01")
budget_end = pd.to_datetime("2021-01-31")
optim_channels = mmm.get_spend_cols()
budget_start = pd.to_datetime("2021-01-01") budget_end = pd.to_datetime("2021-01-31") optim_channels = mmm.get_spend_cols()
InĀ [5]:
Copied!
maximizer = TargetMaximizer(
    model=mmm,
    budget_start=budget_start,
    budget_end=budget_end,
    optim_channels=optim_channels,
    variance_penalty=0.0,
    # response_scaler=10.0,
)
optim_spend_df = maximizer.optimize(maxiter=5000)
maximizer = TargetMaximizer( model=mmm, budget_start=budget_start, budget_end=budget_end, optim_channels=optim_channels, variance_penalty=0.0, # response_scaler=10.0, ) optim_spend_df = maximizer.optimize(maxiter=5000)
Optimization terminated successfully    (Exit mode 0)
            Current function value: -41124.81052156184
            Iterations: 493
            Function evaluations: 76951
            Gradient evaluations: 493
InĀ [6]:
Copied!
optim_spend_matrix = maximizer.get_current_state()
init_spend_matrix = maximizer.get_init_state()
optim_spend_matrix = maximizer.get_current_state() init_spend_matrix = maximizer.get_init_state()
InĀ [7]:
Copied!
# total spend suggest
print("Suggested total spend: {:.0f}".format(np.sum(optim_spend_matrix)))
# total budget
print("Total Budget: {:.0f}".format(np.sum(init_spend_matrix)))
# total spend suggest print("Suggested total spend: {:.0f}".format(np.sum(optim_spend_matrix))) # total budget print("Total Budget: {:.0f}".format(np.sum(init_spend_matrix)))
Suggested total spend: 453606
Total Budget: 453606

Budget Allocation¶

InĀ [8]:
Copied!
init_total_spend = np.sum(init_spend_matrix, 0)
optim_total_spend = np.sum(optim_spend_matrix, 0)
plot_data = np.vstack([init_total_spend, optim_total_spend])
plot_data.shape

fig, ax = plt.subplots(1, 1, figsize=(20, 10))
bottom = 0.0
for idx, label in enumerate(optim_channels):
    ax.bar(
        x=["Pre", "Post"],
        height=plot_data[:, idx],
        bottom=bottom,
        label=label,
        color=ColorConstants.RAINBOW_SIX[idx],
    )
    bottom += plot_data[:, idx]
# ax.set_title("Spend Allocation Pre vs. Post Optimization")
ax.legend(
    loc="lower center",
    bbox_to_anchor=(0.5, -0.10),
    ncol=math.ceil(len(optim_channels) / 2),
);
init_total_spend = np.sum(init_spend_matrix, 0) optim_total_spend = np.sum(optim_spend_matrix, 0) plot_data = np.vstack([init_total_spend, optim_total_spend]) plot_data.shape fig, ax = plt.subplots(1, 1, figsize=(20, 10)) bottom = 0.0 for idx, label in enumerate(optim_channels): ax.bar( x=["Pre", "Post"], height=plot_data[:, idx], bottom=bottom, label=label, color=ColorConstants.RAINBOW_SIX[idx], ) bottom += plot_data[:, idx] # ax.set_title("Spend Allocation Pre vs. Post Optimization") ax.legend( loc="lower center", bbox_to_anchor=(0.5, -0.10), ncol=math.ceil(len(optim_channels) / 2), );
No description has been provided for this image

Average and Marginal Cost Change¶

InĀ [9]:
Copied!
df = mmm.get_raw_df()
df = mmm.get_raw_df()
InĀ [11]:
Copied!
cost_report = generate_cost_report(
    model=mmm,
    start=budget_start,
    end=budget_end,
    pre_spend_df=df,
    post_spend_df=optim_spend_df,
    method="additive",
)
cost_report
cost_report = generate_cost_report( model=mmm, start=budget_start, end=budget_end, pre_spend_df=df, post_spend_df=optim_spend_df, method="additive", ) cost_report
2023-12-10 15:13:18 - karpiu-planning - INFO - Full calculation start=2021-01-01 and end=2021-01-31
2023-12-10 15:13:18 - karpiu-planning - INFO - Attribution start=2021-01-01 and end=2021-01-31
2023-12-10 15:13:18 - karpiu-planning - INFO - Full calculation start=2021-01-01 and end=2021-01-31
2023-12-10 15:13:18 - karpiu-planning - INFO - Attribution start=2021-01-01 and end=2021-01-31
Out[11]:
pre-opt-avg-cost post-opt-avg-cost pre-opt-marginal-cost post-opt-marginal-cost pre-opt-spend post-opt-spend pre-opt-attr post-opt-attr
promo 44.57775 32.00855 47.59562 49.31798 48.64000 49.00586 1091.12732 1531.02402
radio 76.23849 34.81708 56.48530 51.82603 105.58500 49.86311 1384.93044 1432.14523
search 29.39121 29.25783 25.48650 43.41038 75.70300 199.44081 2575.70174 6816.66359
social 33.89162 28.68790 27.00025 45.02274 61.33200 119.26722 1809.65100 4157.40470
tv 97.31447 39.20080 90.79573 55.69172 162.34600 36.02899 1668.26161 919.08801
InĀ [12]:
Copied!
# TODO: figure out the y-axis unit

fig, ax = plt.subplots(3, 1, figsize=(8, 6))
# spend
ax[0].bar(
    x=np.arange(0, 3 * len(optim_channels), 3),
    height=cost_report["pre-opt-spend"],
    color="orange",
    alpha=0.5,
    label="Current",
)
ax[0].bar(
    x=np.arange(1, 3 * len(optim_channels) + 1, 3),
    height=cost_report["post-opt-spend"],
    color="dodgerblue",
    label="Suggestion",
)
ax[0].set_xticks(0.5 + np.arange(0, 3 * len(optim_channels), 3))
ax[0].set_xticklabels(optim_channels)
# ax[0].legend()
ax[0].set_title("Spend")

# Attribution
ax[1].bar(
    x=np.arange(0, 3 * len(optim_channels), 3),
    height=cost_report["pre-opt-attr"],
    color="orange",
    alpha=0.5,
    label="Current",
)
ax[1].bar(
    x=np.arange(1, 3 * len(optim_channels) + 1, 3),
    height=cost_report["post-opt-attr"],
    color="dodgerblue",
    label="Suggestion",
)
ax[1].set_xticks(0.5 + np.arange(0, 3 * len(optim_channels), 3))
ax[1].set_xticklabels(optim_channels)
# ax[1].legend()
ax[1].set_title("Attribution")

# Efficiency
ax[2].bar(
    x=np.arange(0, 3 * len(optim_channels), 3),
    height=cost_report["pre-opt-avg-cost"],
    color="orange",
    alpha=0.5,
    label="Current",
)
ax[2].bar(
    x=np.arange(1, 3 * len(optim_channels) + 1, 3),
    height=cost_report["post-opt-avg-cost"],
    color="dodgerblue",
    label="Suggestion",
)
ax[2].set_xticks(0.5 + np.arange(0, 3 * len(optim_channels), 3))
ax[2].set_xticklabels(optim_channels)
ax[2].legend()
ax[2].set_title("Average Cost per Sales")

fig.tight_layout()
# TODO: figure out the y-axis unit fig, ax = plt.subplots(3, 1, figsize=(8, 6)) # spend ax[0].bar( x=np.arange(0, 3 * len(optim_channels), 3), height=cost_report["pre-opt-spend"], color="orange", alpha=0.5, label="Current", ) ax[0].bar( x=np.arange(1, 3 * len(optim_channels) + 1, 3), height=cost_report["post-opt-spend"], color="dodgerblue", label="Suggestion", ) ax[0].set_xticks(0.5 + np.arange(0, 3 * len(optim_channels), 3)) ax[0].set_xticklabels(optim_channels) # ax[0].legend() ax[0].set_title("Spend") # Attribution ax[1].bar( x=np.arange(0, 3 * len(optim_channels), 3), height=cost_report["pre-opt-attr"], color="orange", alpha=0.5, label="Current", ) ax[1].bar( x=np.arange(1, 3 * len(optim_channels) + 1, 3), height=cost_report["post-opt-attr"], color="dodgerblue", label="Suggestion", ) ax[1].set_xticks(0.5 + np.arange(0, 3 * len(optim_channels), 3)) ax[1].set_xticklabels(optim_channels) # ax[1].legend() ax[1].set_title("Attribution") # Efficiency ax[2].bar( x=np.arange(0, 3 * len(optim_channels), 3), height=cost_report["pre-opt-avg-cost"], color="orange", alpha=0.5, label="Current", ) ax[2].bar( x=np.arange(1, 3 * len(optim_channels) + 1, 3), height=cost_report["post-opt-avg-cost"], color="dodgerblue", label="Suggestion", ) ax[2].set_xticks(0.5 + np.arange(0, 3 * len(optim_channels), 3)) ax[2].set_xticklabels(optim_channels) ax[2].legend() ax[2].set_title("Average Cost per Sales") fig.tight_layout()
No description has been provided for this image

Outcome Plot¶

InĀ [13]:
Copied!
df = mmm.get_raw_df()
optim_pred = mmm.predict(optim_spend_df)
init_pred = mmm.predict(df)
df = mmm.get_raw_df() optim_pred = mmm.predict(optim_spend_df) init_pred = mmm.predict(df)
InĀ [14]:
Copied!
fig, ax = plt.subplots(1, 1, figsize=(16, 8))
ax.plot(
    optim_pred.loc[maximizer.result_mask, "date"].values,
    optim_pred.loc[maximizer.result_mask, "prediction"].values,
    label="optimal",
    alpha=0.8,
)
ax.plot(
    init_pred.loc[maximizer.result_mask, "date"].values,
    init_pred.loc[maximizer.result_mask, "prediction"].values,
    label="original",
    alpha=0.8,
)
fig.legend()
fig.tight_layout();
fig, ax = plt.subplots(1, 1, figsize=(16, 8)) ax.plot( optim_pred.loc[maximizer.result_mask, "date"].values, optim_pred.loc[maximizer.result_mask, "prediction"].values, label="optimal", alpha=0.8, ) ax.plot( init_pred.loc[maximizer.result_mask, "date"].values, init_pred.loc[maximizer.result_mask, "prediction"].values, label="original", alpha=0.8, ) fig.legend() fig.tight_layout();
No description has been provided for this image
InĀ [15]:
Copied!
# check2: total predicted response must be higher than current
total_optim_pred = np.sum(optim_pred.loc[maximizer.result_mask, "prediction"].values)
total_init_pred = np.sum(init_pred.loc[maximizer.result_mask, "prediction"].values)
assert total_optim_pred > total_init_pred
# check2: total predicted response must be higher than current total_optim_pred = np.sum(optim_pred.loc[maximizer.result_mask, "prediction"].values) total_init_pred = np.sum(init_pred.loc[maximizer.result_mask, "prediction"].values) assert total_optim_pred > total_init_pred

Cost Curve¶

InĀ [16]:
Copied!
from karpiu.planning.cost_curves import CostCurves
from karpiu.planning.cost_curves import CostCurves
InĀ [17]:
Copied!
cc = CostCurves(
    model=mmm,
    n_steps=10,
    curve_type="individual",
    extend_multiplier=1.05,
)
cc.generate_cost_curves()
cc_optim = CostCurves(
    model=mmm,
    n_steps=10,
    curve_type="individual",
    spend_df=optim_spend_df,
    extend_multiplier=1.05,
)
cc_optim.generate_cost_curves()
cc = CostCurves( model=mmm, n_steps=10, curve_type="individual", extend_multiplier=1.05, ) cc.generate_cost_curves() cc_optim = CostCurves( model=mmm, n_steps=10, curve_type="individual", spend_df=optim_spend_df, extend_multiplier=1.05, ) cc_optim.generate_cost_curves()
2023-12-10 15:13:32 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:13:32 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:13:34 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:13:34 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
InĀ [18]:
Copied!
cc.plot(optim_cost_curves=cc_optim.cost_curves, include_organic=False);
cc.plot(optim_cost_curves=cc_optim.cost_curves, include_organic=False);
No description has been provided for this image

The following code block is to create animation of the optimization process along the cost curve.

InĀ [19]:
Copied!
xs = maximizer.get_callback_metrics()["xs"]
fig_paths = list()

idx_seq = np.round(np.linspace(0, len(xs) - 1, 10)).astype(int)
for idx in idx_seq:
    x = xs[idx]
    x = x.reshape(-1, maximizer.n_optim_channels) * maximizer.spend_scaler
    # build a temporary spend_df
    temp_df = mmm.get_raw_df()
    temp_df.loc[maximizer.budget_mask, optim_channels] = x

    cc = CostCurves(
        model=mmm,
        n_steps=10,
        curve_type="individual",
        extend_multiplier=1.05,
    )
    cc.generate_cost_curves()
    cc_optim = CostCurves(
        model=mmm,
        n_steps=10,
        curve_type="individual",
        spend_df=temp_df,
        extend_multiplier=1.05,
    )
    cc_optim.generate_cost_curves()
    axes = cc.plot(
        optim_cost_curves=cc_optim.cost_curves, include_organic=False, is_visible=False
    )
    fig = axes[0].get_figure()

    fig_path = "./demo_optim_output/png/fig_{:07d}.png".format(idx)
    fig_paths.append(fig_path)
    fig.savefig(fname=fig_path)

import imageio

with imageio.get_writer(
    "./demo_optim_output/optim.gif", mode="I", duration=500
) as writer:
    for fig_path in fig_paths:
        image = imageio.imread(fig_path)
        writer.append_data(image)
xs = maximizer.get_callback_metrics()["xs"] fig_paths = list() idx_seq = np.round(np.linspace(0, len(xs) - 1, 10)).astype(int) for idx in idx_seq: x = xs[idx] x = x.reshape(-1, maximizer.n_optim_channels) * maximizer.spend_scaler # build a temporary spend_df temp_df = mmm.get_raw_df() temp_df.loc[maximizer.budget_mask, optim_channels] = x cc = CostCurves( model=mmm, n_steps=10, curve_type="individual", extend_multiplier=1.05, ) cc.generate_cost_curves() cc_optim = CostCurves( model=mmm, n_steps=10, curve_type="individual", spend_df=temp_df, extend_multiplier=1.05, ) cc_optim.generate_cost_curves() axes = cc.plot( optim_cost_curves=cc_optim.cost_curves, include_organic=False, is_visible=False ) fig = axes[0].get_figure() fig_path = "./demo_optim_output/png/fig_{:07d}.png".format(idx) fig_paths.append(fig_path) fig.savefig(fname=fig_path) import imageio with imageio.get_writer( "./demo_optim_output/optim.gif", mode="I", duration=500 ) as writer: for fig_path in fig_paths: image = imageio.imread(fig_path) writer.append_data(image)
2023-12-10 15:13:38 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:13:38 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:13:40 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:13:40 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:13:42 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:13:42 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:13:45 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:13:45 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:13:47 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:13:47 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:13:50 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:13:50 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:13:52 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:13:52 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:13:54 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:13:54 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:13:57 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:13:57 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:13:59 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:13:59 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:14:02 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:14:02 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:14:04 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:14:04 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:14:07 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:14:07 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:14:09 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:14:09 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:14:12 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:14:12 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:14:14 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:14:14 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:14:17 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:14:17 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:14:19 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:14:19 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:14:22 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:14:22 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]
2023-12-10 15:14:24 - karpiu-planning - INFO - Minimum spend threshold is hit in some channel(s). Update with value 0.001.
2023-12-10 15:14:24 - karpiu-planning - INFO - Derived channels multipliers based on input spend.
  0%|          | 0/5 [00:00<?, ?it/s]

Documentation built with MkDocs.

Keyboard Shortcuts

Keys Action
? Open this help
n Next page
p Previous page
s Search