BlendMMM/Simulator-UOPX
0
1import numpy as np
2from scipy.optimize import minimize, LinearConstraint, NonlinearConstraint
3from collections import OrderedDict
4import pandas as pd
5from numerize.numerize import numerize
6# from gekko import GEKKO
7def class_to_dict(class_instance):
8 attr_dict = {}
9 if isinstance(class_instance, Channel):
10 attr_dict["type"] = "Channel"
11 attr_dict["name"] = class_instance.name
12 attr_dict["dates"] = class_instance.dates
13 attr_dict["spends"] = class_instance.actual_spends
14 attr_dict["conversion_rate"] = class_instance.conversion_rate
15 attr_dict["modified_spends"] = class_instance.modified_spends
16 attr_dict["modified_sales"] = class_instance.modified_sales
17 attr_dict["response_curve_type"] = class_instance.response_curve_type
18 attr_dict["response_curve_params"] = class_instance.response_curve_params
19 attr_dict["penalty"] = class_instance.penalty
20 attr_dict["bounds"] = class_instance.bounds
21 attr_dict["actual_total_spends"] = class_instance.actual_total_spends
22 attr_dict["actual_total_sales"] = class_instance.actual_total_sales
23 attr_dict["modified_total_spends"] = class_instance.modified_total_spends
24 attr_dict["modified_total_sales"] = class_instance.modified_total_sales
25 # attr_dict["actual_mroi"] = class_instance.get_marginal_roi("actual")
26 # attr_dict["modified_mroi"] = class_instance.get_marginal_roi("modified")
27
28 elif isinstance(class_instance, Scenario):
29 attr_dict["type"] = "Scenario"
30 attr_dict["name"] = class_instance.name
31 channels = []
32 for channel in class_instance.channels.values():
33 channels.append(class_to_dict(channel))
34 attr_dict["channels"] = channels
35 attr_dict["constant"] = class_instance.constant
36 attr_dict["correction"] = class_instance.correction
37 attr_dict["actual_total_spends"] = class_instance.actual_total_spends
38 attr_dict["actual_total_sales"] = class_instance.actual_total_sales
39 attr_dict["modified_total_spends"] = class_instance.modified_total_spends
40 attr_dict["modified_total_sales"] = class_instance.modified_total_sales
41
42 return attr_dict
43
44
45def class_from_dict(attr_dict):
46 if attr_dict["type"] == "Channel":
47 return Channel.from_dict(attr_dict)
48 elif attr_dict["type"] == "Scenario":
49 return Scenario.from_dict(attr_dict)
50
51
52class Channel:
53 def __init__(
54 self,
55 name,
56 dates,
57 spends,
58 sales,
59 response_curve_type,
60 response_curve_params,
61 bounds,channel_bounds_min,channel_bounds_max,
62 conversion_rate=1,
63 modified_spends=None,
64 modified_sales=None,
65 penalty=True,
66 ):
67 self.name = name
68 self.dates = dates
69 self.conversion_rate = conversion_rate
70 self.actual_spends = spends.copy()
71 self.actual_sales = sales.copy()
72
73 if modified_spends is None:
74 self.modified_spends = self.actual_spends.copy()
75 else:
76 self.modified_spends = modified_spends
77
78 if modified_sales is None:
79 # self.modified_sales = self.calculate_sales()
80 self.modified_sales = self.actual_sales.copy()
81 else:
82 self.modified_sales = self.calculate_sales()
83 # self.modified_spends = modified_spends
84
85 self.response_curve_type = response_curve_type
86 self.response_curve_params = response_curve_params
87 self.bounds = bounds
88 self.channel_bounds_min = channel_bounds_min
89 self.channel_bounds_max = channel_bounds_max
90 self.penalty = penalty
91
92 self.upper_limit = self.actual_spends.max() + self.actual_spends.std()
93 self.power = np.ceil(np.log(self.actual_spends.max()) / np.log(10)) - 3
94 # self.actual_sales = None
95 # self.actual_sales = self.response_curve(self.actual_spends)#sales.copy()#
96 self.actual_total_spends = self.actual_spends.sum()
97 self.actual_total_sales = self.actual_sales.sum()
98
99 self.modified_total_spends = self.modified_spends.sum()
100 self.modified_total_sales = self.modified_sales.sum()
101 self.delta_spends = self.modified_total_spends - self.actual_total_spends
102 self.delta_sales = self.modified_total_sales - self.actual_total_sales
103 # # # # print(self.actual_total_spends)
104 def update_penalty(self, penalty):
105 self.penalty = penalty
106
107 def _modify_spends(self, spends_array, total_spends):
108 return spends_array * total_spends / spends_array.sum()
109
110 def modify_spends(self, total_spends):
111 # # # # print(total_spends)
112 self.modified_spends[0] = total_spends
113 # (
114 # self.modified_spends * total_spends / self.modified_spends.sum()
115 # )
116 # # # # print("in spends")
117 # # # # print(self.modified_spends,self.modified_spends.sum())
118
119 def calculate_sales(self):
120 # # # # print("in calc_sales")
121 # # # # print(self.modified_spends)
122 return self.response_curve(self.modified_spends)
123
124 def hill_equation(x, Kd, n):
125 return x**n / (Kd**n + x**n)
126 def response_curve(self, x):
127 # # # # print(x)
128 # if self.penalty:
129 # # # # print("in penalty")
130 # x = np.where(
131 # x < self.upper_limit,
132 # x,
133 # self.upper_limit + (x - self.upper_limit) * self.upper_limit / x,
134 # )
135 if self.response_curve_type == "hill-eq":
136 # dividing_parameter = check_dividing_parameter()
137 # # # # print("lalala")
138 # # # # # print(self.name)\
139 # # # # print(len(x))
140 # # # # print("in response curve function")
141 # # # # print(x)
142 if len(x) == 1:
143 dividing_rate = self.response_curve_params["num_pos_obsv"]
144 # # # # print(dividing_rate)
145 # x = np.sum(x)
146 else:
147 dividing_rate = 1
148 # dividing_rate = self.response_curve_params["num_pos_obsv"]
149 # x = np.sum(x)
150 # dividing_rate = 104
151 dividing_rate = self.response_curve_params["num_pos_obsv"]
152 Kd= self.response_curve_params["Kd"]
153 n= self.response_curve_params["n"]
154 x_min= self.response_curve_params["x_min"]
155 x_max= self.response_curve_params["x_max"]
156 y_min= self.response_curve_params["y_min"]
157 y_max= self.response_curve_params['y_max']
158 # # # # # print(x_min)
159 # # # # # print(Kd,n,x_min,x_max,y_min,y_max)
160 # # # # # print(np.sum(x)/104)
161 x_inp = ( x/dividing_rate- x_min) / (x_max - x_min)
162 # # # # # print("x",x)
163 # # # # # print("x_inp",x_inp)
164 x_out = x_inp**n / (Kd**n + x_inp**n) #self.hill_equation(x_inp,Kd, n)
165 # # # # # print("x_out",x_out)
166
167
168 x_val_inv = (x_out*x_max + (1 - x_out) * x_min)
169 sales = (x_val_inv*y_min/y_max)*dividing_rate
170 # sales = ((x_max - x_min)*x_out + x_min)*dividing_rate
171
172 sales[np.isnan(sales)] = 0
173 # # # # # print(sales)
174 # # # # # print(np.sum(sales))
175 # # # # # print("sales",sales)
176 # # # # print("aa")
177 # # # # print(sales)
178 # # # # print("aa1")
179 if self.response_curve_type == "s-curve":
180 if self.power >= 0:
181 x = x / 10**self.power
182 x = x.astype("float64")
183 K = self.response_curve_params["Kd"]
184 b = self.response_curve_params["b"]
185 a = self.response_curve_params["a"]
186 x0 = self.response_curve_params["x0"]
187 sales = K / (1 + b * np.exp(-a * (x - x0)))
188 if self.response_curve_type == "linear":
189 beta = self.response_curve_params["beta"]
190 sales = beta * x
191
192 return sales
193
194 def get_marginal_roi(self, flag):
195 K = self.response_curve_params["K"]
196 a = self.response_curve_params["a"]
197 # x = self.modified_total_spends
198 # if self.power >= 0 :
199 # x = x / 10**self.power
200 # x = x.astype('float64')
201 # return K*b*a*np.exp(-a*(x-x0)) / (1 + b * np.exp(-a*(x - x0)))**2
202 if flag == "actual":
203 y = self.response_curve(self.actual_spends)
204 # spends_array = self.actual_spends
205 # total_spends = self.actual_total_spends
206 # total_sales = self.actual_total_sales
207
208 else:
209 y = self.response_curve(self.modified_spends)
210 # spends_array = self.modified_spends
211 # total_spends = self.modified_total_spends
212 # total_sales = self.modified_total_sales
213
214 # spends_inc_1 = self._modify_spends(spends_array, total_spends+1)
215 mroi = a * (y) * (1 - y / K)
216 return mroi.sum() / len(self.modified_spends)
217 # spends_inc_1 = self.spends_array + 1
218 # new_total_sales = self.response_curve(spends_inc_1).sum()
219 # return (new_total_sales - total_sales) / len(self.modified_spends)
220
221 def update(self, total_spends):
222 self.modify_spends(total_spends)
223 self.modified_sales = self.calculate_sales()
224 self.modified_total_spends = self.modified_spends.sum()
225 self.modified_total_sales = self.modified_sales.sum()
226 self.delta_spends = self.modified_total_spends - self.actual_total_spends
227 self.delta_sales = self.modified_total_sales - self.actual_total_sales
228
229 def update_bounds_min(self, modified_bound):
230 self.channel_bounds_min = modified_bound
231
232 def update_bounds_max(self, modified_bound):
233 self.channel_bounds_max = modified_bound
234
235 def intialize(self):
236 self.new_spends = self.old_spends
237
238 def __str__(self):
239 return f"{self.name},{self.actual_total_sales}, {self.modified_total_spends}"
240
241 @classmethod
242 def from_dict(cls, attr_dict):
243 return Channel(
244 name=attr_dict["name"],
245 dates=attr_dict["dates"],
246 spends=attr_dict["spends"],
247 bounds=attr_dict["bounds"],
248 modified_spends=attr_dict["modified_spends"],
249 response_curve_type=attr_dict["response_curve_type"],
250 response_curve_params=attr_dict["response_curve_params"],
251 penalty=attr_dict["penalty"],
252 )
253
254 def update_response_curves(self, response_curve_params):
255 self.response_curve_params = response_curve_params
256
257
258class Scenario:
259 def __init__(self, name, channels, constant, correction):
260 self.name = name
261 self.channels = channels
262 self.constant = constant
263 self.correction = correction
264
265 self.actual_total_spends = self.calculate_modified_total_spends()
266 self.actual_total_sales = self.calculate_actual_total_sales()
267 self.modified_total_sales = self.calculate_modified_total_sales()
268 self.modified_total_spends = self.calculate_modified_total_spends()
269 self.delta_spends = self.modified_total_spends - self.actual_total_spends
270 self.delta_sales = self.modified_total_sales - self.actual_total_sales
271
272 def update_penalty(self, value):
273 for channel in self.channels.values():
274 channel.update_penalty(value)
275
276 def calculate_modified_total_spends(self):
277 total_actual_spends = 0.0
278 for channel in self.channels.values():
279 total_actual_spends += channel.actual_total_spends * 1.0
280 return total_actual_spends
281
282 def calculate_modified_total_spends(self):
283 total_modified_spends = 0.0
284 for channel in self.channels.values():
285 # import streamlit as st
286 # st.write(channel.modified_total_spends )
287 total_modified_spends += (
288 channel.modified_total_spends * 1.0
289
290 )
291 return total_modified_spends
292
293 def calculate_actual_total_sales(self):
294 total_actual_sales = 0#self.constant.sum() + self.correction.sum()
295 # # # # print("a")
296 for channel in self.channels.values():
297 total_actual_sales += channel.actual_total_sales
298 # # # # # print(channel.actual_total_sales)
299 # # # # # print(total_actual_sales)
300 return total_actual_sales
301
302 def calculate_modified_total_sales(self):
303
304 total_modified_sales = 0 #self.constant.sum() + self.correction.sum()
305 # # # # print(total_modified_sales)
306 for channel in self.channels.values():
307 # # # # print(channel,channel.modified_total_sales)
308 total_modified_sales += channel.modified_total_sales
309 return total_modified_sales
310
311 def update(self, channel_name, modified_spends):
312 # # # # print("in updtw")
313 self.channels[channel_name].update(modified_spends)
314 self.modified_total_sales = self.calculate_modified_total_sales()
315 self.modified_total_spends = self.calculate_modified_total_spends()
316 self.delta_spends = self.modified_total_spends - self.actual_total_spends
317 self.delta_sales = self.modified_total_sales - self.actual_total_sales
318
319 def update_bounds_min(self, channel_name,modified_bound):
320 # self.modify_spends(total_spends)
321 self.channels[channel_name].update_bounds_min(modified_bound)
322
323 def update_bounds_max(self, channel_name,modified_bound):
324 # self.modify_spends(total_spends)
325 self.channels[channel_name].update_bounds_max(modified_bound)
326
327 # def optimize_spends(self, sales_percent, channels_list, algo="COBYLA"):
328 # desired_sales = self.actual_total_sales * (1 + sales_percent / 100.0)
329
330 # def constraint(x):
331 # for ch, spends in zip(channels_list, x):
332 # self.update(ch, spends)
333 # return self.modified_total_sales - desired_sales
334
335 # bounds = []
336 # for ch in channels_list:
337 # bounds.append(
338 # (1 + np.array([-50.0, 100.0]) / 100.0)
339 # * self.channels[ch].actual_total_spends
340 # )
341
342 # initial_point = []
343 # for bound in bounds:
344 # initial_point.append(bound[0])
345
346 # power = np.ceil(np.log(sum(initial_point)) / np.log(10))
347
348 # constraints = [NonlinearConstraint(constraint, -1.0, 1.0)]
349
350 # res = minimize(
351 # lambda x: sum(x) / 10 ** (power),
352 # bounds=bounds,
353 # x0=initial_point,
354 # constraints=constraints,
355 # method=algo,
356 # options={"maxiter": int(2e7), "catol": 1},
357 # )
358
359 # for channel_name, modified_spends in zip(channels_list, res.x):
360 # self.update(channel_name, modified_spends)
361
362 # return zip(channels_list, res.x)
363
364
365
366
367
368
369 def optimize_spends(self, sales_percent, channels_list, algo="trust-constr"):
370 num_channels = len(channels_list)
371 # # # # # print("%"*100)
372 desired_sales = self.actual_total_sales * (1 + sales_percent / 100.0)
373
374 def constraint(x):
375 for ch, spends in zip(channels_list, x):
376 self.update(ch, spends)
377 return self.modified_total_sales - desired_sales
378
379 # def calc_overall_bounds(channels_list):
380 # total_spends=0
381 # for ch in zip(channels_list):
382 # # print(ch)
383 # total_spends= total_spends+self.channels[ch].actual_total_spends
384 # return total_spends
385
386
387 bounds = []
388 for ch in channels_list:
389 # bounds.append(
390 # (1+np.array([-50.0, 100.0]) / 100.0)
391 # * self.channels[ch].actual_total_spends
392 # )
393 lb = (1- int(self.channels[ch].channel_bounds_min) / 100) * self.channels[ch].actual_total_spends
394 ub = (1+ int(self.channels[ch].channel_bounds_max) / 100) * self.channels[ch].actual_total_spends
395 bounds.append((lb,ub))
396 # # # # # print(self.channels[ch].actual_total_spends)
397 initial_point = []
398 for bound in bounds:
399 initial_point.append(bound[0])
400 # initial_point = np.nan_to_num(initial_point, nan=0.0, posinf=0.0, neginf=0.0)
401
402 power = np.ceil(np.log(sum(initial_point)) / np.log(10))
403
404 constraints = [NonlinearConstraint(constraint, -1.0, 1.0),
405 # LinearConstraint(np.ones((num_channels,)), lb = -50*calc_overall_bounds(channels_list), ub = 50*calc_overall_bounds(channels_list))
406 ]
407
408 res = minimize(
409 lambda x: sum(x) / 10 ** (power),
410 bounds=bounds,
411 x0=initial_point,
412 constraints=constraints,
413 method=algo,
414 options={"maxiter": int(2e7), "xtol": 10},
415 )
416
417 for channel_name, modified_spends in zip(channels_list, res.x):
418 self.update(channel_name, modified_spends)
419
420 return zip(channels_list, res.x)
421
422
423
424 def optimize(self, spends_percent, channels_list):
425 # channels_list = self.channels.keys()
426 num_channels = len(channels_list)
427 spends_constant = []
428 spends_constraint = 0.0
429 for channel_name in channels_list:
430 # spends_constraint += self.channels[channel_name].modified_total_spends
431 spends_constant.append(self.channels[channel_name].conversion_rate)
432 # # # # print(spends_constant)
433 spends_constraint += (
434 self.channels[channel_name].actual_total_spends+ self.channels[channel_name].delta_spends #st.session_state["total_spends_change_abs_slider_options"]
435 )
436 # # # # print("delta spends",self.channels[channel_name].delta_spends)
437 # spends_constraint = spends_constraint * (1 + spends_percent / 100)
438 constraint= LinearConstraint(np.ones((num_channels,)), lb = spends_constraint, ub = spends_constraint)
439 # constraint = LinearConstraint(
440 # np.array(spends_constant),
441 # lb=spends_constraint,
442 # ub=spends_constraint,
443 # )
444 bounds = []
445 old_spends = []
446 for channel_name in channels_list:
447 _channel_class = self.channels[channel_name]
448 channel_bounds = _channel_class.bounds
449 channel_actual_total_spends = _channel_class.actual_total_spends + _channel_class.delta_spends
450 # * (
451 # (1 + _channel_class.delta_spends / 100)
452 # )
453 old_spends.append(channel_actual_total_spends)
454 # bounds.append((1+ channel_bounds / 100) * channel_actual_total_spends)
455 lb = (1- int(_channel_class.channel_bounds_min) / 100) * _channel_class.actual_total_spends
456 ub = (1+ int(_channel_class.channel_bounds_max) / 100) * _channel_class.actual_total_spends
457 bounds.append((lb,ub))
458 # # # # print("aaaaaa")
459 # # # print((_channel_class.channel_bounds_max,_channel_class.channel_bounds_min))
460 # _channel_class.channel_bounds_min
461 # _channel_class.channel_bounds_max
462 def cost_func1(channel,x):
463 response_curve_params = pd.read_excel("response_curves_parameters.xlsx",index_col = "channel")
464 param_dicts = {col: response_curve_params[col].to_dict() for col in response_curve_params.columns}
465
466 Kd= param_dicts["Kd"][channel]
467 n= param_dicts["n"][channel]
468 x_min= param_dicts["x_min"][channel]
469 x_max= param_dicts["x_max"][channel]
470 y_min= param_dicts["y_min"][channel]
471 y_max= param_dicts['y_max'][channel]
472 division_parameter = param_dicts['num_pos_obsv'][channel]
473 x_inp = ( x/division_parameter- x_min) / (x_max - x_min)
474 # # # # print(x_inp)
475 x_out = x_inp**n / (Kd**n + x_inp**n)
476 x_val_inv = (x_out*x_max + (1 - x_out) * x_min)
477 sales = (x_val_inv*y_min/y_max)*division_parameter
478 if np.isnan(sales):
479 # # # # print(sales,channel)
480 sales = 0
481 # # # # print(sales,channel)
482 return sales
483 def objective_function(x):
484 sales = 0
485 it = 0
486 for channel_name, modified_spends in zip(channels_list, x):
487 # sales = sales + cost_func1(channel_name,modified_spends)
488 # # print(channel_name, modified_spends,cost_func1(channel_name, modified_spends))
489 it+=1
490 self.update(channel_name, modified_spends)
491 # # # # print(self.modified_total_sales)
492 # # # # print(channel_name, modified_spends)
493 return -1 * self.modified_total_sales
494
495 # # # # print(bounds)
496 # # # # # print("$"*100)
497 res = minimize(
498 lambda x: objective_function(x)/1e3,
499 method="trust-constr",
500 x0=old_spends,
501 constraints=constraint,
502 bounds=bounds,
503 options={"maxiter": int(1e7), "xtol": 0.1},
504 )
505
506 for channel_name, modified_spends in zip(channels_list, res.x):
507 # # # # print("aaaaaaaaaaaaaa")
508 self.update(channel_name, modified_spends)
509 # # # # print(channel_name, modified_spends,cost_func1(channel_name, modified_spends))
510
511 # # print(it)
512
513 return zip(channels_list, res.x)
514
515
516 def hill_equation(self,x, Kd, n):
517 return x**n / (Kd**n + x**n)
518
519
520
521 # def spends_optimisation(self, spends_percent,channels_list):
522 # m = GEKKO(remote=False)
523 # # Define variables
524 # # Initialize 13 variables with specific bounds
525 # response_curve_params = pd.read_excel(r"C:\Users\PragyaJatav\Downloads\Untitled Folder 2\simulator uploaded - Copy\Simulator-UOPX\response_curves_parameters.xlsx",index_col = "channel")
526 # param_dicts = {col: response_curve_params[col].to_dict() for col in response_curve_params.columns}
527
528 # initial_values = list(param_dicts["x_min"].values())
529 # current_spends = list(param_dicts["current_spends"].values())
530 # lower_bounds = list(param_dicts["x_min"].values())
531
532 # num_channels = len(channels_list)
533
534 # x_vars=[]
535 # x_vars = [m.Var(value=param_dicts["current_spends"][_], lb=param_dicts["x_min"][_]*104, ub=5*param_dicts["current_spends"][_]) for _ in channels_list]
536 # # # # # print(x_vars)
537 # # x_vars,lower_bounds
538
539 # # Define the objective function to minimize
540 # cost = 0
541 # spends = 0
542 # i = 0
543 # for i,c in enumerate(channels_list):
544 # # # # # # print(c)
545 # # # # # # print(x_vars[i])
546 # cost = cost + (self.cost_func(c, x_vars[i]))
547 # spends = spends +x_vars[i]
548
549
550 # m.Maximize(cost)
551
552 # # Define constraints
553 # m.Equation(spends == sum(current_spends)*(1 + spends_percent / 100))
554 # m.Equation(spends <= sum(current_spends)*0.5)
555 # m.Equation(spends >= sum(current_spends)*1.5)
556
557 # m.solve(disp=True)
558
559 # for i, var in enumerate(x_vars):
560 # # # # # print(f"x{i+1} = {var.value[0]}")
561
562 # for channel_name, modified_spends in zip(channels_list, x_vars):
563 # self.update(channel_name, modified_spends.value[0])
564
565 # return zip(channels_list, x_vars)
566
567 def save(self):
568 details = {}
569 actual_list = []
570 modified_list = []
571 data = {}
572 channel_data = []
573
574 summary_rows = []
575 actual_list.append(
576 {
577 "name": "Total",
578 "Spends": self.actual_total_spends,
579 "Sales": self.actual_total_sales,
580 }
581 )
582 modified_list.append(
583 {
584 "name": "Total",
585 "Spends": self.modified_total_spends,
586 "Sales": self.modified_total_sales,
587 }
588 )
589 for channel in self.channels.values():
590 name_mod = channel.name.replace("_", " ")
591 if name_mod.lower().endswith(" imp"):
592 name_mod = name_mod.replace("Imp", " Impressions")
593 summary_rows.append(
594 [
595 name_mod,
596 channel.actual_total_spends,
597 channel.modified_total_spends,
598 channel.actual_total_sales,
599 channel.modified_total_sales,
600 round(channel.actual_total_sales / channel.actual_total_spends, 2),
601 round(
602 channel.modified_total_sales / channel.modified_total_spends,
603 2,
604 ),
605 channel.get_marginal_roi("actual"),
606 channel.get_marginal_roi("modified"),
607 ]
608 )
609 data[channel.name] = channel.modified_spends
610 data["Date"] = channel.dates
611 data["Sales"] = (
612 data.get("Sales", np.zeros((len(channel.dates),)))
613 + channel.modified_sales
614 )
615 actual_list.append(
616 {
617 "name": channel.name,
618 "Spends": channel.actual_total_spends,
619 "Sales": channel.actual_total_sales,
620 "ROI": round(
621 channel.actual_total_sales / channel.actual_total_spends, 2
622 ),
623 }
624 )
625 modified_list.append(
626 {
627 "name": channel.name,
628 "Spends": channel.modified_total_spends,
629 "Sales": channel.modified_total_sales,
630 "ROI": round(
631 channel.modified_total_sales / channel.modified_total_spends,
632 2,
633 ),
634 "Marginal ROI": channel.get_marginal_roi("modified"),
635 }
636 )
637
638 channel_data.append(
639 {
640 "channel": channel.name,
641 "spends_act": channel.actual_total_spends,
642 "spends_mod": channel.modified_total_spends,
643 "sales_act": channel.actual_total_sales,
644 "sales_mod": channel.modified_total_sales,
645 }
646 )
647 summary_rows.append(
648 [
649 "Total",
650 self.actual_total_spends,
651 self.modified_total_spends,
652 self.actual_total_sales,
653 self.modified_total_sales,
654 round(self.actual_total_sales / self.actual_total_spends, 2),
655 round(self.modified_total_sales / self.modified_total_spends, 2),
656 0.0,
657 0.0,
658 ]
659 )
660 details["Actual"] = actual_list
661 details["Modified"] = modified_list
662 columns_index = pd.MultiIndex.from_product(
663 [[""], ["Channel"]], names=["first", "second"]
664 )
665 columns_index = columns_index.append(
666 pd.MultiIndex.from_product(
667 [["Spends", "NRPU", "ROI", "MROI"], ["Actual", "Simulated"]],
668 names=["first", "second"],
669 )
670 )
671 details["Summary"] = pd.DataFrame(summary_rows, columns=columns_index)
672 data_df = pd.DataFrame(data)
673 channel_list = list(self.channels.keys())
674 data_df = data_df[["Date", *channel_list, "Sales"]]
675
676 details["download"] = {
677 "data_df": data_df,
678 "channels_df": pd.DataFrame(channel_data),
679 "total_spends_act": self.actual_total_spends,
680 "total_sales_act": self.actual_total_sales,
681 "total_spends_mod": self.modified_total_spends,
682 "total_sales_mod": self.modified_total_sales,
683 }
684
685 return details
686
687 @classmethod
688 def from_dict(cls, attr_dict):
689 channels_list = attr_dict["channels"]
690 channels = {
691 channel["name"]: class_from_dict(channel) for channel in channels_list
692 }
693 return Scenario(
694 name=attr_dict["name"],
695 channels=channels,
696 constant=attr_dict["constant"],
697 correction=attr_dict["correction"],
698 )
699 