bmatchatom commited on
Commit
9f6d2ac
·
verified ·
1 Parent(s): 969c38d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +210 -2
app.py CHANGED
@@ -1,3 +1,211 @@
1
- import trackio
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- trackio.show()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ from prophet import Prophet
5
+ import plotly.graph_objects as go
6
+ from datetime import timedelta, datetime
7
+ import os
8
+ import math
9
+ from sqlalchemy import create_engine, text
10
+ import urllib.parse
11
+ from streamlit_autorefresh import st_autorefresh
12
+ import gc
13
 
14
+ # =====================================================
15
+ # 1. CONFIGURATION & CONNEXION
16
+ # =====================================================
17
+ st.set_page_config(page_title="Master Planner - Perf & Plan", layout="wide")
18
+
19
+ DB_USER = "balakibawi"
20
+ DB_PASS = "M@tch47om_2026"
21
+ DB_HOST = "10.228.11.110"
22
+ DB_NAME = "wfm_reporting"
23
+
24
+ @st.cache_resource
25
+ def get_engine():
26
+ try:
27
+ safe_password = urllib.parse.quote_plus(DB_PASS)
28
+ return create_engine(f"mysql+pymysql://{DB_USER}:{safe_password}@{DB_HOST}/{DB_NAME}", connect_args={'connect_timeout': 5})
29
+ except:
30
+ return None
31
+
32
+ # =====================================================
33
+ # 2. FONCTIONS DE CALCUL (ERLANG & PRECISION)
34
+ # =====================================================
35
+ def apply_business_rules(row, acts):
36
+ h = row['ds'].hour + row['ds'].minute/60
37
+ d = row['ds'].weekday()
38
+ if h < 7 or h >= 21: return 0
39
+ if "PDV" in acts and d == 6: return 0
40
+ return max(0, row['yhat'])
41
+
42
+ def erlang_c_besoin(calls, aht, interval_sec=1800, service_level=0.8, target_time=20):
43
+ calls = max(0, calls)
44
+ if calls <= 0 or aht <= 0: return 0
45
+ intensity = (calls * aht) / interval_sec
46
+ agents = math.ceil(intensity) + 1
47
+
48
+ def get_service_level(n, intensity, aht, target_time):
49
+ try:
50
+ rho = intensity / n
51
+ if rho >= 1: return 0
52
+ c_part = math.exp(n * math.log(intensity) - (math.lgamma(n + 1) + math.log(1 - rho)))
53
+ sum_inv = sum([math.exp(i * math.log(intensity) - math.lgamma(i + 1)) for i in range(n)])
54
+ prob_attente = c_part / (sum_inv + c_part)
55
+ return 1 - (prob_attente * math.exp(-(n - intensity) * (target_time / aht)))
56
+ except: return 0
57
+
58
+ while agents < 500:
59
+ if get_service_level(agents, intensity, aht, target_time) >= service_level: break
60
+ agents += 1
61
+ return agents
62
+
63
+ def calculer_precision_performance(reel, prev):
64
+ mask = (reel.notnull()) & (reel > 0)
65
+ if not mask.any(): return 0.0
66
+ erreur = np.sum(np.abs(reel[mask].values - prev[mask].clip(lower=0).values))
67
+ somme = np.sum(reel[mask].values)
68
+ return max(0, min(100, (1 - (erreur / somme)) * 100)) if somme > 0 else 0.0
69
+
70
+ # =====================================================
71
+ # 3. CHARGEMENT DES DONNÉES
72
+ # =====================================================
73
+ @st.cache_data(ttl=300)
74
+ def load_data_source(uploaded_file=None):
75
+ if uploaded_file is not None:
76
+ df = pd.read_csv(uploaded_file)
77
+ else:
78
+ path = "full_history.csv"
79
+ if os.path.exists(path):
80
+ df = pd.read_csv(path)
81
+ else:
82
+ return pd.DataFrame()
83
+
84
+ df['ds'] = pd.to_datetime(df['ds'])
85
+ all_times = pd.date_range(start=df['ds'].min(), end=df['ds'].max(), freq='30min')
86
+ df_list = []
87
+ for act in df['activite'].unique():
88
+ temp = df[df['activite'] == act].set_index('ds').reindex(all_times).fillna(0).reset_index()
89
+ temp['activite'] = act
90
+ temp.rename(columns={'index': 'ds'}, inplace=True)
91
+ df_list.append(temp)
92
+ return pd.concat(df_list, ignore_index=True)
93
+
94
+ # =====================================================
95
+ # 4. MOTEUR DE PRÉVISION
96
+ # =====================================================
97
+ @st.cache_resource(ttl=3600)
98
+ def train_and_forecast(_df, activities):
99
+ if _df.empty or not activities: return pd.DataFrame()
100
+ results = []
101
+ for act in activities:
102
+ gc.collect()
103
+ df_act = _df[_df['activite'] == act].tail(8000).copy()
104
+
105
+ m_vol = Prophet(seasonality_mode='multiplicative', daily_seasonality=True, weekly_seasonality=True, uncertainty_samples=50)
106
+ m_vol.add_country_holidays(country_name='FR')
107
+ m_vol.fit(df_act[['ds', 'y']])
108
+
109
+ m_aht = Prophet(daily_seasonality=True, weekly_seasonality=True, uncertainty_samples=50)
110
+ m_aht.fit(df_act[['ds', 'aht']].rename(columns={'aht': 'y'}))
111
+
112
+ future = m_vol.make_future_dataframe(periods=48*14, freq="30min")
113
+ res_vol = m_vol.predict(future)[['ds', 'yhat']]
114
+ res_aht = m_aht.predict(future)[['ds', 'yhat']].rename(columns={'yhat': 'aht_hat'})
115
+
116
+ res_act = res_vol.merge(res_aht, on='ds')
117
+ res_act['activite'] = act
118
+ results.append(res_act)
119
+ return pd.concat(results, ignore_index=True) if results else pd.DataFrame()
120
+
121
+ # =====================================================
122
+ # 5. UI PRINCIPALE
123
+ # =====================================================
124
+ st.sidebar.title("📊 Pilotage Perf & Plan")
125
+ up_file = st.sidebar.file_uploader("Mettre à jour le CSV", type="csv")
126
+
127
+ df_full = load_data_source(up_file)
128
+
129
+ if df_full.empty:
130
+ st.info("Veuillez uploader un fichier 'full_history.csv' pour commencer.")
131
+ st.stop()
132
+
133
+ mode = st.sidebar.selectbox("Vue", ["Rétrospective", "Planification Futur"])
134
+ all_acts = sorted(df_full['activite'].unique().tolist())
135
+ sel_act = st.sidebar.multiselect("Activités", options=all_acts, default=all_acts[:2])
136
+
137
+ # --- NOUVEAU : SIMULATEUR DE SCÉNARIO ---
138
+ st.sidebar.markdown("---")
139
+ st.sidebar.header("🧪 Simulateur d'Impact")
140
+ var_vol = st.sidebar.slider("Variation Volume (%)", -30, 50, 0)
141
+ var_aht = st.sidebar.slider("Variation DMT (%)", -20, 30, 0)
142
+
143
+ if not sel_act:
144
+ st.warning("Sélectionnez une activité.")
145
+ st.stop()
146
+
147
+ with st.spinner("Calcul des prévisions..."):
148
+ fc_all = train_and_forecast(df_full[df_full['activite'].isin(sel_act)], sel_act)
149
+
150
+ if not fc_all.empty:
151
+ fc_all['yhat'] = fc_all.apply(lambda r: apply_business_rules(r, [r['activite']]), axis=1)
152
+ fc_all['work_load_pred'] = fc_all['yhat'] * fc_all['aht_hat']
153
+
154
+ df_filtered = df_full[df_full['activite'].isin(sel_act)][['ds', 'activite', 'y']]
155
+ hist = fc_all.merge(df_filtered, on=['ds', 'activite'], how='left')
156
+
157
+ hist_agg = hist.groupby('ds').agg({'y': 'sum', 'yhat': 'sum', 'work_load_pred': 'sum'}).reset_index()
158
+ hist_agg['aht_hat'] = (hist_agg['work_load_pred'] / hist_agg['yhat']).fillna(180)
159
+
160
+ d_min, d_max = hist_agg['ds'].min().date(), hist_agg['ds'].max().date()
161
+ sel_range = st.sidebar.date_input("Période", value=(d_min, d_max))
162
+
163
+ if len(sel_range) == 2:
164
+ mask = (hist_agg['ds'].dt.date >= sel_range[0]) & (hist_agg['ds'].dt.date <= sel_range[1])
165
+ view = hist_agg[mask].copy()
166
+
167
+ # --- APPLICATION SIMULATION ---
168
+ view['yhat_sim'] = view['yhat'] * (1 + var_vol/100)
169
+ view['aht_sim'] = view['aht_hat'] * (1 + var_aht/100)
170
+
171
+ # Calcul besoins (Base vs Simulé)
172
+ view['besoin_agents'] = view.apply(lambda r: erlang_c_besoin(r['yhat'], r['aht_hat']), axis=1)
173
+ view['besoin_sim'] = view.apply(lambda r: erlang_c_besoin(r['yhat_sim'], r['aht_sim']), axis=1)
174
+
175
+ st.title(f"🚀 Master Planner - {mode}")
176
+
177
+ # Métriques
178
+ c1, c2, c3, c4 = st.columns(4)
179
+ c1.metric("🎯 Précision", f"{calculer_precision_performance(view['y'], view['yhat']):.1f}%")
180
+ c2.metric("📈 Volume Prévu", f"{int(view['yhat_sim'].sum()):,}", delta=f"{var_vol}%" if var_vol!=0 else None)
181
+
182
+ staff_base = math.ceil(view[view['besoin_agents']>0]['besoin_agents'].mean())
183
+ staff_sim = math.ceil(view[view['besoin_sim']>0]['besoin_sim'].mean())
184
+ c3.metric("👥 Staff Requis", f"{staff_sim} agents", delta=f"{staff_sim - staff_base} agents" if staff_sim != staff_base else None)
185
+
186
+ dmt_moy = int(view['aht_sim'].mean())
187
+ c4.metric("⏱️ DMT (moy)", f"{dmt_moy}s", delta=f"{var_aht}%" if var_aht!=0 else None)
188
+
189
+ # Graphique
190
+ fig = go.Figure()
191
+ if mode == "Rétrospective":
192
+ fig.add_trace(go.Scatter(x=view['ds'], y=view['y'], name="RÉEL", line=dict(color="#1f77b4", width=3)))
193
+
194
+ fig.add_trace(go.Scatter(x=view['ds'], y=view['yhat_sim'], name="PRÉVISION (Simulée)", line=dict(color="#ff7f0e", dash='dot')))
195
+
196
+ fig.update_layout(title="Courbe de Charge (Workload)", hovermode="x unified")
197
+ st.plotly_chart(fig, use_container_width=True)
198
+
199
+ # Tableau de bord Staffing
200
+ with st.expander("📅 Détails de la planification"):
201
+ st.dataframe(view[['ds', 'y', 'yhat_sim', 'aht_sim', 'besoin_sim']].rename(columns={
202
+ 'ds': 'Intervalle', 'y': 'Réel', 'yhat_sim': 'Prévu', 'aht_sim': 'DMT', 'besoin_sim': 'Agents Requis'
203
+ }), use_container_width=True)
204
+
205
+ st.download_button("📥 Exporter le plan de charge", view.to_csv(index=False), "planification.csv", "text/csv")
206
+
207
+ # =====================================================
208
+ # 9. FOOTER
209
+ # =====================================================
210
+ st.sidebar.markdown("---")
211
+ st.sidebar.caption(f"Propulsé par Prophet & Erlang-C | MatchAtom 2026")