|
|
|
import streamlit as st |
|
|
|
st.set_page_config( |
|
page_title="Scenario Planner", |
|
page_icon="⚖️", |
|
layout="wide", |
|
initial_sidebar_state="collapsed", |
|
) |
|
|
|
import os |
|
import math |
|
import pickle |
|
import sqlite3 |
|
import numpy as np |
|
from classes import numerize |
|
import plotly.graph_objects as go |
|
from collections import OrderedDict |
|
from scipy.optimize import minimize |
|
from utilities import project_selection, initialize_data, set_header, load_local_css |
|
from utilities import ( |
|
get_panels_names, |
|
get_metrics_names, |
|
name_formating, |
|
load_json_files, |
|
load_pickle_files, |
|
generate_rcs_data, |
|
generate_scenario_data, |
|
) |
|
|
|
|
|
if "roi_threshold" not in st.session_state: |
|
st.session_state.roi_threshold = 1 |
|
|
|
|
|
if "message_display" not in st.session_state: |
|
st.session_state.message_display = {"type": "success", "message": None, "icon": ""} |
|
|
|
|
|
|
|
def reset_scenario(metrics_selected=None, panel_selected=None): |
|
|
|
st.session_state.message_display = {"type": "success", "message": None, "icon": ""} |
|
|
|
|
|
if metrics_selected is None: |
|
metrics_selected = st.session_state["response_metrics_selectbox_sp"] |
|
if panel_selected is None: |
|
panel_selected = st.session_state["response_panel_selectbox_sp"] |
|
|
|
|
|
original_pickle_file_path = os.path.join( |
|
st.session_state["project_path"], "scenario_data_original.pkl" |
|
) |
|
modified_pickle_file_path = os.path.join( |
|
st.session_state["project_path"], "scenario_data_modified.pkl" |
|
) |
|
|
|
|
|
try: |
|
|
|
with open(original_pickle_file_path, "rb") as original_pickle_file: |
|
original_data = pickle.load(original_pickle_file) |
|
original_scenario_data = original_data[metrics_selected][panel_selected] |
|
|
|
|
|
with open(modified_pickle_file_path, "rb+") as modified_pickle_file: |
|
data = pickle.load(modified_pickle_file) |
|
|
|
data[metrics_selected][panel_selected] = original_scenario_data |
|
|
|
modified_pickle_file.seek(0) |
|
pickle.dump(data, modified_pickle_file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
return |
|
|
|
|
|
|
|
def s_curve(x, power, K, b, a, x0): |
|
return K / (1 + b * np.exp(-a * ((x / 10**power) - x0))) |
|
|
|
|
|
|
|
def get_s_curve_params( |
|
metrics_selected, |
|
panel_selected, |
|
channel_selected, |
|
original_json_data, |
|
modified_json_data, |
|
modified_pickle_file_path, |
|
): |
|
|
|
power = original_json_data[metrics_selected][panel_selected][channel_selected][ |
|
"power" |
|
] |
|
|
|
|
|
s_curve_param = modified_json_data[metrics_selected][panel_selected][ |
|
channel_selected |
|
] |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "rb") as file: |
|
data = pickle.load(file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
data[metrics_selected][panel_selected]["channels"][channel_selected][ |
|
"response_curve_params" |
|
] = s_curve_param |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "wb") as file: |
|
pickle.dump(data, file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
s_curve_param["power"] = power |
|
|
|
|
|
return s_curve_param |
|
|
|
|
|
|
|
def get_total_contribution( |
|
spends, channels, s_curve_params, channels_proportion, modified_scenario_data |
|
): |
|
total_contribution = 0 |
|
for i in range(len(channels)): |
|
channel_name = channels[i] |
|
channel_s_curve_params = s_curve_params[channel_name] |
|
spend_proportion = spends[i] * channels_proportion[channel_name] |
|
total_contribution += sum( |
|
s_curve( |
|
spend_proportion, |
|
channel_s_curve_params["power"], |
|
channel_s_curve_params["K"], |
|
channel_s_curve_params["b"], |
|
channel_s_curve_params["a"], |
|
channel_s_curve_params["x0"], |
|
) |
|
) |
|
return total_contribution + sum(modified_scenario_data["constant"]) |
|
|
|
|
|
|
|
def get_total_spends(spends, channels_conversion_ratio): |
|
return np.sum(spends * np.array(list(channels_conversion_ratio.values()))) |
|
|
|
|
|
|
|
def optimizer( |
|
optimization_goal, |
|
s_curve_params, |
|
channels_spends, |
|
channels_proportion, |
|
channels_conversion_ratio, |
|
total_target, |
|
bounds_dict, |
|
modified_scenario_data, |
|
): |
|
|
|
channels = list(channels_spends.keys()) |
|
actual_spends = np.array(list(channels_spends.values())) |
|
num_channels = len(actual_spends) |
|
|
|
|
|
def objective_fun(spends): |
|
if optimization_goal == "Spends": |
|
|
|
return -get_total_contribution( |
|
spends, |
|
channels, |
|
s_curve_params, |
|
channels_proportion, |
|
modified_scenario_data, |
|
) |
|
else: |
|
|
|
return get_total_spends(spends, channels_conversion_ratio) |
|
|
|
def constraint_fun(spends): |
|
if optimization_goal == "Spends": |
|
|
|
return get_total_spends(spends, channels_conversion_ratio) |
|
else: |
|
|
|
return get_total_contribution( |
|
spends, |
|
channels, |
|
s_curve_params, |
|
channels_proportion, |
|
modified_scenario_data, |
|
) |
|
|
|
|
|
constraints = { |
|
"type": "eq", |
|
"fun": lambda spends: constraint_fun(spends) - total_target, |
|
} |
|
|
|
|
|
bounds = [ |
|
( |
|
actual_spends[i] * (1 + bounds_dict[channels[i]][0] / 100), |
|
actual_spends[i] * (1 + bounds_dict[channels[i]][1] / 100), |
|
) |
|
for i in range(num_channels) |
|
] |
|
|
|
|
|
initial_guess = np.array(actual_spends) |
|
|
|
|
|
xtol = max(10, 0.001 * np.min(actual_spends)) |
|
|
|
|
|
result = minimize( |
|
objective_fun, |
|
initial_guess, |
|
method="trust-constr", |
|
constraints=constraints, |
|
bounds=bounds, |
|
options={ |
|
"disp": True, |
|
"xtol": xtol, |
|
"maxiter": 1e5, |
|
}, |
|
) |
|
|
|
|
|
print(result) |
|
|
|
|
|
optimized_spends_array = result.x |
|
|
|
|
|
optimized_spends = { |
|
channels[i]: optimized_spends_array[i] for i in range(num_channels) |
|
} |
|
|
|
return optimized_spends, result.success |
|
|
|
|
|
|
|
@st.cache_data(show_spinner=False) |
|
def max_target_achievable( |
|
channels_spends, |
|
s_curve_params, |
|
channels_proportion, |
|
modified_scenario_data, |
|
bounds_dict, |
|
): |
|
|
|
channels = list(channels_spends.keys()) |
|
actual_spends = np.array(list(channels_spends.values())) |
|
num_channels = len(actual_spends) |
|
|
|
|
|
lower_spends, upper_spends = [], [] |
|
for i in range(num_channels): |
|
lower_spends.append(actual_spends[i] * (1 + bounds_dict[channels[i]][0] / 100)) |
|
upper_spends.append(actual_spends[i] * (1 + bounds_dict[channels[i]][1] / 100)) |
|
|
|
|
|
lower_achievable_target = get_total_contribution( |
|
lower_spends, |
|
channels, |
|
s_curve_params, |
|
channels_proportion, |
|
modified_scenario_data, |
|
) |
|
upper_achievable_target = get_total_contribution( |
|
upper_spends, |
|
channels, |
|
s_curve_params, |
|
channels_proportion, |
|
modified_scenario_data, |
|
) |
|
|
|
|
|
return max(0, 1.001 * lower_achievable_target), 0.999 * upper_achievable_target |
|
|
|
|
|
|
|
def is_valid_number_format(number_str): |
|
|
|
if number_str is None: |
|
|
|
st.session_state.message_display = { |
|
"type": "warning", |
|
"message": "Invalid input: Please enter a valid number.", |
|
"icon": "⚠️", |
|
} |
|
return False |
|
|
|
|
|
valid_suffixes = {"K", "M", "B", "T"} |
|
|
|
|
|
if number_str[0] == "-": |
|
|
|
st.session_state.message_display = { |
|
"type": "warning", |
|
"message": "Invalid input: Please enter a valid number.", |
|
"icon": "⚠️", |
|
} |
|
return False |
|
|
|
|
|
if number_str[-1].isdigit(): |
|
try: |
|
|
|
number = float(number_str) |
|
|
|
if number >= 0: |
|
return True |
|
else: |
|
|
|
st.session_state.message_display = { |
|
"type": "warning", |
|
"message": "Invalid input: Please enter a valid number.", |
|
"icon": "⚠️", |
|
} |
|
return False |
|
except ValueError: |
|
|
|
st.session_state.message_display = { |
|
"type": "warning", |
|
"message": "Invalid input: Please enter a valid number.", |
|
"icon": "⚠️", |
|
} |
|
return False |
|
|
|
|
|
suffix = number_str[-1].upper() |
|
if suffix in valid_suffixes: |
|
num_part = number_str[:-1] |
|
try: |
|
|
|
number = float(num_part) |
|
|
|
if number >= 0: |
|
return True |
|
else: |
|
|
|
st.session_state.message_display = { |
|
"type": "warning", |
|
"message": "Invalid input: Please enter a valid number.", |
|
"icon": "⚠️", |
|
} |
|
return False |
|
except ValueError: |
|
|
|
st.session_state.message_display = { |
|
"type": "warning", |
|
"message": "Invalid input: Please enter a valid number.", |
|
"icon": "⚠️", |
|
} |
|
return False |
|
|
|
|
|
st.session_state.message_display = { |
|
"type": "warning", |
|
"message": "Invalid input: Please enter a valid number.", |
|
"icon": "⚠️", |
|
} |
|
return False |
|
|
|
|
|
|
|
def convert_to_float(number_str): |
|
|
|
multipliers = { |
|
"K": 1e3, |
|
"M": 1e6, |
|
"B": 1e9, |
|
"T": 1e12, |
|
} |
|
|
|
|
|
if number_str[-1].isdigit(): |
|
return float(number_str) |
|
|
|
|
|
suffix = number_str[-1].upper() |
|
num_part = number_str[:-1] |
|
|
|
|
|
return float(num_part) * multipliers[suffix] |
|
|
|
|
|
|
|
def absolute_channel_spends_change( |
|
channel_key, |
|
channel_spends_actual, |
|
channel, |
|
metrics_selected, |
|
panel_selected, |
|
modified_pickle_file_path, |
|
): |
|
|
|
if not is_valid_number_format(st.session_state[f"{channel_key}_abs_spends_key"]): |
|
return |
|
|
|
|
|
new_absolute_spends = convert_to_float( |
|
st.session_state[f"{channel_key}_abs_spends_key"] |
|
) |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "rb") as file: |
|
data = pickle.load(file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
total_channel_spends = 0 |
|
for current_channel in list( |
|
data[metrics_selected][panel_selected]["channels"].keys() |
|
): |
|
|
|
channel_key = f"{metrics_selected}_{panel_selected}_{current_channel}" |
|
|
|
total_channel_spends += convert_to_float( |
|
st.session_state[f"{channel_key}_abs_spends_key"] |
|
) |
|
|
|
|
|
if ( |
|
total_channel_spends |
|
< 1.5 * data[metrics_selected][panel_selected]["actual_total_spends"] |
|
and total_channel_spends |
|
> 0.5 * data[metrics_selected][panel_selected]["actual_total_spends"] |
|
): |
|
|
|
data[metrics_selected][panel_selected]["channels"][channel][ |
|
"modified_total_spends" |
|
] = new_absolute_spends / float( |
|
data[metrics_selected][panel_selected]["channels"][channel][ |
|
"conversion_rate" |
|
] |
|
) |
|
|
|
|
|
data[metrics_selected][panel_selected][ |
|
"modified_total_spends" |
|
] = total_channel_spends |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "wb") as file: |
|
pickle.dump(data, file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
else: |
|
|
|
st.session_state.message_display = { |
|
"type": "warning", |
|
"message": "Keep total spending within ±50% of the original value.", |
|
"icon": "⚠️", |
|
} |
|
|
|
|
|
|
|
def percentage_channel_spends_change( |
|
channel_key, |
|
channel_spends_actual, |
|
channel, |
|
metrics_selected, |
|
panel_selected, |
|
modified_pickle_file_path, |
|
): |
|
|
|
percentage_channel_spends = round( |
|
st.session_state[f"{channel_key}_per_spends_key"], 0 |
|
) |
|
|
|
|
|
new_absolute_spends = channel_spends_actual * (1 + percentage_channel_spends / 100) |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "rb") as file: |
|
data = pickle.load(file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
total_channel_spends = 0 |
|
for current_channel in list( |
|
data[metrics_selected][panel_selected]["channels"].keys() |
|
): |
|
|
|
channel_key = f"{metrics_selected}_{panel_selected}_{current_channel}" |
|
|
|
|
|
current_channel_spends_actual = data[metrics_selected][panel_selected][ |
|
"channels" |
|
][current_channel]["actual_total_spends"] |
|
|
|
|
|
current_channel_conversion_rate = data[metrics_selected][panel_selected][ |
|
"channels" |
|
][current_channel]["conversion_rate"] |
|
|
|
|
|
current_channel_absolute_spends = ( |
|
current_channel_spends_actual |
|
* current_channel_conversion_rate |
|
* (1 + st.session_state[f"{channel_key}_per_spends_key"] / 100) |
|
) |
|
|
|
total_channel_spends += current_channel_absolute_spends |
|
|
|
|
|
if ( |
|
total_channel_spends |
|
< 1.5 * data[metrics_selected][panel_selected]["actual_total_spends"] |
|
and total_channel_spends |
|
> 0.5 * data[metrics_selected][panel_selected]["actual_total_spends"] |
|
): |
|
|
|
data[metrics_selected][panel_selected]["channels"][channel][ |
|
"modified_total_spends" |
|
] = float(new_absolute_spends) / float( |
|
data[metrics_selected][panel_selected]["channels"][channel][ |
|
"conversion_rate" |
|
] |
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "wb") as file: |
|
pickle.dump(data, file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
|
|
def total_input_change(modified_pickle_file_path, per_change): |
|
|
|
try: |
|
with open(modified_pickle_file_path, "rb") as file: |
|
data = pickle.load(file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
channel_list = list(data[metrics_selected][panel_selected]["channels"].keys()) |
|
|
|
|
|
|
|
|
|
|
|
for channel in channel_list: |
|
|
|
channel_actual_spends = data[metrics_selected][panel_selected]["channels"][ |
|
channel |
|
]["actual_total_spends"] |
|
|
|
|
|
modified_channel_metrics = channel_actual_spends * ((100 + per_change) / 100) |
|
|
|
|
|
data[metrics_selected][panel_selected]["channels"][channel][ |
|
"modified_total_spends" |
|
] = modified_channel_metrics |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "wb") as file: |
|
pickle.dump(data, file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
|
|
def total_absolute_main_key_change( |
|
metrics_selected, panel_selected, modified_pickle_file_path, optimization_goal |
|
): |
|
|
|
if not is_valid_number_format(st.session_state["total_absolute_main_key"]): |
|
return |
|
|
|
|
|
new_absolute = convert_to_float(st.session_state["total_absolute_main_key"]) |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "rb") as file: |
|
data = pickle.load(file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
if optimization_goal == "Spends": |
|
|
|
old_absolute = data[metrics_selected][panel_selected]["actual_total_spends"] |
|
else: |
|
|
|
old_absolute = data[metrics_selected][panel_selected]["actual_total_sales"] |
|
|
|
|
|
lower_bound = old_absolute * 0.5 |
|
upper_bound = old_absolute * 1.5 |
|
|
|
|
|
if new_absolute < lower_bound or new_absolute > upper_bound: |
|
new_absolute = old_absolute |
|
|
|
if optimization_goal == "Spends": |
|
|
|
data[metrics_selected][panel_selected]["modified_total_spends"] = new_absolute |
|
else: |
|
|
|
data[metrics_selected][panel_selected]["modified_total_sales"] = new_absolute |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "wb") as file: |
|
pickle.dump(data, file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
if optimization_goal == "Spends": |
|
per_change = ((new_absolute - old_absolute) / old_absolute) * 100 |
|
total_input_change(modified_pickle_file_path, per_change) |
|
|
|
|
|
|
|
def total_absolute_key_change( |
|
metrics_selected, panel_selected, modified_pickle_file_path, optimization_goal |
|
): |
|
|
|
new_absolute = convert_to_float(st.session_state["total_absolute_key"]) |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "rb") as file: |
|
data = pickle.load(file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
if optimization_goal == "Spends": |
|
|
|
data[metrics_selected][panel_selected]["modified_total_spends"] = new_absolute |
|
old_absolute = data[metrics_selected][panel_selected]["actual_total_spends"] |
|
else: |
|
|
|
data[metrics_selected][panel_selected]["modified_total_sales"] = new_absolute |
|
old_absolute = data[metrics_selected][panel_selected]["actual_total_sales"] |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "wb") as file: |
|
pickle.dump(data, file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
if optimization_goal == "Spends": |
|
per_change = ((new_absolute - old_absolute) / old_absolute) * 100 |
|
total_input_change(modified_pickle_file_path, per_change) |
|
|
|
|
|
|
|
def total_percentage_key_change( |
|
metrics_selected, |
|
panel_selected, |
|
modified_pickle_file_path, |
|
absolute_value, |
|
optimization_goal, |
|
): |
|
|
|
new_absolute = absolute_value * (1 + st.session_state["total_percentage_key"] / 100) |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "rb") as file: |
|
data = pickle.load(file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
if optimization_goal == "Spends": |
|
|
|
data[metrics_selected][panel_selected]["modified_total_spends"] = new_absolute |
|
old_absolute = data[metrics_selected][panel_selected]["actual_total_spends"] |
|
else: |
|
|
|
data[metrics_selected][panel_selected]["modified_total_sales"] = new_absolute |
|
old_absolute = data[metrics_selected][panel_selected]["actual_total_sales"] |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "wb") as file: |
|
pickle.dump(data, file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
if optimization_goal == "Spends": |
|
per_change = ((new_absolute - old_absolute) / old_absolute) * 100 |
|
total_input_change(modified_pickle_file_path, per_change) |
|
|
|
|
|
|
|
def bound_change( |
|
metrics_selected, panel_selected, modified_pickle_file_path, channel_key, channel |
|
): |
|
|
|
new_lower_bound = st.session_state[f"{channel_key}_lower_key"] |
|
new_upper_bound = st.session_state[f"{channel_key}_upper_key"] |
|
if new_lower_bound > new_upper_bound: |
|
new_bounds = [-10, 10] |
|
|
|
|
|
st.session_state.message_display = { |
|
"type": "warning", |
|
"message": "Lower bound cannot be greater than Upper bound.", |
|
"icon": "⚠️", |
|
} |
|
|
|
else: |
|
new_bounds = [new_lower_bound, new_upper_bound] |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "rb") as file: |
|
data = pickle.load(file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
data[metrics_selected][panel_selected]["channels"][channel]["bounds"] = new_bounds |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "wb") as file: |
|
pickle.dump(data, file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
|
|
def freeze_change( |
|
metrics_selected, |
|
panel_selected, |
|
modified_pickle_file_path, |
|
channel_key, |
|
channel, |
|
): |
|
if st.session_state[f"{channel_key}_allow_optimize_key"]: |
|
|
|
new_lower_bound, new_upper_bound = 0, 0 |
|
new_bounds = [new_lower_bound, new_upper_bound] |
|
new_freeze = True |
|
else: |
|
|
|
new_lower_bound, new_upper_bound = -10, 10 |
|
new_bounds = [new_lower_bound, new_upper_bound] |
|
new_freeze = False |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "rb") as file: |
|
data = pickle.load(file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
data[metrics_selected][panel_selected]["channels"][channel]["bounds"] = new_bounds |
|
data[metrics_selected][panel_selected]["channels"][channel]["freeze"] = new_freeze |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "wb") as file: |
|
pickle.dump(data, file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
|
|
def get_point_parms( |
|
x_val, current_s_curve_params, current_channel_proportion, current_conversion_rate |
|
): |
|
|
|
y_val = sum( |
|
s_curve( |
|
(x_val * current_channel_proportion), |
|
current_s_curve_params["power"], |
|
current_s_curve_params["K"], |
|
current_s_curve_params["b"], |
|
current_s_curve_params["a"], |
|
current_s_curve_params["x0"], |
|
) |
|
) |
|
|
|
|
|
nudge = 1e-3 |
|
x1 = float(x_val * current_conversion_rate) |
|
y1 = float(y_val) |
|
x2 = x1 + nudge |
|
y2 = sum( |
|
s_curve( |
|
((x2 / current_conversion_rate) * current_channel_proportion), |
|
current_s_curve_params["power"], |
|
current_s_curve_params["K"], |
|
current_s_curve_params["b"], |
|
current_s_curve_params["a"], |
|
current_s_curve_params["x0"], |
|
) |
|
) |
|
mroi_val = (float(y2) - y1) / (x2 - x1) if x2 != x1 else 0 |
|
|
|
|
|
roi_val = y_val / (x_val * current_conversion_rate) |
|
|
|
return roi_val, mroi_val, y_val |
|
|
|
|
|
|
|
def find_segment_value(x, roi, mroi, roi_threshold=1, mroi_threshold=0.05): |
|
|
|
start_value = x[0] |
|
end_value = x[-1] |
|
|
|
|
|
green_condition = (roi > roi_threshold) & (mroi > mroi_threshold) |
|
|
|
|
|
left_indices = np.where(roi > roi_threshold)[0] |
|
|
|
|
|
right_indices = np.where(green_condition)[0] |
|
|
|
|
|
left_value = x[left_indices[0]] if left_indices.size > 0 else x[0] |
|
|
|
|
|
right_value = x[right_indices[-1]] if right_indices.size > 0 else x[0] |
|
|
|
|
|
if left_value > right_value: |
|
left_value = right_value |
|
|
|
return start_value, end_value, left_value, right_value |
|
|
|
|
|
|
|
@st.cache_data(show_spinner=False) |
|
def generate_response_curve_plots( |
|
channel_list, s_curve_params, channels_proportion, original_scenario_data |
|
): |
|
figures, channel_roi_mroi, region_start_end = [], {}, {} |
|
|
|
for channel in channel_list: |
|
spends_actual = original_scenario_data["channels"][channel][ |
|
"actual_total_spends" |
|
] |
|
conversion_rate = original_scenario_data["channels"][channel]["conversion_rate"] |
|
|
|
x_actual = np.linspace(0, 5 * spends_actual, 100) |
|
x_plot = x_actual * conversion_rate |
|
|
|
|
|
y_plot = [ |
|
sum( |
|
s_curve( |
|
(x * channels_proportion[channel]), |
|
s_curve_params[channel]["power"], |
|
s_curve_params[channel]["K"], |
|
s_curve_params[channel]["b"], |
|
s_curve_params[channel]["a"], |
|
s_curve_params[channel]["x0"], |
|
) |
|
) |
|
for x in x_actual |
|
] |
|
|
|
|
|
roi = [float(y) / float(x) if x != 0 else 0 for x, y in zip(x_plot, y_plot)] |
|
|
|
|
|
nudge = 1e-3 |
|
mroi = [] |
|
for i in range(len(x_plot)): |
|
x1 = float(x_plot[i]) |
|
y1 = float(y_plot[i]) |
|
x2 = x1 + nudge |
|
y2 = sum( |
|
s_curve( |
|
((x2 / conversion_rate) * channels_proportion[channel]), |
|
s_curve_params[channel]["power"], |
|
s_curve_params[channel]["K"], |
|
s_curve_params[channel]["b"], |
|
s_curve_params[channel]["a"], |
|
s_curve_params[channel]["x0"], |
|
) |
|
) |
|
mroi_value = (float(y2) - y1) / (x2 - x1) if x2 != x1 else 0 |
|
mroi.append(mroi_value) |
|
|
|
|
|
roi_actual, mroi_actual, y_actual = get_point_parms( |
|
spends_actual, |
|
s_curve_params[channel], |
|
channels_proportion[channel], |
|
conversion_rate, |
|
) |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
fig.add_trace( |
|
go.Scatter( |
|
x=x_plot, |
|
y=y_plot, |
|
mode="lines", |
|
name="Metrics", |
|
hoverinfo="text", |
|
text=[ |
|
f"Spends: {numerize(x)}<br>{metrics_selected_formatted}: {numerize(y)}<br>ROI: {r:.2f}<br>MROI: {m:.2f}" |
|
for x, y, r, m in zip(x_plot, y_plot, roi, mroi) |
|
], |
|
) |
|
) |
|
|
|
|
|
fig.add_trace( |
|
go.Scatter( |
|
x=[spends_actual * conversion_rate], |
|
y=[y_actual], |
|
mode="markers", |
|
marker=dict(color="cyan", size=10, symbol="circle"), |
|
name="Actual Spend", |
|
hoverinfo="text", |
|
text=[ |
|
f"Actual Spend: {numerize(spends_actual * conversion_rate)}<br>{metrics_selected_formatted}: {numerize(y_actual)}<br>ROI: {roi_actual:.2f}<br>MROI: {mroi_actual:.2f}" |
|
], |
|
showlegend=True, |
|
) |
|
) |
|
|
|
|
|
roi_threshold = st.session_state.roi_threshold |
|
|
|
|
|
x, y = np.array(x_plot), np.array(y_plot) |
|
x_scaled, y_scaled = x / max(x), y / max(y) |
|
|
|
|
|
mroi_scaled = np.zeros_like(x_scaled) |
|
for j in range(1, len(x_scaled)): |
|
x1, y1 = x_scaled[j - 1], y_scaled[j - 1] |
|
x2, y2 = x_scaled[j], y_scaled[j] |
|
mroi_scaled[j] = (y2 - y1) / (x2 - x1) if (x2 - x1) != 0 else 0 |
|
|
|
|
|
start_value, end_value, left_value, right_value = find_segment_value( |
|
x_plot, np.array(roi), mroi_scaled, roi_threshold, 0.05 |
|
) |
|
|
|
|
|
region_start_end[channel] = { |
|
"start_value": start_value, |
|
"end_value": end_value, |
|
"left_value": left_value, |
|
"right_value": right_value, |
|
} |
|
|
|
|
|
y_max = max(y_plot) * 1.3 |
|
|
|
|
|
fig.add_shape( |
|
type="rect", |
|
x0=start_value, |
|
y0=0, |
|
x1=left_value, |
|
y1=y_max, |
|
line=dict(width=0), |
|
fillcolor="rgba(255, 255, 0, 0.3)", |
|
layer="below", |
|
) |
|
|
|
|
|
fig.add_shape( |
|
type="rect", |
|
x0=left_value, |
|
y0=0, |
|
x1=right_value, |
|
y1=y_max, |
|
line=dict(width=0), |
|
fillcolor="rgba(0, 255, 0, 0.3)", |
|
layer="below", |
|
) |
|
|
|
|
|
fig.add_shape( |
|
type="rect", |
|
x0=right_value, |
|
y0=0, |
|
x1=end_value, |
|
y1=y_max, |
|
line=dict(width=0), |
|
fillcolor="rgba(255, 0, 0, 0.3)", |
|
layer="below", |
|
) |
|
|
|
|
|
fig.update_layout( |
|
title=f"{name_formating(channel)}", |
|
showlegend=False, |
|
xaxis=dict( |
|
showgrid=True, |
|
showticklabels=True, |
|
tickformat=".2s", |
|
gridcolor="lightgrey", |
|
gridwidth=0.5, |
|
griddash="dot", |
|
), |
|
yaxis=dict( |
|
showgrid=True, |
|
showticklabels=True, |
|
tickformat=".2s", |
|
gridcolor="lightgrey", |
|
gridwidth=0.5, |
|
griddash="dot", |
|
), |
|
template="plotly_white", |
|
margin=dict(l=20, r=20, t=30, b=20), |
|
height=100 * math.ceil(len(channel_list) / 4), |
|
) |
|
|
|
figures.append(fig) |
|
|
|
|
|
channel_roi_mroi[channel] = { |
|
"actual_roi": roi_actual, |
|
"actual_mroi": mroi_actual, |
|
} |
|
|
|
return figures, channel_roi_mroi, region_start_end |
|
|
|
|
|
|
|
def modified_metrics_point( |
|
fig, modified_spends, s_curve_params, channels_proportion, conversion_rate |
|
): |
|
|
|
roi_modified, mroi_modified, y_modified = get_point_parms( |
|
modified_spends, s_curve_params, channels_proportion, conversion_rate |
|
) |
|
|
|
|
|
fig.add_trace( |
|
go.Scatter( |
|
x=[modified_spends * conversion_rate], |
|
y=[y_modified], |
|
mode="markers", |
|
marker=dict(color="blueviolet", size=10, symbol="circle"), |
|
name="Optimized Spend", |
|
hoverinfo="text", |
|
text=[ |
|
f"Modified Spend: {numerize(modified_spends * conversion_rate)}<br>{metrics_selected_formatted}: {numerize(y_modified)}<br>ROI: {roi_modified:.2f}<br>MROI: {mroi_modified:.2f}" |
|
], |
|
showlegend=True, |
|
) |
|
) |
|
|
|
return roi_modified, mroi_modified, fig |
|
|
|
|
|
|
|
def bound_type_change(modified_pickle_file_path): |
|
|
|
new_bound_type = st.session_state["bound_type_key"] |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "rb") as file: |
|
data = pickle.load(file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
data[metrics_selected][panel_selected]["bound_type"] = new_bound_type |
|
|
|
|
|
channel_list = list(data[metrics_selected][panel_selected]["channels"].keys()) |
|
if not new_bound_type: |
|
for channel in channel_list: |
|
data[metrics_selected][panel_selected]["channels"][channel]["bounds"] = [ |
|
-10, |
|
10, |
|
] |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "wb") as file: |
|
pickle.dump(data, file) |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
|
|
def format_value(input_value): |
|
value = abs(input_value) |
|
return f"{input_value:.4f}" if value < 1 else f"{numerize(input_value, 1)}" |
|
|
|
|
|
|
|
def round_value(input_value): |
|
value = abs(input_value) |
|
return round(input_value, 4) if value < 1 else round(input_value, 1) |
|
|
|
|
|
|
|
@st.cache_data(show_spinner=False) |
|
def roi_mori_plot(channel_roi_mroi): |
|
|
|
channel_roi_mroi_plot = {} |
|
for channel in channel_roi_mroi: |
|
channel_roi_mroi_data = channel_roi_mroi[channel] |
|
|
|
actual_roi = channel_roi_mroi_data["actual_roi"] |
|
optimized_roi = channel_roi_mroi_data["optimized_roi"] |
|
actual_mroi = channel_roi_mroi_data["actual_mroi"] |
|
optimized_mroi = channel_roi_mroi_data["optimized_mroi"] |
|
|
|
|
|
fig_roi = go.Figure() |
|
fig_roi.add_trace( |
|
go.Bar( |
|
x=["Actual ROI"], |
|
y=[actual_roi], |
|
name="Actual ROI", |
|
marker_color="cyan", |
|
width=1, |
|
text=[format_value(actual_roi)], |
|
textposition="auto", |
|
textfont=dict(color="black", size=14), |
|
) |
|
) |
|
fig_roi.add_trace( |
|
go.Bar( |
|
x=["Optimized ROI"], |
|
y=[optimized_roi], |
|
name="Optimized ROI", |
|
marker_color="blueviolet", |
|
width=1, |
|
text=[format_value(optimized_roi)], |
|
textposition="auto", |
|
textfont=dict(color="black", size=14), |
|
) |
|
) |
|
|
|
fig_roi.update_layout( |
|
annotations=[ |
|
dict( |
|
x=0.5, |
|
y=1.3, |
|
xref="paper", |
|
yref="paper", |
|
text="ROI", |
|
showarrow=False, |
|
font=dict(size=14), |
|
) |
|
], |
|
barmode="group", |
|
bargap=0, |
|
showlegend=False, |
|
width=110, |
|
height=110, |
|
xaxis=dict( |
|
showticklabels=True, |
|
showgrid=False, |
|
tickangle=0, |
|
ticktext=["Actual", "Optimized"], |
|
tickvals=["Actual ROI", "Optimized ROI"], |
|
), |
|
yaxis=dict(showticklabels=False, showgrid=False), |
|
margin=dict(t=20, b=20, r=0, l=0), |
|
) |
|
|
|
|
|
fig_mroi = go.Figure() |
|
fig_mroi.add_trace( |
|
go.Bar( |
|
x=["Actual MROI"], |
|
y=[actual_mroi], |
|
name="Actual MROI", |
|
marker_color="cyan", |
|
width=1, |
|
text=[format_value(actual_mroi)], |
|
textposition="auto", |
|
textfont=dict(color="black", size=14), |
|
) |
|
) |
|
fig_mroi.add_trace( |
|
go.Bar( |
|
x=["Optimized MROI"], |
|
y=[optimized_mroi], |
|
name="Optimized MROI", |
|
marker_color="blueviolet", |
|
width=1, |
|
text=[format_value(optimized_mroi)], |
|
textposition="auto", |
|
textfont=dict(color="black", size=14), |
|
) |
|
) |
|
|
|
fig_mroi.update_layout( |
|
annotations=[ |
|
dict( |
|
x=0.5, |
|
y=1.3, |
|
xref="paper", |
|
yref="paper", |
|
text="MROI", |
|
showarrow=False, |
|
font=dict(size=14), |
|
) |
|
], |
|
barmode="group", |
|
bargap=0, |
|
showlegend=False, |
|
width=110, |
|
height=110, |
|
xaxis=dict( |
|
showticklabels=True, |
|
showgrid=False, |
|
tickangle=0, |
|
ticktext=["Actual", "Optimized"], |
|
tickvals=["Actual MROI", "Optimized MROI"], |
|
), |
|
yaxis=dict(showticklabels=False, showgrid=False), |
|
margin=dict(t=20, b=20, r=0, l=0), |
|
) |
|
|
|
|
|
channel_roi_mroi_plot[channel] = {"fig_roi": fig_roi, "fig_mroi": fig_mroi} |
|
|
|
return channel_roi_mroi_plot |
|
|
|
|
|
|
|
def save_scenario( |
|
scenario_dict, metrics_selected, panel_selected, optimization_goal, channel_roi_mroi |
|
): |
|
|
|
if st.session_state["scenario_name"] is not None: |
|
st.session_state["scenario_name"] = st.session_state["scenario_name"].strip() |
|
|
|
if ( |
|
st.session_state["scenario_name"] is None |
|
or st.session_state["scenario_name"] == "" |
|
): |
|
|
|
st.session_state.message_display = { |
|
"type": "warning", |
|
"message": "Please provide a name to save the scenario.", |
|
"icon": "⚠️", |
|
} |
|
return |
|
|
|
|
|
if not scenario_dict: |
|
|
|
st.session_state.message_display = { |
|
"type": "warning", |
|
"message": "Nothing to save. The scenario data is empty.", |
|
"icon": "⚠️", |
|
} |
|
return |
|
|
|
|
|
scenario_dict["panel_selected"] = panel_selected |
|
scenario_dict["metrics_selected"] = metrics_selected |
|
scenario_dict["optimization"] = optimization_goal |
|
scenario_dict["channel_roi_mroi"] = channel_roi_mroi |
|
|
|
|
|
saved_scenarios_dict_path = os.path.join( |
|
st.session_state["project_path"], "saved_scenarios.pkl" |
|
) |
|
|
|
|
|
try: |
|
if os.path.exists(saved_scenarios_dict_path): |
|
with open(saved_scenarios_dict_path, "rb") as f: |
|
saved_scenarios_dict = pickle.load(f) |
|
else: |
|
saved_scenarios_dict = OrderedDict() |
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
if st.session_state["scenario_name"] in saved_scenarios_dict.keys(): |
|
|
|
st.session_state.message_display = { |
|
"type": "warning", |
|
"message": "Name already exists. Please change the name or delete the existing scenario from the Saved Scenario page.", |
|
"icon": "⚠️", |
|
} |
|
return |
|
|
|
|
|
saved_scenarios_dict[st.session_state["scenario_name"]] = scenario_dict |
|
|
|
|
|
try: |
|
with open(saved_scenarios_dict_path, "wb") as f: |
|
pickle.dump(saved_scenarios_dict, f) |
|
|
|
|
|
st.session_state.message_display = { |
|
"type": "success", |
|
"message": f"Scenario '{st.session_state.scenario_name}' has been successfully saved!", |
|
"icon": "💾", |
|
} |
|
|
|
except: |
|
st.toast("Failed to Load/Update. Tool reset to default settings.", icon="⚠️") |
|
reset_scenario() |
|
return |
|
|
|
|
|
st.session_state["scenario_name"] = "" |
|
|
|
|
|
|
|
def calculate_rgba(spends_value, region_start_end): |
|
|
|
start_value = region_start_end["start_value"] |
|
end_value = region_start_end["end_value"] |
|
left_value = region_start_end["left_value"] |
|
right_value = region_start_end["right_value"] |
|
|
|
|
|
def calculate_alpha(position, start, end, min_alpha=0.1, max_alpha=0.4): |
|
return min_alpha + (max_alpha - min_alpha) * (position - start) / (end - start) |
|
|
|
if start_value <= spends_value <= left_value: |
|
|
|
alpha = calculate_alpha(spends_value, left_value, start_value) |
|
return (255, 255, 0, alpha) |
|
elif left_value < spends_value <= right_value: |
|
|
|
alpha = calculate_alpha(spends_value, right_value, left_value) |
|
return (0, 128, 0, alpha) |
|
elif right_value < spends_value <= end_value: |
|
|
|
alpha = calculate_alpha(spends_value, right_value, end_value) |
|
return (255, 0, 0, alpha) |
|
|
|
|
|
|
|
def display_channel_name_with_background_color( |
|
channel_name, background_color=(0, 128, 0, 0.1) |
|
): |
|
formatted_name = name_formating(channel_name) |
|
|
|
|
|
r, g, b, a = background_color |
|
|
|
|
|
html_content = f""" |
|
<div style=" |
|
background-color: rgba({r}, {g}, {b}, {a}); |
|
padding: 10px; |
|
display: inline-block; |
|
border-radius: 5px;"> |
|
<strong>{formatted_name}</strong> |
|
</div> |
|
""" |
|
|
|
return html_content |
|
|
|
|
|
|
|
def check_optimization_success( |
|
channel_list, |
|
input_channels_spends, |
|
output_channels_spends, |
|
bounds_dict, |
|
optimization_goal, |
|
modified_total_metrics, |
|
actual_total_metrics, |
|
modified_total_spends, |
|
actual_total_spends, |
|
original_total_spends, |
|
optimization_success, |
|
): |
|
for channel in channel_list: |
|
input_channel_spends = input_channels_spends[channel] |
|
output_channel_spends = output_channels_spends[channel] |
|
|
|
lower_percent = bounds_dict[channel][0] |
|
upper_percent = bounds_dict[channel][1] |
|
|
|
lower_allowed_value = ( |
|
input_channel_spends * (100 + lower_percent - 1) / 100 |
|
) |
|
upper_allowed_value = ( |
|
input_channel_spends * (100 + upper_percent + 1) / 100 |
|
) |
|
|
|
|
|
if ( |
|
output_channel_spends > upper_allowed_value |
|
or output_channel_spends < lower_allowed_value |
|
): |
|
error_message = "Optimization failed: strict bounds. Use flexible bounds." |
|
return False, error_message, "❌" |
|
|
|
|
|
if optimization_goal == "Spends": |
|
percent_change_happened = abs( |
|
(modified_total_spends - actual_total_spends) / actual_total_spends |
|
) |
|
if percent_change_happened > 0.01: |
|
error_message = "Optimization failed: input and optimized spends differ. Use flexible bounds." |
|
return False, error_message, "❌" |
|
else: |
|
percent_change_happened = abs( |
|
(modified_total_metrics - actual_total_metrics) / actual_total_metrics |
|
) |
|
if percent_change_happened > 0.01: |
|
error_message = "Optimization failed: input and optimized metrics differ. Use flexible bounds." |
|
return False, error_message, "❌" |
|
|
|
|
|
lower_limit = original_total_spends * 0.5 |
|
upper_limit = original_total_spends * 1.5 |
|
|
|
|
|
if modified_total_spends < lower_limit or modified_total_spends > upper_limit: |
|
error_message = "New spends optimized are outside the allowed range of ±50%." |
|
return False, error_message, "❌" |
|
|
|
|
|
if not optimization_success: |
|
error_message = "Optimization failed to converge." |
|
return False, error_message, "❌" |
|
|
|
return True, "Optimization successful.", "💸" |
|
|
|
|
|
|
|
@st.cache_data(show_spinner=False) |
|
def check_target_achievability( |
|
optimize_allow, |
|
fixed_target, |
|
lower_achievable_target, |
|
upper_achievable_target, |
|
total_absolute_target, |
|
): |
|
|
|
minimum_achievable_message = f"Minimum achievable {fixed_target} with the given spends and bounds is {numerize(lower_achievable_target)}" |
|
maximum_achievable_message = f"Maximum achievable {fixed_target} with the given spends and bounds is {numerize(upper_achievable_target)}" |
|
|
|
|
|
if (lower_achievable_target > total_absolute_target) or ( |
|
upper_achievable_target < total_absolute_target |
|
): |
|
if lower_achievable_target > total_absolute_target: |
|
|
|
st.session_state.message_display = { |
|
"type": "error", |
|
"message": minimum_achievable_message, |
|
"icon": "🔼", |
|
} |
|
else: |
|
|
|
st.session_state.message_display = { |
|
"type": "error", |
|
"message": maximum_achievable_message, |
|
"icon": "🔽", |
|
} |
|
optimize_allow = False |
|
else: |
|
|
|
if st.session_state.message_display["message"] in [ |
|
minimum_achievable_message, |
|
maximum_achievable_message, |
|
]: |
|
st.session_state.message_display = { |
|
"type": "success", |
|
"message": None, |
|
"icon": "", |
|
} |
|
|
|
return optimize_allow |
|
|
|
|
|
|
|
def display_message(): |
|
|
|
message_type = st.session_state.message_display["type"] |
|
message = st.session_state.message_display["message"] |
|
icon = st.session_state.message_display["icon"] |
|
|
|
|
|
if message is not None: |
|
if message_type == "success": |
|
st.success(message, icon=icon) |
|
elif message_type == "warning": |
|
st.warning(message, icon=icon) |
|
elif message_type == "error": |
|
st.error(message, icon=icon) |
|
else: |
|
st.info(message, icon=icon) |
|
|
|
|
|
|
|
load_local_css("styles.css") |
|
set_header() |
|
|
|
|
|
if "project_dct" not in st.session_state: |
|
|
|
project_selection() |
|
st.stop() |
|
|
|
database_file = r"DB\User.db" |
|
|
|
conn = sqlite3.connect( |
|
database_file, check_same_thread=False |
|
) |
|
c = conn.cursor() |
|
|
|
|
|
col_project_data = st.columns([2, 1]) |
|
with col_project_data[0]: |
|
st.markdown(f"**Welcome {st.session_state['username']}**") |
|
with col_project_data[1]: |
|
st.markdown(f"**Current Project: {st.session_state['project_name']}**") |
|
|
|
|
|
st.title("Scenario Planner") |
|
|
|
|
|
directory = os.path.join(st.session_state["project_path"], "metrics_level_data") |
|
|
|
|
|
metrics_list = get_metrics_names(directory) |
|
|
|
|
|
if len(metrics_list) == 0: |
|
|
|
st.warning( |
|
"Please tune at least one model to generate response curves data.", |
|
icon="⚠️", |
|
) |
|
|
|
st.stop() |
|
|
|
|
|
metric_col, panel_col = st.columns(2) |
|
|
|
|
|
metrics_selected = metric_col.selectbox( |
|
"Response Metrics", |
|
sorted(metrics_list), |
|
format_func=name_formating, |
|
key="response_metrics_selectbox_sp", |
|
index=0, |
|
) |
|
metrics_selected_formatted = name_formating(metrics_selected) |
|
|
|
|
|
file_selected = f"metrics_level_data/data_test_overview_panel@#{metrics_selected}.xlsx" |
|
file_selected_path = os.path.join(st.session_state["project_path"], file_selected) |
|
panel_list = get_panels_names(file_selected_path) |
|
|
|
|
|
panel_selected = panel_col.selectbox( |
|
"Panel", |
|
sorted(panel_list), |
|
key="panel_selected_selectbox_sp", |
|
index=0, |
|
) |
|
panel_selected_formatted = name_formating(panel_selected) |
|
|
|
|
|
original_json_file_path = os.path.join( |
|
st.session_state["project_path"], "rcs_data_original.json" |
|
) |
|
modified_json_file_path = os.path.join( |
|
st.session_state["project_path"], "rcs_data_modified.json" |
|
) |
|
|
|
|
|
if not os.path.exists(original_json_file_path) or not os.path.exists( |
|
modified_json_file_path |
|
): |
|
print( |
|
f"RCS JSON file does not exist at {original_json_file_path}. Generating new RCS data..." |
|
) |
|
generate_rcs_data(original_json_file_path, modified_json_file_path) |
|
else: |
|
print( |
|
f"RCS JSON file already exists at {original_json_file_path}. No need to generate new RCS data." |
|
) |
|
|
|
|
|
original_json_data, modified_json_data = load_json_files( |
|
original_json_file_path, modified_json_file_path |
|
) |
|
|
|
|
|
original_pickle_file_path = os.path.join( |
|
st.session_state["project_path"], "scenario_data_original.pkl" |
|
) |
|
modified_pickle_file_path = os.path.join( |
|
st.session_state["project_path"], "scenario_data_modified.pkl" |
|
) |
|
|
|
|
|
if not os.path.exists(original_pickle_file_path) or not os.path.exists( |
|
modified_pickle_file_path |
|
): |
|
print( |
|
f"Scenario file does not exist at {original_pickle_file_path}. Generating new senario file data..." |
|
) |
|
generate_scenario_data(original_pickle_file_path, modified_pickle_file_path) |
|
else: |
|
print( |
|
f"Scenario file already exists at {original_pickle_file_path}. No need to generate new senario file data." |
|
) |
|
|
|
|
|
original_data, modified_data = load_pickle_files( |
|
original_pickle_file_path, modified_pickle_file_path |
|
) |
|
|
|
|
|
original_scenario_data = original_data[metrics_selected][panel_selected] |
|
|
|
|
|
modified_scenario_data = modified_data[metrics_selected][panel_selected] |
|
|
|
|
|
st.divider() |
|
( |
|
actual_spends_col, |
|
actual_metrics_col, |
|
actual_CPA_col, |
|
optimized_spends_col, |
|
optimized_metrics_col, |
|
optimized_CPA_col, |
|
) = st.columns(6) |
|
|
|
|
|
actual_spends = numerize(original_scenario_data["actual_total_spends"]) |
|
actual_metric_value = numerize(original_scenario_data["actual_total_sales"]) |
|
optimized_spends = numerize(modified_scenario_data["modified_total_spends"]) |
|
optimized_metric_value = numerize(modified_scenario_data["modified_total_sales"]) |
|
|
|
|
|
spends_delta = numerize( |
|
modified_scenario_data["modified_total_spends"] |
|
- original_scenario_data["actual_total_spends"] |
|
) |
|
metrics_delta = numerize( |
|
modified_scenario_data["modified_total_sales"] |
|
- original_scenario_data["actual_total_sales"] |
|
) |
|
|
|
|
|
actual_CPA = ( |
|
original_scenario_data["actual_total_spends"] |
|
/ original_scenario_data["actual_total_sales"] |
|
) |
|
optimized_CPA = ( |
|
modified_scenario_data["modified_total_spends"] |
|
/ modified_scenario_data["modified_total_sales"] |
|
) |
|
CPA_delta = round_value(optimized_CPA - actual_CPA) |
|
|
|
actual_CPA_col.metric("Actual CPA", round_value(actual_CPA)) |
|
optimized_spends_col.metric("Optimized Spends", optimized_spends, delta=spends_delta) |
|
optimized_metrics_col.metric( |
|
f"Optimized {metrics_selected_formatted}", |
|
optimized_metric_value, |
|
delta=metrics_delta, |
|
) |
|
optimized_CPA_col.metric( |
|
"Optimized CPA", |
|
round_value(optimized_CPA), |
|
delta=CPA_delta, |
|
delta_color="inverse", |
|
) |
|
|
|
|
|
actual_spends_col.metric("Actual Spends", actual_spends) |
|
actual_metrics_col.metric(f"Actual {metrics_selected_formatted}", actual_metric_value) |
|
st.divider() |
|
|
|
|
|
st.session_state.roi_threshold = ( |
|
original_scenario_data["actual_total_sales"] |
|
/ original_scenario_data["actual_total_spends"] |
|
) |
|
|
|
|
|
channel_list = list(original_scenario_data["channels"].keys()) |
|
|
|
|
|
optimization_goal_col, message_display_col, button_col = st.columns([3, 6, 6]) |
|
|
|
|
|
absolute_text_col, absolute_slider_col, percentage_number_col, bound_type_col = ( |
|
st.columns([2, 4, 2, 2]) |
|
) |
|
|
|
|
|
optimization_goal = optimization_goal_col.selectbox( |
|
"Fix", ["Spends", metrics_selected_formatted] |
|
) |
|
|
|
|
|
with button_col: |
|
st.write("##") |
|
optimize_button_col, reset_button_col = st.columns(2) |
|
reset_button_col.button( |
|
"Reset", |
|
use_container_width=True, |
|
on_click=reset_scenario, |
|
args=(metrics_selected, panel_selected), |
|
) |
|
|
|
|
|
|
|
if optimization_goal == "Spends": |
|
absolute_value = modified_scenario_data["actual_total_spends"] |
|
st.session_state.total_absolute_main_key = numerize( |
|
modified_scenario_data["modified_total_spends"] |
|
) |
|
else: |
|
absolute_value = modified_scenario_data["actual_total_sales"] |
|
st.session_state.total_absolute_main_key = numerize( |
|
modified_scenario_data["modified_total_sales"] |
|
) |
|
|
|
total_absolute = absolute_text_col.text_input( |
|
"Absolute", |
|
key="total_absolute_main_key", |
|
on_change=total_absolute_main_key_change, |
|
args=( |
|
metrics_selected, |
|
panel_selected, |
|
modified_pickle_file_path, |
|
optimization_goal, |
|
), |
|
) |
|
|
|
|
|
slider_options = list( |
|
np.linspace(int(0.5 * absolute_value), int(1.5 * absolute_value), 50) |
|
) |
|
slider_options.append( |
|
modified_scenario_data["modified_total_spends"] |
|
if optimization_goal == "Spends" |
|
else modified_scenario_data["modified_total_sales"] |
|
) |
|
slider_options = sorted(slider_options) |
|
numerized_slider_options = [ |
|
numerize(value) for value in slider_options |
|
] |
|
|
|
|
|
|
|
st.session_state.total_absolute_key = numerize( |
|
modified_scenario_data["modified_total_spends"] |
|
if optimization_goal == "Spends" |
|
else modified_scenario_data["modified_total_sales"] |
|
) |
|
slider_value = absolute_slider_col.select_slider( |
|
"Absolute", |
|
numerized_slider_options, |
|
key="total_absolute_key", |
|
on_change=total_absolute_key_change, |
|
args=( |
|
metrics_selected, |
|
panel_selected, |
|
modified_pickle_file_path, |
|
optimization_goal, |
|
), |
|
) |
|
|
|
|
|
if optimization_goal == "Spends": |
|
st.session_state.total_percentage_key = int( |
|
round( |
|
( |
|
( |
|
modified_scenario_data["modified_total_spends"] |
|
- modified_scenario_data["actual_total_spends"] |
|
) |
|
/ modified_scenario_data["actual_total_spends"] |
|
) |
|
* 100, |
|
0, |
|
) |
|
) |
|
else: |
|
st.session_state.total_percentage_key = int( |
|
round( |
|
( |
|
( |
|
modified_scenario_data["modified_total_sales"] |
|
- modified_scenario_data["actual_total_sales"] |
|
) |
|
/ modified_scenario_data["actual_total_sales"] |
|
) |
|
* 100, |
|
0, |
|
) |
|
) |
|
|
|
percentage_target = percentage_number_col.number_input( |
|
"Percentage", |
|
min_value=-50, |
|
max_value=50, |
|
key="total_percentage_key", |
|
on_change=total_percentage_key_change, |
|
args=( |
|
metrics_selected, |
|
panel_selected, |
|
modified_pickle_file_path, |
|
absolute_value, |
|
optimization_goal, |
|
), |
|
) |
|
|
|
|
|
st.session_state["bound_type_key"] = modified_scenario_data["bound_type"] |
|
with bound_type_col: |
|
st.write("##") |
|
bound_type = st.toggle( |
|
"Apply Custom Bounds", |
|
on_change=bound_type_change, |
|
args=(modified_pickle_file_path,), |
|
key="bound_type_key", |
|
) |
|
|
|
|
|
total_channel_spends, optimize_allow = 0, True |
|
bounds_dict = {} |
|
s_curve_params = {} |
|
channels_spends = {} |
|
channels_proportion = {} |
|
channels_conversion_ratio = {} |
|
channels_name_plot_placeholder = {} |
|
|
|
|
|
with st.expander("Optimization Inputs", expanded=True): |
|
for channel in channel_list: |
|
st.divider() |
|
|
|
|
|
channel_key = f"{metrics_selected}_{panel_selected}_{channel}" |
|
|
|
|
|
if st.session_state["bound_type_key"]: |
|
( |
|
name_plot_col, |
|
input_col, |
|
spends_col, |
|
metrics_col, |
|
bounds_input_col, |
|
bounds_display_col, |
|
allow_col, |
|
) = st.columns([2, 1, 1, 1, 1, 1, 1]) |
|
else: |
|
( |
|
name_plot_col, |
|
input_col, |
|
spends_col, |
|
metrics_col, |
|
bounds_display_col, |
|
allow_col, |
|
) = st.columns([2, 1, 1.5, 1.5, 1, 1]) |
|
bounds_input_col = st.empty() |
|
|
|
|
|
with name_plot_col: |
|
|
|
channel_name_placeholder = st.empty() |
|
channel_name_placeholder.markdown( |
|
display_channel_name_with_background_color(channel), |
|
unsafe_allow_html=True, |
|
) |
|
|
|
|
|
channel_plot_placeholder = st.container() |
|
|
|
|
|
channels_name_plot_placeholder[channel] = { |
|
"channel_name_placeholder": channel_name_placeholder, |
|
"channel_plot_placeholder": channel_plot_placeholder, |
|
} |
|
|
|
|
|
channel_spends_actual = ( |
|
original_scenario_data["channels"][channel]["actual_total_spends"] |
|
* original_scenario_data["channels"][channel]["conversion_rate"] |
|
) |
|
channel_metrics_actual = original_scenario_data["channels"][channel][ |
|
"modified_total_sales" |
|
] |
|
|
|
channel_spends_modified = ( |
|
modified_scenario_data["channels"][channel]["modified_total_spends"] |
|
* original_scenario_data["channels"][channel]["conversion_rate"] |
|
) |
|
channel_metrics_modified = modified_scenario_data["channels"][channel][ |
|
"modified_total_sales" |
|
] |
|
|
|
|
|
with input_col: |
|
|
|
st.session_state[f"{channel_key}_abs_spends_key"] = numerize( |
|
modified_scenario_data["channels"][channel]["modified_total_spends"] |
|
* original_scenario_data["channels"][channel]["conversion_rate"] |
|
) |
|
absolute_channel_spends = st.text_input( |
|
"Absolute Spends", |
|
key=f"{channel_key}_abs_spends_key", |
|
on_change=absolute_channel_spends_change, |
|
args=( |
|
channel_key, |
|
channel_spends_actual, |
|
channel, |
|
metrics_selected, |
|
panel_selected, |
|
modified_pickle_file_path, |
|
), |
|
) |
|
|
|
|
|
st.session_state[f"{channel_key}_per_spends_key"] = int( |
|
round( |
|
( |
|
( |
|
convert_to_float( |
|
st.session_state[f"{channel_key}_abs_spends_key"] |
|
) |
|
- float(channel_spends_actual) |
|
) |
|
/ channel_spends_actual |
|
) |
|
* 100, |
|
0, |
|
) |
|
) |
|
|
|
|
|
percentage_channel_spends = st.number_input( |
|
"Percentage Spends", |
|
min_value=-1000, |
|
max_value=1000, |
|
key=f"{channel_key}_per_spends_key", |
|
on_change=percentage_channel_spends_change, |
|
args=( |
|
channel_key, |
|
channel_spends_actual, |
|
channel, |
|
metrics_selected, |
|
panel_selected, |
|
modified_pickle_file_path, |
|
), |
|
) |
|
|
|
|
|
channels_spends[channel] = original_scenario_data["channels"][channel][ |
|
"actual_total_spends" |
|
] * (1 + percentage_channel_spends / 100) |
|
|
|
channels_conversion_ratio[channel] = original_scenario_data["channels"][ |
|
channel |
|
]["conversion_rate"] |
|
|
|
channels_proportion[channel] = original_scenario_data["channels"][channel][ |
|
"spends" |
|
] / sum(original_scenario_data["channels"][channel]["spends"]) |
|
|
|
|
|
with metrics_col: |
|
|
|
st.metric( |
|
f"Actual {name_formating(metrics_selected)}", |
|
value=numerize(channel_metrics_actual), |
|
) |
|
|
|
|
|
st.metric( |
|
f"Optimized {name_formating(metrics_selected)}", |
|
value=numerize(channel_metrics_modified), |
|
delta=numerize(channel_metrics_modified - channel_metrics_actual), |
|
) |
|
|
|
|
|
with spends_col: |
|
|
|
st.metric( |
|
"Actual Spends", |
|
value=numerize(channel_spends_actual), |
|
) |
|
|
|
|
|
st.metric( |
|
"Optimized Spends", |
|
value=numerize(channel_spends_modified), |
|
delta=numerize(channel_spends_modified - channel_spends_actual), |
|
) |
|
|
|
|
|
with allow_col: |
|
|
|
st.write("#") |
|
st.session_state[f"{channel_key}_allow_optimize_key"] = ( |
|
modified_scenario_data["channels"][channel]["freeze"] |
|
) |
|
freeze = st.checkbox( |
|
"Freeze", |
|
key=f"{channel_key}_allow_optimize_key", |
|
on_change=freeze_change, |
|
args=( |
|
metrics_selected, |
|
panel_selected, |
|
modified_pickle_file_path, |
|
channel_key, |
|
channel, |
|
), |
|
) |
|
|
|
|
|
if freeze: |
|
lower_bound, upper_bound = 0, 0 |
|
|
|
|
|
if st.session_state["bound_type_key"]: |
|
with bounds_input_col: |
|
|
|
st.session_state[f"{channel_key}_upper_key"] = ( |
|
modified_scenario_data["channels"][channel]["bounds"] |
|
)[1] |
|
upper_bound = st.number_input( |
|
"Upper bound (%)", |
|
min_value=-100, |
|
max_value=100, |
|
key=f"{channel_key}_upper_key", |
|
disabled=st.session_state[f"{channel_key}_allow_optimize_key"], |
|
on_change=bound_change, |
|
args=( |
|
metrics_selected, |
|
panel_selected, |
|
modified_pickle_file_path, |
|
channel_key, |
|
channel, |
|
), |
|
) |
|
|
|
|
|
st.session_state[f"{channel_key}_lower_key"] = ( |
|
modified_scenario_data["channels"][channel]["bounds"] |
|
)[0] |
|
lower_bound = st.number_input( |
|
"Lower bound (%)", |
|
min_value=-100, |
|
max_value=100, |
|
key=f"{channel_key}_lower_key", |
|
disabled=st.session_state[f"{channel_key}_allow_optimize_key"], |
|
on_change=bound_change, |
|
args=( |
|
metrics_selected, |
|
panel_selected, |
|
modified_pickle_file_path, |
|
channel_key, |
|
channel, |
|
), |
|
) |
|
|
|
|
|
if lower_bound > upper_bound: |
|
lower_bound = -10 |
|
upper_bound = 10 |
|
|
|
|
|
bounds_dict[channel] = [lower_bound, upper_bound] |
|
|
|
else: |
|
|
|
if freeze: |
|
lower_bound, upper_bound = 0, 0 |
|
else: |
|
lower_bound = -10 |
|
upper_bound = 10 |
|
|
|
|
|
bounds_dict[channel] = modified_scenario_data["channels"][channel]["bounds"] |
|
|
|
|
|
with bounds_display_col: |
|
|
|
actual_spends = ( |
|
modified_scenario_data["channels"][channel]["modified_total_spends"] |
|
* modified_scenario_data["channels"][channel]["conversion_rate"] |
|
) |
|
|
|
|
|
upper_limit_spends = actual_spends * (1 + upper_bound / 100) |
|
lower_limit_spends = actual_spends * (1 + lower_bound / 100) |
|
|
|
|
|
st.metric("Upper Bound", numerize(upper_limit_spends)) |
|
st.metric("Lower Bound", numerize(lower_limit_spends)) |
|
|
|
|
|
s_curve_params[channel] = get_s_curve_params( |
|
metrics_selected, |
|
panel_selected, |
|
channel, |
|
original_json_data, |
|
modified_json_data, |
|
modified_pickle_file_path, |
|
) |
|
|
|
|
|
total_channel_spends += convert_to_float( |
|
st.session_state[f"{channel_key}_abs_spends_key"] |
|
) |
|
|
|
|
|
if ( |
|
total_channel_spends > 1.5 * original_scenario_data["actual_total_spends"] |
|
or total_channel_spends < 0.5 * original_scenario_data["actual_total_spends"] |
|
): |
|
|
|
st.session_state.message_display = { |
|
"type": "warning", |
|
"message": "Keep total spending within ±50% of the original value.", |
|
"icon": "⚠️", |
|
} |
|
|
|
if optimization_goal == "Spends": |
|
|
|
lower_achievable_target, upper_achievable_target = 0, 0 |
|
for channel in channel_list: |
|
channel_spends_actual = ( |
|
channels_spends[channel] * channels_conversion_ratio[channel] |
|
) |
|
lower_achievable_target += channel_spends_actual * ( |
|
1 + bounds_dict[channel][0] / 100 |
|
) |
|
upper_achievable_target += channel_spends_actual * ( |
|
1 + bounds_dict[channel][1] / 100 |
|
) |
|
else: |
|
|
|
lower_achievable_target, upper_achievable_target = max_target_achievable( |
|
channels_spends, |
|
s_curve_params, |
|
channels_proportion, |
|
modified_scenario_data, |
|
bounds_dict, |
|
) |
|
|
|
|
|
total_absolute_target = convert_to_float(total_absolute) |
|
|
|
|
|
if optimize_allow: |
|
optimize_allow = check_target_achievability( |
|
optimize_allow, |
|
name_formating(optimization_goal), |
|
lower_achievable_target, |
|
upper_achievable_target, |
|
total_absolute_target, |
|
) |
|
|
|
|
|
if optimize_button_col.button( |
|
"Optimize", use_container_width=True, disabled=not optimize_allow |
|
): |
|
with message_display_col: |
|
st.write("##") |
|
with st.spinner("Optimizing..."): |
|
|
|
optimized_spends, optimization_success = optimizer( |
|
optimization_goal, |
|
s_curve_params, |
|
channels_spends, |
|
channels_proportion, |
|
channels_conversion_ratio, |
|
convert_to_float(total_absolute), |
|
bounds_dict, |
|
modified_scenario_data, |
|
) |
|
|
|
|
|
input_channels_spends, output_channels_spends = {}, {} |
|
for channel in channel_list: |
|
|
|
input_channels_spends[channel] = ( |
|
channels_spends[channel] * channels_conversion_ratio[channel] |
|
) |
|
|
|
output_channels_spends[channel] = ( |
|
optimized_spends[channel] * channels_conversion_ratio[channel] |
|
) |
|
|
|
|
|
actual_total_spends = sum(list(input_channels_spends.values())) |
|
modified_total_spends = sum(list(output_channels_spends.values())) |
|
|
|
|
|
actual_total_metrics = modified_scenario_data["modified_total_sales"] |
|
modified_total_metrics = 0 |
|
modified_channels_metrics = {} |
|
|
|
|
|
for channel in optimized_spends.keys(): |
|
channel_s_curve_params = s_curve_params[channel] |
|
spend_proportion = ( |
|
optimized_spends[channel] * channels_proportion[channel] |
|
) |
|
|
|
modified_channels_metrics[channel] = sum( |
|
s_curve( |
|
spend_proportion, |
|
channel_s_curve_params["power"], |
|
channel_s_curve_params["K"], |
|
channel_s_curve_params["b"], |
|
channel_s_curve_params["a"], |
|
channel_s_curve_params["x0"], |
|
) |
|
) |
|
modified_total_metrics += modified_channels_metrics[ |
|
channel |
|
] |
|
|
|
|
|
modified_total_metrics += sum(modified_scenario_data["constant"]) |
|
|
|
original_total_spends = modified_scenario_data["actual_total_spends"] |
|
|
|
|
|
success, message, icon = check_optimization_success( |
|
channel_list, |
|
input_channels_spends, |
|
output_channels_spends, |
|
bounds_dict, |
|
optimization_goal, |
|
modified_total_metrics, |
|
actual_total_metrics, |
|
modified_total_spends, |
|
actual_total_spends, |
|
original_total_spends, |
|
optimization_success, |
|
) |
|
|
|
|
|
st.session_state.message_display = { |
|
"type": "success" if success else "error", |
|
"message": message, |
|
"icon": icon, |
|
} |
|
|
|
|
|
if success: |
|
|
|
for channel in channel_list: |
|
modified_scenario_data["channels"][channel][ |
|
"modified_total_spends" |
|
] = optimized_spends[channel] |
|
|
|
|
|
modified_scenario_data["channels"][channel][ |
|
"modified_total_sales" |
|
] = modified_channels_metrics[channel] |
|
|
|
|
|
modified_scenario_data["modified_total_spends"] = modified_total_spends |
|
|
|
|
|
modified_scenario_data["modified_total_sales"] = modified_total_metrics |
|
|
|
|
|
try: |
|
with open(modified_pickle_file_path, "rb+") as pickle_file: |
|
|
|
data = pickle.load(pickle_file) |
|
|
|
data[metrics_selected][panel_selected] = modified_scenario_data |
|
|
|
pickle_file.seek(0) |
|
pickle.dump(data, pickle_file) |
|
|
|
except: |
|
st.toast( |
|
"Failed to Load/Update. Tool reset to default settings.", |
|
icon="⚠️", |
|
) |
|
|
|
|
|
st.rerun() |
|
|
|
|
|
|
|
|
|
|
|
|
|
figures, channel_roi_mroi, region_start_end = generate_response_curve_plots( |
|
channel_list, s_curve_params, channels_proportion, original_scenario_data |
|
) |
|
|
|
|
|
st.subheader(f"Response Curve (X: Spends Vs Y: {metrics_selected_formatted})") |
|
with st.expander("Response Curve", expanded=True): |
|
cols = st.columns(4) |
|
for i, fig in enumerate(figures): |
|
col = cols[i % 4] |
|
with col: |
|
|
|
channel = channel_list[i] |
|
modified_total_spends = modified_scenario_data["channels"][channel][ |
|
"modified_total_spends" |
|
] |
|
conversion_rate = modified_scenario_data["channels"][channel][ |
|
"conversion_rate" |
|
] |
|
|
|
|
|
roi_optimized, mroi_optimized, fig_updated = modified_metrics_point( |
|
fig, |
|
modified_total_spends, |
|
s_curve_params[channel], |
|
channels_proportion[channel], |
|
conversion_rate, |
|
) |
|
|
|
|
|
channel_roi_mroi[channel]["optimized_roi"] = roi_optimized |
|
channel_roi_mroi[channel]["optimized_mroi"] = mroi_optimized |
|
|
|
st.plotly_chart(fig_updated, use_container_width=True) |
|
|
|
|
|
if (i + 1) % 4 == 0 and i + 1 < len(figures): |
|
cols = st.columns(4) |
|
|
|
|
|
|
|
channel_roi_mroi_plot = roi_mori_plot(channel_roi_mroi) |
|
|
|
|
|
for channel in channel_list: |
|
with channels_name_plot_placeholder[channel]["channel_plot_placeholder"]: |
|
|
|
roi_plot_col, mroi_plot_col = st.columns(2) |
|
|
|
|
|
roi_plot_col.plotly_chart(channel_roi_mroi_plot[channel]["fig_roi"]) |
|
mroi_plot_col.plotly_chart(channel_roi_mroi_plot[channel]["fig_mroi"]) |
|
|
|
|
|
channel_name_placeholder = channels_name_plot_placeholder[channel][ |
|
"channel_name_placeholder" |
|
] |
|
|
|
|
|
modified_total_spends = modified_scenario_data["channels"][channel][ |
|
"modified_total_spends" |
|
] |
|
conversion_rate = modified_scenario_data["channels"][channel]["conversion_rate"] |
|
|
|
|
|
channel_spends_value = modified_total_spends * conversion_rate |
|
|
|
|
|
channel_rgba_value = calculate_rgba(channel_spends_value, region_start_end[channel]) |
|
|
|
|
|
channel_name_placeholder.markdown( |
|
display_channel_name_with_background_color(channel, channel_rgba_value), |
|
unsafe_allow_html=True, |
|
) |
|
|
|
|
|
st.text_input("Scenario Name", key="scenario_name") |
|
|
|
|
|
if st.session_state["scenario_name"] is None or st.session_state["scenario_name"] == "": |
|
save_scenario_button_disabled = True |
|
else: |
|
save_scenario_button_disabled = False |
|
|
|
|
|
st.button( |
|
"Save Scenario", |
|
on_click=save_scenario, |
|
args=( |
|
modified_scenario_data, |
|
metrics_selected, |
|
panel_selected, |
|
optimization_goal, |
|
channel_roi_mroi, |
|
), |
|
disabled=save_scenario_button_disabled, |
|
) |
|
|
|
|
|
|
|
|
|
|
|
with message_display_col: |
|
st.write("###") |
|
display_message() |
|
|
|
|
|
st.session_state.message_display = { |
|
"type": "success", |
|
"message": None, |
|
"icon": "", |
|
} |
|
|