import streamlit as st
import pandas as pd
import scipy.stats as stats
import plotly.express as px
from datetime import datetime
import pydeck as pdk
import numpy as np
#########################
## STREAMLIT STRUCTURE ##
#########################
st.set_page_config(layout="wide")
# Title of the page
st.title("Safety stock, EOQ, ROP Recommender")
st.write("Recommend safety stock, economic order quantity (EOQ), reorder point (ROP) based on established supply chain algorithm. Assumptions made that demand and lead time are independent variables")
# Create placement containers in sequence
stats_col1, stats_col2, stats_col3 = st.columns(3)
###############
## FUNCTIONS ##
###############
average_days_per_month = 30.437
# to add emoji to the transport mode
transport_emoji = {
"Air":"Air đŠī¸",
"Land": "Land đ",
"Ocean": "Ocean đĸ"
}
# we need the following so that streamlit will only run the following once
# in this case, it will cache our df and not rerun it everytime we change something on the app
# https://discuss.streamlit.io/t/not-to-run-entire-script-when-a-widget-value-is-changed/502/5
@st.experimental_memo
def read_df_combined():
df_combined = pd.read_csv('https://generalassemblydsi32.s3.ap-southeast-1.amazonaws.com/ABC_Analysis-221125/df_combined.csv')
# df_combined = pd.read_csv("df_combined.csv")
df_combined["stock_month_2dp"] = df_combined["stock_month"].round(2)
df_combined["stock_days_2dp"] = (df_combined["stock_month"]*average_days_per_month).round(2)
df_combined["current_stock_formatted"] = df_combined["current_stock"].apply(lambda d: f"{int(d):,}")
df_combined["avg_shipped_qty_2dp"] = df_combined["avg_shipped_qty"].round(2)
df_combined["rop_date"] = pd.to_datetime(df_combined["rop_date"], format=r"%Y-%m-%d").dt.date
st.session_state.df = df_combined
return df_combined
if "df" not in st.session_state:
st.session_state.df = read_df_combined()
# Function to get and display preset values in data_form based on historical data
def preset(hub, sku, mode):
try:
target_row_no = st.session_state.df[(st.session_state.df['hub']==st.session_state.hub) & (st.session_state.df['sku'] == st.session_state.sku) & (st.session_state.df['mode'] == st.session_state.mode)].index[0]
preset_service_level = st.session_state.df.loc[target_row_no, 'service_level']*100
preset_demand_mean = st.session_state.df.loc[target_row_no, 'avg_shipped_qty']
preset_demand_sd = st.session_state.df.loc[target_row_no, 'sd_shipped_qty']
preset_leadtime_mean = st.session_state.df.loc[target_row_no, 'leadtime_days']
preset_leadtime_sd = st.session_state.df.loc[target_row_no, 'leadtime_days_sd']
preset_holding_cost_per_unit_per_month = st.session_state.df.loc[target_row_no, 'holding_cost_per_unit_year']/12
preset_handling_cost = st.session_state.df.loc[target_row_no, 'handling_cost']
preset_today_inventory = st.session_state.df.loc[target_row_no, 'current_stock']
st.session_state.service_level = preset_service_level
st.session_state.demand_mean = round(preset_demand_mean,2)
st.session_state.demand_sd = round(preset_demand_sd,2)
st.session_state.leadtime_mean = round(preset_leadtime_mean,2)
st.session_state.leadtime_sd = round(preset_leadtime_sd,2)
st.session_state.holding_cost_per_unit_per_month = round(preset_holding_cost_per_unit_per_month,2)
st.session_state.handling_cost = round(preset_handling_cost,2)
st.session_state.today_inventory = round(preset_today_inventory,2)
st.session_state.preset = True
safety_stock = st.session_state.df.loc[target_row_no, 'safety_stock']
eoq = st.session_state.df.loc[target_row_no, 'eoq']
rop = st.session_state.df.loc[target_row_no, 'rop']
# display result in 3 col metrics
result_col1.metric("Safety Stock", f"{safety_stock:,}")
result_col2.metric("Economic Order Quantiy", f"{eoq:,}")
result_col3.metric("Reorder Point", f"{rop:,}")
# Display re-order date
with result_container:
# get target row for the chosen SKU and transport mode in CONSOLE HUB
target_row_no_console = st.session_state.df[(st.session_state.df['hub']=="R.HUB") & (st.session_state.df['sku'] == sku) & (st.session_state.df['mode'] == mode)].index[0]
console_support = st.session_state.df.loc[target_row_no_console, "console_support"]
if np.isnan(console_support):
background_color = "rgba(250, 202, 43, 0.2)"
text_color = "rgb(148, 124, 45)"
RHUB_support_text = None
elif console_support == False:
background_color = "rgba(255, 43, 43, 0.09)"
text_color = "rgb(125, 53, 59)"
RHUB_support_text = 'R.HUB support within next 90 days for this SKU in all hubs: NO đ¨
'
elif console_support == True:
background_color = "rgba(33, 195, 84, 0.1)"
text_color = "rgb(23, 114, 51)"
RHUB_support_text = 'R.HUB support within next 90 days for this SKU in all hubs: YES â
'
tab1, tab2, = st.tabs(["Status", "Pull Quantity Details"])
with tab1:
st.markdown(f"""
Next re-order date: {st.session_state.df.loc[target_row_no, "rop_date"]}
Recommended Transport Mode from R.HUB to Hubs: {transport_emoji[st.session_state.df.loc[target_row_no, "recommended_mode"]]}
{RHUB_support_text}
Current stock in R.HUB: {int(st.session_state.df.loc[target_row_no_console, "current_stock"])}
Expected total pull quantity from R.HUB within next 90 days: {int(st.session_state.df.loc[target_row_no_console, "expected_pull_next_90d"])}
* Calculation is based on monthly actual demand (constantly refresh using shipment order detail report) & transit time (transport mode) assuming that all hubs will use the selected transport mode
""", unsafe_allow_html=True)
with tab2:
pull_qty_breakdown = st.session_state.df[["hub","actual_eoq", "rop_date"]][(st.session_state.df["hub"] != "R.HUB") & (st.session_state.df["sku"] == st.session_state.sku) & (st.session_state.df["mode"] == st.session_state.mode) & (st.session_state.df["days_to_rop"] < 90)].set_index("hub")
pull_qty_breakdown['actual_eoq'] = pull_qty_breakdown['actual_eoq'].apply(lambda d: f"{int(d):,}")
pull_qty_breakdown["rop_date"] = pd.to_datetime(pull_qty_breakdown["rop_date"], format=r"%Y-%m-%d").dt.date
st.dataframe(pull_qty_breakdown.sort_values(by=["rop_date"]), use_container_width= True)
# display warning if rop is higher than eoq
if rop > eoq:
result_container.info("Since EOQ is less than ROP due to low handling cost, demand or holding cost, we will plot the inventory level assuming that we order in multiple of EOQ until it exceed ROP. In actual situation, combine them with other skus to yield much better overall costs")
# calculate inventory level over 365 days
data = calc_365_inventory(safety_stock, eoq, rop, st.session_state.demand_mean, st.session_state.demand_sd, st.session_state.leadtime_mean, st.session_state.leadtime_sd, random_demand_leadtime_boolean = False)
# display matplotlib chart based on data
fig = px.line(data, x="day", y="inventory", title='Inventory levels over a year', labels={"day":"Number of days in a year", "inventory": "Inventory Level"})
fig.add_hline(y=rop, line_dash="dash", line_color="orange", annotation_text="reorder point")
fig.add_hline(y=safety_stock, line_dash="dash", line_color="green", annotation_text="safety stock")
result_container.plotly_chart(fig, theme = 'streamlit')
except:
preset_container.write(f"This SKU is not a cat b item at {st.session_state.hub}. \n **NO PRESET LOADED**")
# Function to return nan, false or true
# for use when determine whether console can support based on expected pull in next 90 days
def pos_neg_nan(val):
if np.isnan(val):
return np.nan
elif val > 0:
return True
elif val <= 0:
return False
# Calculate the expected total pull from console hub from all hubs, specific sku and mode
# and evaluate if console hub can support the expected pull
def console_support():
date_90days_later = pd.to_datetime('today').normalize() + pd.Timedelta(days=90)
# df_expected_pull = st.session_state.df[["sku", "mode", "actual_eoq"]][(st.session_state.df["hub"]!="R.HUB") & (pd.to_datetime(st.session_state.df["rop_date"], format=r"%Y-%m-%d") < date_90days_later)].groupby(["sku", "mode"]).sum("actual_eoq").reset_index()
df_expected_pull = st.session_state.df[["sku", "mode", "actual_eoq"]][(st.session_state.df["hub"]!="R.HUB") & (st.session_state.df["days_to_rop"] < 90)].groupby(["sku", "mode"]).sum("actual_eoq").reset_index()
df_expected_pull.columns = ["sku","mode","expected_pull_next_90d"]
st.session_state.df.drop(["expected_pull_next_90d","console_support"], axis = 1, inplace = True)
st.session_state.df = st.session_state.df.merge(df_expected_pull, how="left", on=["sku","mode"])
st.session_state.df = st.session_state.df.fillna(0)
st.session_state.df["console_support"] = st.session_state.df["current_stock"] - st.session_state.df["expected_pull_next_90d"]
st.session_state.df["console_support"] = st.session_state.df["console_support"].apply(pos_neg_nan)
def recommend_transport_mode(hub, sku, df):
target_rows = df[["hub","sku","mode","handling_cost", "leadtime_days", 'days_to_rop']][(df["hub"]==hub)&(df["sku"]==sku)].index
# if any of the days_to_rop is more than 0, we will take the one that is the lowest handling cost
if any(df.loc[target_rows, "days_to_rop"]>0):
temp_df = df[["mode","handling_cost"]][(df["hub"]==hub)&(df["sku"]==sku)&(df["days_to_rop"]>0)]
recommended_mode = temp_df["mode"][temp_df["handling_cost"] == temp_df["handling_cost"].min()].iloc[0]
# if all the days_to_rop is 0, it means we are already behind time, hence, we take the fastest leadtime, ignoring cost
elif all(df.loc[target_rows, "days_to_rop"]) == 0:
temp_df = df[["mode","leadtime_days"]][(df["hub"]==hub)&(df["sku"]==sku)&(df["leadtime_days"]>0)]
recommended_mode = temp_df["mode"][temp_df["leadtime_days"] == temp_df["leadtime_days"].min()].iloc[0]
return recommended_mode
# Function to calculate SS, EOQ, ROP
def recommender(service_level, demand_mean, demand_sd, leadtime_mean, leadtime_sd, holding_cost_per_unit_per_month, handling_cost, inventory, hub , sku, mode):
st.session_state.df = st.session_state.df
# calculate safety stock
# assume that demand and leadtime are independent
z_score = stats.norm.ppf(service_level/100)
safety_stock = z_score * ((demand_mean * (leadtime_sd/average_days_per_month))**2 + ((leadtime_mean/average_days_per_month) * demand_sd**2))**0.5
safety_stock = round(safety_stock, 0)
# calculate economic order quantity
# (we look at it in a yearly time period because we may only order every few months. Hence, holding cost is on a yearly basis)
eoq = ((2*handling_cost*demand_mean*12)/(holding_cost_per_unit_per_month*12))**0.5
eoq = round(eoq, 0)
# calculate reorder point
rop = safety_stock + demand_mean * (leadtime_mean / average_days_per_month)
rop = round(rop, 0)
# calculate ROP date
days_to_rop = max((inventory - rop) / (demand_mean / average_days_per_month),0)
rop_date = (pd.to_datetime('today').normalize() + pd.Timedelta(days=int(days_to_rop))).date()
# update these calculated values to the df so that it can be downloaded later
target_row_no = st.session_state.df[(st.session_state.df['hub']==hub) & (st.session_state.df['sku'] == sku) & (st.session_state.df['mode'] == mode)].index[0]
if st.session_state.preset:
print("save")
st.session_state.df.loc[target_row_no, "service_level"] = service_level/100
st.session_state.df.loc[target_row_no, "avg_shipped_qty"] = demand_mean
st.session_state.df.loc[target_row_no, "sd_shipped_qty"] = demand_sd
st.session_state.df.loc[target_row_no, "leadtime_days"] = leadtime_mean
st.session_state.df.loc[target_row_no, "leadtime_days_sd"] = leadtime_sd
st.session_state.df.loc[target_row_no, "holding_cost_per_unit_year"] = holding_cost_per_unit_per_month * 12
st.session_state.df.loc[target_row_no, "handling_cost"] = handling_cost
st.session_state.df.loc[target_row_no, "safety_stock"] = safety_stock
st.session_state.df.loc[target_row_no, "eoq"] = eoq
st.session_state.df.loc[target_row_no, "rop"] = rop
st.session_state.df.loc[target_row_no, "days_to_rop"] = days_to_rop
st.session_state.df.loc[target_row_no, "rop_date"] = rop_date
# calculate recommended transport mode
# we have to calculate this after rop dates are updated
recommended_mode = recommend_transport_mode(st.session_state.hub, st.session_state.sku, st.session_state.df)
if st.session_state.preset:
# we don't use target_row_no as we do not need the mode
st.session_state.df.loc[st.session_state.df[(st.session_state.df["hub"]==hub)&(st.session_state.df["sku"]==sku)].index, "recommended_mode"] = recommended_mode
# recalculate expected pull and whether console hub can support
console_support()
return safety_stock, eoq, rop, rop_date, recommended_mode
# function to calculate the stock level per day taking into consideration all the variables
def calc_365_inventory(safety_stock, eoq, rop, demand_mean, demand_sd, leadtime_mean, leadtime_sd, random_demand_leadtime_boolean):
safety_stock = round(safety_stock,0)
eoq = round(eoq,0)
rop = round(rop,0)
# we do the following in case rop is higher than eoq. if that happens, then we order multiple times of eoq
actual_eoq = eoq * np.ceil(rop/eoq)
demand_per_day = demand_mean / average_days_per_month
demand_sd_per_day = demand_mean / average_days_per_month
data = {'day':[], 'inventory':[], 'order':None}
for day in range(365):
# track day count
data['day'].append(day)
# if day 0, assume we have eoq inventory on hand
if day == 0:
data['inventory'].append(actual_eoq)
else:
# randomised demand based on normal distribution and defined mean and standarad deviation
if random_demand_leadtime_boolean:
randomised_demand = round(stats.norm(loc=demand_per_day,scale=demand_sd_per_day).rvs(size=1)[0],0)
else:
randomised_demand = demand_per_day
# deduct inventory per day based on demand
data['inventory'].append(data['inventory'][-1]-randomised_demand)
# check if stock after deduct demand is lower than rop. If yes, then raise order
if data['inventory'][-1] < rop and data['order'] == None:
# randomised demand based on normal distribution and defined mean and standarad deviation
if random_demand_leadtime_boolean:
randomised_leadtime = round(stats.norm(loc=leadtime_mean,scale=leadtime_sd).rvs(size=1)[0],0)
else:
randomised_leadtime = leadtime_mean
# deduct inventory per day based on demand
data['order'] = randomised_leadtime
elif data['inventory'][-1] < rop and data['order'] > 0:
data['order'] -= 1
elif data['inventory'][-1] < rop and data['order'] == 0:
data['order'] = None
data['inventory'][day] += actual_eoq
return data
# Function to change the result output upon pressing submit button
def display_recommendations():
# we have to do this rather than passing it to the function using arg because streamlit only update the args after 1 pass
# to get the most updated value immediately, we need to use session state instead
service_level = st.session_state.service_level
demand_mean = st.session_state.demand_mean
demand_sd = st.session_state.demand_sd
leadtime_mean = st.session_state.leadtime_mean
leadtime_sd = st.session_state.leadtime_sd
holding_cost_per_unit_per_month = st.session_state.holding_cost_per_unit_per_month
handling_cost = st.session_state.handling_cost
random_demand_leadtime_boolean = st.session_state.random_demand_leadtime
inventory = st.session_state.today_inventory
hub = st.session_state.hub
sku = st.session_state.sku
mode = st.session_state.mode
# Run recommender function
safety_stock, eoq, rop, rop_date, recommended_mode = recommender(service_level, demand_mean, demand_sd, leadtime_mean, leadtime_sd, holding_cost_per_unit_per_month, handling_cost, inventory, hub , sku, mode)
# display result in 3 col metrics
result_col1.metric("Safety Stock", f"{safety_stock:,}")
result_col2.metric("Economic Order Quantiy", f"{eoq:,}")
result_col3.metric("Reorder Point", f"{rop:,}")
# Display re-order date
with result_container:
# get target row for the chosen SKU and transport mode in CONSOLE HUB
target_row_no = st.session_state.df[(st.session_state.df['hub']=="R.HUB") & (st.session_state.df['sku'] == sku) & (st.session_state.df['mode'] == mode)].index[0]
console_support = st.session_state.df.loc[target_row_no, "console_support"]
if np.isnan(console_support):
background_color = "rgba(250, 202, 43, 0.2)"
text_color = "rgb(148, 124, 45)"
RHUB_support_text = None
elif console_support == False:
background_color = "rgba(255, 43, 43, 0.09)"
text_color = "rgb(125, 53, 59)"
RHUB_support_text = 'R.HUB support within next 90 days for this SKU in all hubs: NO đ¨
'
elif console_support == True:
background_color = "rgba(33, 195, 84, 0.1)"
text_color = "rgb(23, 114, 51)"
RHUB_support_text = 'R.HUB support within next 90 days for this SKU in all hubs: YES â
'
tab1, tab2, = st.tabs(["Status", "Pull Quantity Details"])
with tab1:
st.markdown(f"""
Next re-order date: {rop_date}
Recommended Transport Mode from R.HUB to Hubs: {transport_emoji[recommended_mode]}
{RHUB_support_text}
Current stock in R.HUB: {int(st.session_state.df.loc[target_row_no, "current_stock"])}
Expected total pull quantity from R.HUB within next 90 days: {int(st.session_state.df.loc[target_row_no, "expected_pull_next_90d"])}
* Calculation is based on monthly actual demand (constantly refresh using shipment order detail report) & transit time (transport mode) assuming that all hubs will use the selected transport mode
""", unsafe_allow_html=True)
with tab2:
pull_qty_breakdown = st.session_state.df[["hub","actual_eoq", "rop_date"]][(st.session_state.df["hub"] != "R.HUB") & (st.session_state.df["sku"] == st.session_state.sku) & (st.session_state.df["mode"] == st.session_state.mode) & (st.session_state.df["days_to_rop"] < 90)].set_index("hub")
pull_qty_breakdown['actual_eoq'] = pull_qty_breakdown['actual_eoq'].apply(lambda d: f"{int(d):,}")
pull_qty_breakdown["rop_date"] = pd.to_datetime(pull_qty_breakdown["rop_date"], format=r"%Y-%m-%d").dt.date
st.dataframe(pull_qty_breakdown.sort_values(by=["rop_date"]), use_container_width= True)
# display warning if rop is higher than eoq
if rop > eoq:
result_container.info("Since EOQ is less than ROP due to low handling cost, demand or holding cost, we will plot the inventory level assuming that we order in multiple of EOQ until it exceed ROP. In actual situation, combine them with other skus to yield much better overall costs")
# calculate inventory level over 365 days
data = calc_365_inventory(safety_stock, eoq, rop, demand_mean, demand_sd, leadtime_mean, leadtime_sd, random_demand_leadtime_boolean)
# display matplotlib chart based on data
fig = px.line(data, x="day", y="inventory", title='Inventory levels over a year', labels={"day":"Number of days in a year", "inventory": "Inventory Level"})
fig.add_hline(y=rop, line_dash="dash", line_color="orange", annotation_text="reorder point")
fig.add_hline(y=safety_stock, line_dash="dash", line_color="green", annotation_text="safety stock")
result_container.plotly_chart(fig, theme = 'streamlit')
###############
## STREAMLIT ##
###############
# Create placement containers in sequence
with st.sidebar:
st.subheader("Preset values based on historical data")
# Preset Form
hub_val = st.selectbox('Hub', sorted(st.session_state.df['hub'].unique()), key='hub')
sku_val = st.selectbox('SKU', sorted(st.session_state.df['sku'][(st.session_state.df["hub"]==hub_val)].unique()), key='sku')
mode_val = st.selectbox('Transport Mode', sorted(st.session_state.df['mode'][(st.session_state.df["hub"]==hub_val)&(st.session_state.df["sku"]==sku_val)].unique()), help ="Mode selected would be the same for all hubs and chosen SKU. This would also affect the expected demand from R.HUB within the next 90 days", key='mode')
preset_container = st.container()
preset_container.empty()
preset_submit = st.button(label='Get Preset', on_click=preset, args = (hub_val, sku_val, mode_val))
# download_df = st.session_state.df[['hub','sku','mode','current_stock', 'service_level', 'avg_shipped_qty', 'sd_shipped_qty', 'leadtime_days', 'leadtime_days_sd', 'holding_cost_per_unit_year', 'handling_cost','safety_stock','eoq','actual_eoq','rop', 'rop_date', 'expected_pull_next_90d', 'console_support']].copy()
# download_df.columns = ['hub','sku','mode','current_stock','service_level', 'demand_mean', 'demand_sd', 'leadtime_days', 'leadtime_sd', 'holding_cost_per_unit_year', 'handling_cost_per_order','safety_stock','economic_order_qty','adjusted_economic_order_qty','reorder_point', 'reorder_date', 'expected_pull_next_90d', 'R.HUB_support']
# download_csv = download_df.to_csv().encode('utf-8')
# st.download_button('Download Historical Data with calculated SS, EOQ, ROP', data = download_csv, file_name = 'download_csv.csv', mime='text/csv')
# Important statistics
if "today_inventory" not in st.session_state:
st.session_state.today_inventory = 0
st.session_state.preset = False
stats_col1.metric("Today's Inventory", 0)
stats_col2.metric("Average Demand / Mnth", 0)
stats_col3.metric("Stock Month", 0)
else:
target_row_no = st.session_state.df[(st.session_state.df['hub']==st.session_state.hub) & (st.session_state.df['sku'] == st.session_state.sku) & (st.session_state.df['mode'] == st.session_state.mode)].index[0]
stats_col1.metric("Today's Inventory", f"{st.session_state.df.loc[target_row_no, 'current_stock']:,}")
stats_col2.metric("Average Demand / Mnth", f"{round(st.session_state.df.loc[target_row_no, 'avg_shipped_qty'],2):,}")
stats_col3.metric("Stock Month", f"{round(st.session_state.df.loc[target_row_no, 'current_stock']/st.session_state.df.loc[target_row_no, 'avg_shipped_qty'], 2):,}")
# display inventory across all hubs
try:
with st.expander("Expand to see inventory across all hubs", expanded=False):
# Display Inventory Map across hub
st.subheader(f"Stock months across all hubs: {int(st.session_state.df['current_stock'][(st.session_state.df['sku']==st.session_state.sku)&(st.session_state.df['mode']==st.session_state.mode)].sum()):,}")
layer = pdk.Layer(
"ColumnLayer",
data=st.session_state.df[["hub","current_stock_formatted","stock_month_2dp","lat", "lng", "color"]][(st.session_state.df['sku'] == st.session_state.sku) & (st.session_state.df['mode'] == "Air")],
get_position="[lng, lat]",
auto_highlight=True,
get_elevation="stock_month_2dp",
elevation_scale=200_000,
radius=200_000,
pickable=True,
# elevation_range=[0, 100],
extruded=True,
coverage=1,
# get_fill_color=[255, 165, 0, 100]
get_fill_color=["255", "165 + color", "0", 100]
)
# Set the viewport location
view_state = pdk.ViewState(
longitude=0.0, latitude=10.2323, zoom=1, min_zoom=1, max_zoom=15, pitch=45, bearing=0
)
# Combined all of it and render a viewport
r = pdk.Deck(
map_style="mapbox://styles/mapbox/light-v9",
layers=[layer],
initial_view_state=view_state,
tooltip={"html":
"""Hub: {hub}
Stock Month: {stock_month_2dp}
Stock Level: {current_stock_formatted}
""",
"style": {"color": "white"}},
)
st.pydeck_chart(r)
except:
pass
with st.expander("Expand to see how demand and leadtime affect SS, EOQ, ROP", expanded=False):
data_form = st.form(key="data_form")
data_form.write("*Input the variables in the form below and click submit to derive the recommendation*")
# data_form.write("*Please note that tweaking these variables are only to simulate how different factors will affect the recommendation. These tweaks will not affect the actual reorder date*")
data_form_no_col = data_form.columns(1)
data_form_col1, data_form_col2 = data_form.columns(2)
result_container = st.container()
with result_container:
result_col1, result_col2, result_col3 = st.columns(3)
# Data Form
service_level_val = data_form.slider('Desired service level for SKU (%)', 1, 100, 90, help="Higher service level will result in higher safety stock", key='service_level')
demand_mean_val = data_form_col1.number_input('Average monthly demand for SKU at hub', value = 12.0, min_value = 0.01, step = 1.0, format = "%f", help="Higher demand will result in higher safety stock, economic order quantity and reorder point", key='demand_mean')
demand_sd_val = data_form_col1.number_input('Standard deviation of monthly demand for SKU at hub', value = 1.0, min_value = 0.01, step = 0.01, format = "%f", help="Higher variability of demand month on month will result in higher safety stock", key='demand_sd')
leadtime_mean_val = data_form_col2.number_input('Average lead time from SIN to hub (days)', value = 1.0, min_value = 0.01, step = 1.0, format = "%f", help="Longer lead time will result in higher safety stock and reorder point", key='leadtime_mean')
leadtime_sd_val = data_form_col2.number_input('Standard deviation of lead time from SIN to hub (days)', value = 1.0, min_value = 0.01, step = 0.01, format = "%f", help="Higher variability of lead time will result in higher safety stock", key='leadtime_sd')
holding_cost_per_unit_per_month_val = data_form_col1.number_input('Holding cost per unit of SKU per month', value = 1.0, min_value = 0.01, step = 0.01, format = "%f", help="Higher holding cost will reduce the economic order quantity per order", key='holding_cost_per_unit_per_month')
handling_cost_val = data_form_col2.number_input('Handling cost per shipment order', value = 1.0, min_value = 0.01, step = 1.0, format = "%f", help="Higher handling cost per order will increase the economic order quantity per order", key='handling_cost')
random_demand_leadtime_boolean = data_form.checkbox("Simulate normalised random demand and leadtime", value=False, key='random_demand_leadtime', help="Enabling this will assuming randomised demand and leadtime normalised according to mean and standard deviation input")
data_submit = data_form.form_submit_button(label = "Submit", on_click = display_recommendations, args=())