Upload 3 files
Browse files- app.py +176 -0
- model.pkl +3 -0
- requirements.txt +7 -0
app.py
ADDED
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import numpy as np
|
4 |
+
import torch
|
5 |
+
import torch.nn as nn
|
6 |
+
import pickle
|
7 |
+
import plotly.express as px
|
8 |
+
from sklearn.preprocessing import MinMaxScaler
|
9 |
+
|
10 |
+
# Define the LSTM model class (same as in train_model.py)
|
11 |
+
class LSTMModel(nn.Module):
|
12 |
+
def __init__(self, input_size=1, hidden_size=50, num_layers=2):
|
13 |
+
super(LSTMModel, self).__init__()
|
14 |
+
self.hidden_size = hidden_size
|
15 |
+
self.num_layers = num_layers
|
16 |
+
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
|
17 |
+
self.fc = nn.Linear(hidden_size, 1)
|
18 |
+
|
19 |
+
def forward(self, x):
|
20 |
+
h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size)
|
21 |
+
c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size)
|
22 |
+
out, _ = self.lstm(x, (h0, c0))
|
23 |
+
out = self.fc(out[:, -1, :])
|
24 |
+
return out
|
25 |
+
|
26 |
+
# Load the pre-trained model and scaler
|
27 |
+
@st.cache_resource
|
28 |
+
def load_model_and_scaler():
|
29 |
+
with open("model.pkl", "rb") as f:
|
30 |
+
saved_data = pickle.load(f)
|
31 |
+
model = saved_data['model']
|
32 |
+
scaler = saved_data['scaler']
|
33 |
+
model.eval()
|
34 |
+
return model, scaler
|
35 |
+
|
36 |
+
# Load and preprocess data
|
37 |
+
@st.cache_data
|
38 |
+
def load_and_preprocess_data():
|
39 |
+
df = pd.read_excel("SmartLab_Consumables_Forecast_Sample.xlsx")
|
40 |
+
df['Usage_Date'] = pd.to_datetime(df['Usage_Date'])
|
41 |
+
return df
|
42 |
+
|
43 |
+
# Function to generate forecasts using the model
|
44 |
+
def generate_forecasts(df, model, scaler, sequence_length=10):
|
45 |
+
consumables = df['Consumable_Name'].unique()
|
46 |
+
forecasts = []
|
47 |
+
|
48 |
+
for consumable in consumables:
|
49 |
+
consumable_data = df[df['Consumable_Name'] == consumable].set_index('Usage_Date')
|
50 |
+
consumable_data = consumable_data['Quantity_Used'].resample('D').sum().fillna(0)
|
51 |
+
if len(consumable_data) < sequence_length:
|
52 |
+
continue
|
53 |
+
|
54 |
+
# Scale the data
|
55 |
+
scaled_data = scaler.transform(consumable_data.values.reshape(-1, 1))
|
56 |
+
|
57 |
+
# Prepare the last sequence for prediction
|
58 |
+
last_sequence = scaled_data[-sequence_length:]
|
59 |
+
last_sequence = torch.FloatTensor(last_sequence).unsqueeze(0) # Shape: (1, sequence_length, 1)
|
60 |
+
|
61 |
+
# Predict future usage
|
62 |
+
with torch.no_grad():
|
63 |
+
pred_7 = model(last_sequence).numpy()
|
64 |
+
pred_14 = model(last_sequence).numpy() * 2 # Simplified scaling for 14 days
|
65 |
+
pred_30 = model(last_sequence).numpy() * 4 # Simplified scaling for 30 days
|
66 |
+
|
67 |
+
# Inverse transform predictions
|
68 |
+
pred_7 = scaler.inverse_transform(pred_7)[0][0]
|
69 |
+
pred_14 = scaler.inverse_transform(pred_14)[0][0]
|
70 |
+
pred_30 = scaler.inverse_transform(pred_30)[0][0]
|
71 |
+
|
72 |
+
# Add to forecasts
|
73 |
+
latest_date = consumable_data.index.max()
|
74 |
+
for _, row in df[df['Consumable_Name'] == consumable].iterrows():
|
75 |
+
forecasts.append({
|
76 |
+
'Lab_ID': row['Lab_ID'],
|
77 |
+
'Consumable_Name': consumable,
|
78 |
+
'Usage_Date': latest_date,
|
79 |
+
'Quantity_Used': row['Quantity_Used'],
|
80 |
+
'Current_Stock': row['Current_Stock'],
|
81 |
+
'Reorder_Threshold': row['Reorder_Threshold'],
|
82 |
+
'Forecast_7_Days': pred_7,
|
83 |
+
'Forecast_14_Days': pred_14,
|
84 |
+
'Forecast_30_Days': pred_30,
|
85 |
+
'Reorder_Recommended': row['Current_Stock'] < row['Reorder_Threshold'] or (row['Current_Stock'] - pred_30) < row['Reorder_Threshold'],
|
86 |
+
'Order_Suggestion': max(pred_30 - row['Current_Stock'] + row['Reorder_Threshold'], 0) if (row['Current_Stock'] < row['Reorder_Threshold'] or (row['Current_Stock'] - pred_30) < row['Reorder_Threshold']) else 0,
|
87 |
+
'Forecast_Generated_Date': pd.to_datetime('2025-06-06')
|
88 |
+
})
|
89 |
+
|
90 |
+
return pd.DataFrame(forecasts)
|
91 |
+
|
92 |
+
# Streamlit app
|
93 |
+
def main():
|
94 |
+
st.set_page_config(page_title="SmartLab Consumables Forecasting", layout="wide")
|
95 |
+
st.title("SmartLab Consumables Forecasting Dashboard")
|
96 |
+
st.markdown("Forecast consumable usage, monitor stock thresholds, and get order suggestions.")
|
97 |
+
|
98 |
+
# Load model and data
|
99 |
+
with st.spinner("Loading model and data..."):
|
100 |
+
model, scaler = load_model_and_scaler()
|
101 |
+
df = load_and_preprocess_data()
|
102 |
+
forecast_df = generate_forecasts(df, model, scaler)
|
103 |
+
|
104 |
+
# Sidebar filters
|
105 |
+
st.sidebar.header("Filters")
|
106 |
+
labs = ['All Labs'] + sorted(forecast_df['Lab_ID'].unique().tolist())
|
107 |
+
selected_lab = st.sidebar.selectbox("Filter by Lab", labs)
|
108 |
+
|
109 |
+
consumables = ['All Consumables'] + sorted(forecast_df['Consumable_Name'].unique().tolist())
|
110 |
+
selected_consumable = st.sidebar.selectbox("Filter by Consumable", consumables)
|
111 |
+
|
112 |
+
# Apply filters
|
113 |
+
filtered_df = forecast_df.copy()
|
114 |
+
if selected_lab != 'All Labs':
|
115 |
+
filtered_df = filtered_df[filtered_df['Lab_ID'] == selected_lab]
|
116 |
+
if selected_consumable != 'All Consumables':
|
117 |
+
filtered_df = filtered_df[filtered_df['Consumable_Name'] == selected_consumable]
|
118 |
+
|
119 |
+
# Forecast Overview Chart
|
120 |
+
st.header("Forecast Overview")
|
121 |
+
chart_data = filtered_df.groupby(['Lab_ID', 'Consumable_Name']).agg({
|
122 |
+
'Forecast_7_Days': 'mean',
|
123 |
+
'Forecast_14_Days': 'mean',
|
124 |
+
'Forecast_30_Days': 'mean'
|
125 |
+
}).reset_index()
|
126 |
+
chart_data['name'] = chart_data['Lab_ID'] + ' - ' + chart_data['Consumable_Name']
|
127 |
+
chart_data = chart_data.melt(id_vars=['name'], value_vars=['Forecast_7_Days', 'Forecast_14_Days', 'Forecast_30_Days'],
|
128 |
+
var_name='Forecast_Period', value_name='Forecasted_Usage')
|
129 |
+
|
130 |
+
fig = px.line(chart_data, x='name', y='Forecasted_Usage', color='Forecast_Period',
|
131 |
+
labels={'Forecasted_Usage': 'Forecasted Usage', 'name': 'Lab - Consumable'},
|
132 |
+
color_discrete_map={
|
133 |
+
'Forecast_7_Days': '#8884d8',
|
134 |
+
'Forecast_14_Days': '#82ca9d',
|
135 |
+
'Forecast_30_Days': '#ff7300'
|
136 |
+
})
|
137 |
+
fig.update_layout(
|
138 |
+
xaxis_title="Lab - Consumable",
|
139 |
+
yaxis_title="Forecasted Usage",
|
140 |
+
xaxis_tickangle=45,
|
141 |
+
margin=dict(b=150),
|
142 |
+
legend_title_text='Forecast Period'
|
143 |
+
)
|
144 |
+
st.plotly_chart(fig, use_container_width=True)
|
145 |
+
|
146 |
+
# Inventory Status Table
|
147 |
+
st.header("Inventory Status")
|
148 |
+
display_df = filtered_df.copy()
|
149 |
+
display_df['Reorder_Recommended'] = display_df['Reorder_Recommended'].apply(lambda x: 'Yes 🚩' if x else 'No')
|
150 |
+
display_df['Order_Suggestion'] = display_df['Order_Suggestion'].apply(lambda x: f"{x:.0f} units" if x > 0 else "None")
|
151 |
+
display_df = display_df[[
|
152 |
+
'Lab_ID', 'Consumable_Name', 'Current_Stock', 'Reorder_Threshold',
|
153 |
+
'Forecast_7_Days', 'Forecast_14_Days', 'Forecast_30_Days',
|
154 |
+
'Reorder_Recommended', 'Order_Suggestion'
|
155 |
+
]]
|
156 |
+
# Apply conditional styling
|
157 |
+
def highlight_reorder(row):
|
158 |
+
return ['background-color: #fee2e2' if row['Reorder_Recommended'] == 'Yes 🚩' else '' for _ in row]
|
159 |
+
styled_df = display_df.style.apply(highlight_reorder, axis=1).format({
|
160 |
+
'Forecast_7_Days': '{:.2f}',
|
161 |
+
'Forecast_14_Days': '{:.2f}',
|
162 |
+
'Forecast_30_Days': '{:.2f}'
|
163 |
+
})
|
164 |
+
st.dataframe(styled_df, use_container_width=True)
|
165 |
+
|
166 |
+
# Interesting Fact
|
167 |
+
st.header("Interesting Fact")
|
168 |
+
max_forecast = chart_data[chart_data['Forecast_Period'] == 'Forecast_30_Days'].nlargest(1, 'Forecasted_Usage')
|
169 |
+
if not max_forecast.empty:
|
170 |
+
st.info(
|
171 |
+
f"Did you know? The consumable with the highest 30-day forecast is **{max_forecast['name'].iloc[0]}** "
|
172 |
+
f"at **{max_forecast['Forecasted_Usage'].iloc[0]:.2f} units**, indicating a significant upcoming demand!"
|
173 |
+
)
|
174 |
+
|
175 |
+
if __name__ == "__main__":
|
176 |
+
main()
|
model.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:83cb56b3d8af822dfd20570b08fe98294b0bd0a58604b3846f68b93f4d2537d8
|
3 |
+
size 129057
|
requirements.txt
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
pandas
|
2 |
+
numpy
|
3 |
+
torch
|
4 |
+
scikit-learn
|
5 |
+
openpyxl
|
6 |
+
streamlit
|
7 |
+
plotly
|