Skip to content

Commit

Permalink
formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
edwinnglabs committed Dec 12, 2023
1 parent 0fb16cb commit 7cf9c9c
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 113 deletions.
126 changes: 69 additions & 57 deletions docs/examples/net_returns_max.ipynb

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions docs/examples/target_max.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1075,12 +1075,13 @@
" optim_cost_curves=cc_optim.cost_curves, include_organic=False, is_visible=False\n",
" )\n",
" fig = axes[0].get_figure()\n",
" \n",
"\n",
" fig_path = \"./demo_optim_output/png/fig_{:07d}.png\".format(idx)\n",
" fig_paths.append(fig_path)\n",
" fig.savefig(fname=fig_path)\n",
" \n",
"\n",
"import imageio\n",
"\n",
"with imageio.get_writer(\n",
" \"./demo_optim_output/optim.gif\", mode=\"I\", duration=500\n",
") as writer:\n",
Expand Down
57 changes: 33 additions & 24 deletions karpiu/planning/optim/channel_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,11 +260,11 @@ def __init__(self, attributor: AttributorGamma, ltv_arr: np.ndarray, **kwargs):
def objective_func(self, spend: np.ndarray, extra_info: bool = False):
# spend(n_optim_channels, ) -> (broadcast) -> input spend matrix (n_budget_steps, n_optim_channels)
# time weight(n_budget_steps, ) -> (expand_dim) -> time weight(n_budget_steps, 1)
# input spend matrix (n_budget_steps, n_optim_channels) * time weight(n_budget_steps, 1)
# input spend matrix (n_budget_steps, n_optim_channels) * time weight(n_budget_steps, 1)
# -> (multiply) -> distributed spend matrix
input_channel_spend_matrix = (
spend
* np.ones((self.n_budget_steps, self.n_optim_channels))
# * np.ones((self.n_budget_steps, self.n_optim_channels))
* np.expand_dims(self.weight, -1)
)
# the full spend matrix pass into attribution calculation
Expand Down Expand Up @@ -306,7 +306,7 @@ def objective_func(self, spend: np.ndarray, extra_info: bool = False):
attr_marketing=attr_marketing,
)

# For attribution, revenue, and cost are calculated
# For attribution, revenue, and cost are calculated
# with all channels spend (not just the two we are optimizing) as the shape
# (n_optim_channels, )
revenue = self.ltv_arr * np.sum(spend_attr_matrix, 0)
Expand All @@ -315,7 +315,7 @@ def objective_func(self, spend: np.ndarray, extra_info: bool = False):
net_profit = np.sum(revenue - cost)
loss = -1 * net_profit / self.response_scaler
if extra_info:
return np.sum(revenue), np.sum(cost)
return np.sum(revenue), np.sum(cost), input_channel_spend_matrix
else:
return loss

Expand All @@ -324,6 +324,7 @@ def _init_callback_metrics(self):
"xs": list(),
"optim_revenues": list(),
"optim_costs": list(),
"spend_matrix": list(),
}

# override parent class
Expand All @@ -332,32 +333,36 @@ def optim_callback(self, xk: np.ndarray, *_):
See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html for details.
"""
self.callback_metrics["xs"].append(xk)
revs, costs = self.objective_func(xk, extra_info=True)
revs, costs, spend_matrix = self.objective_func(xk, extra_info=True)
self.callback_metrics["optim_revenues"].append(revs)
self.callback_metrics["optim_costs"].append(costs)
self.callback_metrics["spend_matrix"].append(spend_matrix)


def ch_based_net_profit_response_curve(ch_npm: ChannelNetProfitMaximizer, model:MMM, n_iters=10):
def ch_based_net_profit_response_curve(
ch_npm: ChannelNetProfitMaximizer, model: MMM, n_iters=10
):
net_profits = np.empty((n_iters, n_iters))
total_budget = ch_npm.total_budget
date_col = ch_npm.date_col
budget_start = ch_npm.budget_start
budget_end = ch_npm.budget_end

logger = logging.getLogger('karpiu-planning-test')
logger = logging.getLogger("karpiu-planning-test")
logger.setLevel(30)

def ch_based_net_profit_response(x1, x2, attributor, time_steps_weight,
base_spend_df, optim_channels, ltv_arr
def ch_based_net_profit_response(
x1, x2, attributor, time_steps_weight, base_spend_df, optim_channels, ltv_arr
) -> np.ndarray:
# (n_steps, n_channels)
input_spend_matrix = np.stack([x1, x2]) * time_steps_weight
input_spend_matrix = np.stack([x1, x2]) * time_steps_weight
temp_spend_df = base_spend_df.copy()
temp_spend_df.loc[
(temp_spend_df[date_col] >= budget_start)
(temp_spend_df[date_col] >= budget_start)
& (temp_spend_df[date_col] <= budget_end),
optim_channels
optim_channels,
] = input_spend_matrix

attributor = AttributorGamma(
model=model,
df=temp_spend_df,
Expand All @@ -366,14 +371,16 @@ def ch_based_net_profit_response(x1, x2, attributor, time_steps_weight,
logger=logger,
)
_, spend_attr, _, _ = attributor.make_attribution()

# For attribution, revenue, and cost are calculated with all channels spend (not just the two we are optimizing) as the input
cost = np.sum(temp_spend_df.loc[
(temp_spend_df[date_col] >= budget_start)
& (temp_spend_df[date_col] <= budget_end),
# always use full channels in time-based optimization
model.get_spend_cols()
].values)
cost = np.sum(
temp_spend_df.loc[
(temp_spend_df[date_col] >= budget_start)
& (temp_spend_df[date_col] <= budget_end),
# always use full channels in time-based optimization
model.get_spend_cols(),
].values
)

return np.sum(spend_attr.loc[:, model.get_spend_cols()].values * ltv_arr) - cost

Expand All @@ -387,11 +394,13 @@ def ch_based_net_profit_response(x1, x2, attributor, time_steps_weight,
x1 = x1s[i, j]
x2 = x2s[i, j]
net_profits[i, j] = ch_based_net_profit_response(
x1, x2, attributor=ch_npm.attributor,
time_steps_weight=ch_npm.weight,
x1,
x2,
attributor=ch_npm.attributor,
time_steps_weight=ch_npm.weight,
base_spend_df=ch_npm.df,
optim_channels=ch_npm.optim_channels,
optim_channels=ch_npm.optim_channels,
ltv_arr=ch_npm.ltv_arr,
)

return x1s, x2s, net_profits
return x1s, x2s, net_profits
62 changes: 32 additions & 30 deletions karpiu/planning/optim/time_base_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def set_bounds_and_constraints(self, df: pd.DataFrame) -> None:
"""_summary_
Args:
df (pd.DataFrame): df assumes each index sequentially represents the time step in optimization period;
df (pd.DataFrame): df assumes each index sequentially represents the time step in optimization period;
a special index can be specified once as "total" which will be used as budget constraints instead of bounds
"""
# "date" is a reserved keyword
Expand Down Expand Up @@ -278,10 +278,7 @@ def objective_func(self, spend: np.ndarray, extra_info: bool = False):
# spend(n_budget_steps, ) -> (expand) -> spend(n_budget_steps, 1)
# (multiply channels weight(n_optim_channels)
# -> (broadcast) -> input_channel_spend_matrix (n_budget_steps, n_optim_channels)
input_channel_spend_matrix = (
np.expand_dims(spend, -1)
* self.weight
)
input_channel_spend_matrix = np.expand_dims(spend, -1) * self.weight
# the full spend matrix pass into attribution calculation
spend_matrix = self.full_channels_spend_matrix.copy()
spend_matrix[:, self.optim_channels_idx] = input_channel_spend_matrix
Expand Down Expand Up @@ -320,7 +317,7 @@ def objective_func(self, spend: np.ndarray, extra_info: bool = False):
attr_marketing=attr_marketing,
)

# For attribution, revenue, and cost are calculated
# For attribution, revenue, and cost are calculated
# with all channels spend (not just the two we are optimizing) as the shape
# (n_optim_channels, )
revenue = self.ltv_arr * np.sum(spend_attr_matrix, 0)
Expand All @@ -331,7 +328,7 @@ def objective_func(self, spend: np.ndarray, extra_info: bool = False):
# add punishment of variance of spend; otherwise may risk of identifiability issue with adstock
loss += self.variance_penalty * np.var(spend)
if extra_info:
return np.sum(revenue), np.sum(cost)
return np.sum(revenue), np.sum(cost), input_channel_spend_matrix
else:
return loss

Expand All @@ -340,6 +337,7 @@ def _init_callback_metrics(self):
"xs": list(),
"optim_revenues": list(),
"optim_costs": list(),
"spend_matrix": list(),
}

# override parent class
Expand All @@ -348,34 +346,37 @@ def optim_callback(self, xk: np.ndarray, *_):
See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html for details.
"""
self.callback_metrics["xs"].append(xk)
revs, costs = self.objective_func(xk, extra_info=True)
revs, costs, spend_matrix = self.objective_func(xk, extra_info=True)
self.callback_metrics["optim_revenues"].append(revs)
self.callback_metrics["optim_costs"].append(costs)
self.callback_metrics["spend_matrix"].append(spend_matrix)


# assert budget start and and end are only two steps
def time_based_net_profit_response_curve(t_npm: TimeNetProfitMaximizer, model:MMM, n_iters=10):
def time_based_net_profit_response_curve(
t_npm: TimeNetProfitMaximizer, model: MMM, n_iters=10
):
net_profits = np.empty((n_iters, n_iters))
total_budget = t_npm.total_budget
date_col = t_npm.date_col
budget_start = t_npm.budget_start
budget_end = t_npm.budget_end

logger = logging.getLogger('karpiu-planning-test')
logger = logging.getLogger("karpiu-planning-test")
logger.setLevel(30)

def time_based_net_profit_response(x1, x2, attributor, channels_weight,
base_spend_df, optim_channels, ltv_arr
def time_based_net_profit_response(
x1, x2, attributor, channels_weight, base_spend_df, optim_channels, ltv_arr
) -> np.ndarray:
# (n_steps, n_channels)
input_spend_matrix = np.vstack([x1, x2]) * channels_weight
input_spend_matrix = np.vstack([x1, x2]) * channels_weight
temp_spend_df = base_spend_df.copy()
temp_spend_df.loc[
(temp_spend_df[date_col] >= budget_start)
(temp_spend_df[date_col] >= budget_start)
& (temp_spend_df[date_col] <= budget_end),
optim_channels
optim_channels,
] = input_spend_matrix

attributor = AttributorGamma(
model=model,
df=temp_spend_df,
Expand All @@ -384,15 +385,17 @@ def time_based_net_profit_response(x1, x2, attributor, channels_weight,
logger=logger,
)
_, spend_attr, _, _ = attributor.make_attribution()

# For attribution, revenue, and cost are calculated with all channels spend (not just the two we are optimizing) as the input
cost = np.sum(temp_spend_df.loc[
(temp_spend_df[date_col] >= budget_start)
& (temp_spend_df[date_col] <= budget_end),
# always use full channels in time-based optimization
model.get_spend_cols()
].values)

cost = np.sum(
temp_spend_df.loc[
(temp_spend_df[date_col] >= budget_start)
& (temp_spend_df[date_col] <= budget_end),
# always use full channels in time-based optimization
model.get_spend_cols(),
].values
)

return np.sum(spend_attr.loc[:, model.get_spend_cols()].values * ltv_arr) - cost

x1s = total_budget * np.linspace(0, 1, n_iters)
Expand All @@ -405,14 +408,13 @@ def time_based_net_profit_response(x1, x2, attributor, channels_weight,
x1 = x1s[i, j]
x2 = x2s[i, j]
net_profits[i, j] = time_based_net_profit_response(
x1, x2, attributor=t_npm.attributor,
channels_weight=t_npm.weight,
x1,
x2,
attributor=t_npm.attributor,
channels_weight=t_npm.weight,
base_spend_df=t_npm.df,
optim_channels=t_npm.optim_channels,
optim_channels=t_npm.optim_channels,
ltv_arr=t_npm.ltv_arr,
)

return x1s, x2s, net_profits



0 comments on commit 7cf9c9c

Please sign in to comment.