-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpresentation_tier.py
More file actions
231 lines (190 loc) · 10.5 KB
/
Copy pathpresentation_tier.py
File metadata and controls
231 lines (190 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
import streamlit as st
import plotly.graph_objects as go
import pandas as pd
import tempfile
from fpdf import FPDF
from data_tier import ingest_and_clean_data
from app_tier import generate_forecast
# Set page config to 'wide'
st.set_page_config(page_title="Decision Support System", layout="wide")
# Custom CSS for primary button styling and disabled state
st.markdown("""
<style>
div.stButton > button[kind="primary"] {
background-color: #add8e6;
color: #1a1a1a;
border: 1px solid #add8e6;
}
div.stButton > button[kind="primary"]:disabled {
background-color: #d3d3d3;
color: #808080;
border: 1px solid #d3d3d3;
opacity: 0.6;
}
</style>
""", unsafe_allow_html=True)
st.title("Demand Decision Support System (DSS)")
st.markdown("Supermarket SMEs automated mathematical restock predictor.")
# Sidebar
st.sidebar.header("Data & Configuration")
uploaded_file = st.sidebar.file_uploader("Upload Historical Sales", type=['csv', 'xlsx', 'xls'])
forecast_horizon = st.sidebar.slider("Forecast Horizon (Days)", min_value=7, max_value=60, value=30, step=1)
# Main Execution
if uploaded_file is not None:
# Phase 1: Dynamic Data Mapping
raw_df = ingest_and_clean_data(uploaded_file)
headers = raw_df.columns.tolist()
date_col = st.sidebar.selectbox("Select the Date column", options=headers)
primary_col = st.sidebar.selectbox("Primary Filter Column (e.g., Item/Product)", options=headers)
secondary_col = st.sidebar.selectbox("Secondary Filter Column (e.g., Store/Location)", options=["None"] + headers)
sales_col = st.sidebar.selectbox("Select the Sales/Quantity column", options=headers)
# Phase 2: Isolation & Selection
selected_primary = st.sidebar.selectbox(f"Select specific {primary_col}", options=sorted(raw_df[primary_col].dropna().unique().tolist()))
selected_secondary = None
if secondary_col != "None":
selected_secondary = st.sidebar.selectbox(f"Select specific {secondary_col}", options=sorted(raw_df[secondary_col].dropna().unique().tolist()))
# Session State Memory Handling
current_inputs = {
'forecast_horizon': forecast_horizon,
'date_col': date_col,
'primary_col': primary_col,
'secondary_col': secondary_col,
'sales_col': sales_col,
'selected_primary': selected_primary,
'selected_secondary': selected_secondary
}
last_run_inputs = st.session_state.get('last_run_inputs')
if current_inputs == last_run_inputs:
is_disabled = True
else:
is_disabled = False
# Phase 3: The Execution Trigger
execute = st.button("Run Forecast Engine", type="primary", disabled=is_disabled)
if execute:
st.session_state['last_run_inputs'] = current_inputs
st.session_state['show_results'] = True
st.rerun()
# If the user changes an input, the button wakes up. We must hide old results.
if not is_disabled:
st.session_state['show_results'] = False
if st.session_state.get('show_results', False):
with st.spinner("Analyzing dataset and crunching predictive numbers..."):
try:
# Phase 4: The 3-Year Chop
# Filter the dataframe to only include the selected product
filtered_df = raw_df[raw_df[primary_col] == selected_primary].copy()
if secondary_col != "None":
filtered_df = filtered_df[filtered_df[secondary_col] == selected_secondary].copy()
# Convert the user-mapped Date column to datetime format and set it as the index
filtered_df[date_col] = pd.to_datetime(filtered_df[date_col], errors='coerce')
filtered_df = filtered_df.dropna(subset=[date_col])
filtered_df = filtered_df.set_index(date_col)
filtered_df = filtered_df.sort_index()
# Convert the user-mapped Sales column to numeric
filtered_df[sales_col] = pd.to_numeric(filtered_df[sales_col], errors='coerce')
filtered_df = filtered_df.dropna(subset=[sales_col])
# Resample the data by day (.resample('D').sum().fillna(0))
# CRITICAL: Apply a 3-year data limit using .tail(1095)
clean_series = filtered_df[sales_col].resample('D').sum().fillna(0).tail(1095)
# Data Health X-Ray
with st.expander("Data Health X-Ray", expanded=False):
st.write(f"**Number of clean records:** {len(clean_series)}")
if len(clean_series) > 0:
st.write(f"**Date Range:** {clean_series.index.min().strftime('%Y-%m-%d')} to {clean_series.index.max().strftime('%Y-%m-%d')}")
if len(clean_series) == 0:
st.error("No valid data available after processing. Please check the dataset and mappings.")
else:
# Business engine Layer processing
forecast_series, order, has_seasonality, custom_mae, custom_rmse = generate_forecast(
clean_series,
forecast_horizon
)
# KPI Metrics
col1, col2, col3 = st.columns(3)
with col1:
total_restock = sum(forecast_series)
st.metric("Total Restock Units", f"{total_restock:,.0f}")
with col2:
algo_str = "SARIMA" if has_seasonality else "ARIMA"
st.metric("Algorithm Selected", f"{algo_str} {order}")
with col3:
st.metric(
"System Error Rate",
f"MAE: {custom_mae:.2f}",
delta=f"RMSE: {custom_rmse:.2f}",
delta_color="inverse"
)
st.divider()
# Visualization
st.subheader("Sales Visualizer")
history_plot = clean_series.tail(90)
fig = go.Figure()
# Historical data line
fig.add_trace(go.Scatter(
x=history_plot.index,
y=history_plot.values,
mode='lines',
name='Historical Sales',
line=dict(color='blue')
))
# Bridging points to visually connect the historical line and forecast line
if len(history_plot) > 0 and len(forecast_series) > 0:
bridge_x = [history_plot.index[-1]] + list(forecast_series.index)
bridge_y = [history_plot.values[-1]] + list(forecast_series.values)
# Future forecasted data line
fig.add_trace(go.Scatter(
x=bridge_x,
y=bridge_y,
mode='lines',
name='System Forecast',
line=dict(color='red', dash='dash')
))
fig.update_layout(
xaxis_title="Timeline",
yaxis_title="Sales Quantity",
hovermode="x unified",
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
)
st.plotly_chart(fig, use_container_width=True)
# Procurement Strategy Table
st.subheader("Procurement Strategy")
# Group daily prediction to weekly
weekly_forecast = forecast_series.resample('W').sum().reset_index()
weekly_forecast.columns = ['Week', 'Required Total Units']
# Render dataframe
st.dataframe(weekly_forecast, use_container_width=True)
st.info("Strategic Advisory: To mitigate supply chain friction and anomalous demand spikes, it is recommended to procure an additional 5% contingency buffer on top of the final forecasted volumes.")
# PDF Download Button Config
pdf = FPDF()
pdf.add_page()
pdf.set_font("Arial", "B", 16)
pdf.cell(0, 10, "SME Procurement Strategy", ln=True, align="C")
pdf.ln(10)
pdf.set_font("Arial", "B", 12)
pdf.cell(95, 10, "Week", border=1, align="C")
pdf.cell(95, 10, "Required Total Units", border=1, align="C")
pdf.ln()
pdf.set_font("Arial", "", 12)
for _, row in weekly_forecast.iterrows():
week_str = str(row["Week"]).split(" ")[0]
units_str = f"{row['Required Total Units']:.2f}"
pdf.cell(95, 10, week_str, border=1, align="C")
pdf.cell(95, 10, units_str, border=1, align="C")
pdf.ln()
pdf.ln(5)
pdf.set_font("Arial", "I", 11)
pdf.multi_cell(0, 10, "Strategic Advisory: To mitigate supply chain friction and anomalous demand spikes, it is recommended to procure an additional 5% contingency buffer on top of the final forecasted volumes.", align="L")
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
pdf.output(tmp.name)
with open(tmp.name, "rb") as f:
pdf_payload = f.read()
st.download_button(
label=" Export Weekly Strategy as PDF",
data=pdf_payload,
file_name="weekly_restock_protocol.pdf",
mime="application/pdf"
)
except Exception as e:
st.error(f"Error Processing Request: {e}")
else:
st.info("Awaiting file upload... Please configure limits and provide historical sales data on the left to begin.")