jaimalklyde commited on
Commit
8074021
·
verified ·
1 Parent(s): 8d6bb64

Update src/module/Dashboard.py

Browse files
Files changed (1) hide show
  1. src/module/Dashboard.py +141 -141
src/module/Dashboard.py CHANGED
@@ -1,19 +1,19 @@
1
  """
2
  MODULE: EXECUTIVE DASHBOARD (DIRECTOR VIEW)
3
  ===========================================
4
- Responsabilité : Vue synthétique et décisionnelle pour la direction.
5
- Agrège : ETL (Passé) + Forecasting (Futur) + DQ (Qualité).
6
- Style : Gotham UI (Haute Visibilité)
7
  """
8
 
9
  import streamlit as st
10
  import pandas as pd
11
  import plotly.express as px
12
  import plotly.graph_objects as go
13
- from datetime import timedelta
14
  import numpy as np
 
15
 
16
- # Import pour charger les données si nécessaire
17
  from module.Validation_Contract import ETL_Engine
18
 
19
  class HospitalDashboard:
@@ -26,7 +26,7 @@ class HospitalDashboard:
26
 
27
  def load_data(self):
28
  """Charge les données via le cache ou l'ETL"""
29
- # 1. Tenter de récupérer depuis le session_state (Data Explorer)
30
  if 'data_explorer' in st.session_state and hasattr(st.session_state.data_explorer, 'dataframes'):
31
  dfs = st.session_state.data_explorer.dataframes
32
  self.df_res = dfs.get('ressources_jour')
@@ -46,26 +46,17 @@ class HospitalDashboard:
46
 
47
  def _get_kpis(self):
48
  """Calcule les indicateurs MACRO pour le bandeau"""
49
- # On prend la dernière date disponible (Aujourd'hui simulé)
50
  last_date = pd.to_datetime(self.df_res['date']).max()
51
-
52
- # Filtre sur "Aujourd'hui"
53
  df_today = self.df_res[pd.to_datetime(self.df_res['date']) == last_date]
54
  df_adm_today = self.df_adm[pd.to_datetime(self.df_adm['date']) == last_date]
55
 
56
- # 1. Taux d'Occupation Global
57
- # Somme des lits occupés / Somme des lits totaux
58
  total_lits = df_today['lits_total'].sum()
59
  total_occupes = df_today['lits_occupes'].sum()
60
  taux_global = (total_occupes / total_lits * 100) if total_lits > 0 else 0
61
 
62
- # 2. Admissions du jour
63
  nb_adm = df_adm_today['nb_admissions'].sum()
64
-
65
- # 3. Services en Tension (Occupation > 85%)
66
  nb_tension = df_today[df_today['taux_occupation'] > 0.85].shape[0]
67
 
68
- # 4. Alertes Stock (Dernière date stock)
69
  last_date_stock = pd.to_datetime(self.df_stock['date']).max()
70
  df_stock_today = self.df_stock[pd.to_datetime(self.df_stock['date']) == last_date_stock]
71
  nb_ruptures = df_stock_today[df_stock_today['alerte_stock'] == 1].shape[0]
@@ -79,185 +70,194 @@ class HospitalDashboard:
79
  }
80
 
81
  def plot_main_chart(self):
82
- """Le graphique hybride : Historique + Prédiction (si dispo)"""
83
-
84
  fig = go.Figure()
85
 
86
- # --- A. HISTORIQUE (ETL) ---
87
- # On agrège par date pour avoir une courbe globale
88
  df_hist = self.df_res.copy()
89
  df_hist['date'] = pd.to_datetime(df_hist['date'])
90
  daily_occ = df_hist.groupby('date')['taux_occupation'].mean() * 100
91
-
92
- # On affiche les 30 derniers jours
93
  daily_occ = daily_occ.tail(30)
94
 
95
  fig.add_trace(go.Scatter(
96
  x=daily_occ.index, y=daily_occ.values,
97
- mode='lines+markers',
98
- name='Historique',
99
  line=dict(color='#58a6ff', width=3),
100
- fill='tozeroy',
101
- fillcolor='rgba(88, 166, 255, 0.1)'
102
  ))
103
 
104
- # --- B. PRÉDICTION (Forecasting Module) ---
105
- # On vérifie si une prédiction a été générée dans Forecasting
106
  has_forecast = False
107
  if 'forecaster' in st.session_state:
108
  forecaster = st.session_state['forecaster']
109
- # On génère 30 jours de forecast à la volée (rapide)
110
- # Attention : forecaster travaille sur 'nb_admissions' ou 'taux_occupation' selon le choix
111
- # Pour l'exemple dashboard, on suppose qu'on veut afficher ce qui a été entraîné
112
  try:
 
113
  f_dates, f_values = forecaster.generate_future_forecast(months=1)
114
-
115
- # Si le modèle a prédit le taux d'occupation (valeurs ~0.8), on multiplie par 100
116
- if np.mean(f_values) < 2:
117
- f_values = f_values * 100
118
 
119
  fig.add_trace(go.Scatter(
120
  x=f_dates, y=f_values,
121
- mode='lines',
122
- name='Projection IA',
123
  line=dict(color='#00E676', width=3, dash='dot')
124
  ))
125
  has_forecast = True
126
- except:
127
- pass # Si erreur forecast, on affiche juste l'historique
128
 
129
- # Ligne de seuil critique (100%)
130
  fig.add_hline(y=100, line_dash="dash", line_color="#FF5252", annotation_text="Saturation")
131
 
132
- title = "Taux d'Occupation : Réalité vs Projection IA" if has_forecast else "Taux d'Occupation (Historique Récent)"
133
-
134
- fig.update_layout(
135
- title=title,
136
- template="plotly_dark",
137
- height=350,
138
- margin=dict(l=20, r=20, t=40, b=20),
139
- hovermode="x unified",
140
- yaxis_title="Occupation (%)"
141
- )
142
  return fig
143
 
144
  def plot_service_tension(self):
145
- """Bar chart des services les plus chargés aujourd'hui"""
146
  last_date = pd.to_datetime(self.df_res['date']).max()
147
  df_today = self.df_res[pd.to_datetime(self.df_res['date']) == last_date].copy()
148
-
149
  df_today['taux_pct'] = df_today['taux_occupation'] * 100
150
- df_sorted = df_today.sort_values('taux_pct', ascending=True) # Ascending pour barh
151
 
152
- # Couleurs conditionnelles
153
  colors = ['#FF5252' if x >= 100 else '#FFA726' if x >= 85 else '#66BB6A' for x in df_sorted['taux_pct']]
154
 
155
  fig = go.Figure(go.Bar(
156
- x=df_sorted['taux_pct'],
157
- y=df_sorted['service_code'],
158
- orientation='h',
159
- marker_color=colors,
160
- text=df_sorted['taux_pct'].round(1).astype(str) + '%',
161
- textposition='auto'
162
  ))
163
-
164
- fig.update_layout(
165
- title=f"État des Services ({last_date.strftime('%d/%m')})",
166
- template="plotly_dark",
167
- height=350,
168
- margin=dict(l=20, r=20, t=40, b=20),
169
- xaxis_title="% Occupation"
170
- )
171
  return fig
172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
  def show_dashboard(client, sheet_name):
175
  st.markdown("## 🚁 COCKPIT DE DIRECTION")
176
- st.caption("Vue synthétique en temps réel de l'hôpital.")
177
 
178
  dashboard = HospitalDashboard(client, sheet_name)
179
 
180
- # Chargement
181
- with st.spinner("Actualisation des données décisionnelles..."):
182
- if not dashboard.load_data():
183
- st.error("Impossible de charger les données. Vérifiez la connexion.")
184
- return
185
 
186
- # --- 1. BANDEAU KPIs ---
187
  kpis = dashboard._get_kpis()
188
-
189
- # Style CSS pour les cartes KPI (Optionnel, sinon st.metric standard)
190
  k1, k2, k3, k4 = st.columns(4)
191
 
192
- k1.metric(
193
- "Taux d'Occupation Global",
194
- f"{kpis['taux_global']:.1f}%",
195
- delta="Critique" if kpis['taux_global'] > 90 else "Stable",
196
- delta_color="inverse"
197
- )
198
-
199
- k2.metric(
200
- "Admissions du Jour",
201
- f"{int(kpis['nb_adm'])}",
202
- delta="Patients"
203
- )
204
-
205
- k3.metric(
206
- "Services en Tension",
207
- f"{kpis['nb_tension']}",
208
- delta="> 85% Occ.",
209
- delta_color="inverse"
210
- )
211
-
212
- k4.metric(
213
- "Alertes Stock",
214
- f"{kpis['nb_ruptures']}",
215
- delta="Ruptures",
216
- delta_color="inverse"
217
- )
218
 
219
  st.divider()
220
-
221
- # --- 2. GRAPHIQUES CENTRAUX ---
222
- col_main, col_side = st.columns([2, 1])
223
-
224
- with col_main:
225
- # Graphique Temporel (Hybride)
226
- st.plotly_chart(dashboard.plot_main_chart(), use_container_width=True)
227
-
228
- with col_side:
229
- # Graphique Services
230
- st.plotly_chart(dashboard.plot_service_tension(), use_container_width=True)
231
 
232
- # --- 3. FLASH REPORT & ACTIONS ---
233
- st.markdown("### ⚡ Flash Report & Recommandations")
234
-
235
- col_msg, col_act = st.columns([3, 1])
236
-
237
- with col_msg:
238
- # Logique simple de génération de texte (Simule l'IA Jasmine)
239
- if kpis['taux_global'] > 95:
240
- msg_status = "🔴 **CRITIQUE :** L'hôpital est proche de la saturation totale."
241
- msg_action = "Recommandation : Activer le Plan Blanc et délester les services d'Urgence."
242
- elif kpis['taux_global'] > 85:
243
- msg_status = "🟠 **ATTENTION :** Tensions fortes sur les capacités."
244
- msg_action = "Recommandation : Surveiller les sorties et préparer des lits d'aval."
245
- else:
246
- msg_status = "🟢 **NOMINAL :** Activité hospitalière sous contrôle."
247
- msg_action = "Aucune action critique requise."
248
 
249
- st.info(f"{msg_status}\n\n{msg_action}")
 
 
250
 
251
- with col_act:
252
- st.markdown("**Actions Rapides**")
253
- if st.button("📢 Alerter Chefs de Service"):
254
- st.toast("Notification envoyée aux 12 chefs de service.", icon="📨")
255
-
256
- if st.button("📊 Rapport PDF Complet"):
257
- st.toast("Génération du PDF en cours...", icon="📄")
 
 
 
 
 
 
258
 
 
 
 
 
 
 
 
 
 
 
259
  if 'forecaster' not in st.session_state:
260
- st.warning("⚠️ Prévisions IA non disponibles")
261
- if st.button("Activer l'IA Prédictive"):
262
- st.switch_page("src/streamlit_app.py") # Astuce pour rediriger vers le module forecasting si besoin
263
- # En vrai on demanderait à l'utilisateur d'aller dans l'onglet Forecasting
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  MODULE: EXECUTIVE DASHBOARD (DIRECTOR VIEW)
3
  ===========================================
4
+ Responsabilité : Cockpit de pilotage + Simulateur de crise.
5
+ Agrège : ETL (Passé) + Forecasting (Futur) + Simulation (Hypothèses).
6
+ Style : Gotham UI
7
  """
8
 
9
  import streamlit as st
10
  import pandas as pd
11
  import plotly.express as px
12
  import plotly.graph_objects as go
 
13
  import numpy as np
14
+ from datetime import timedelta
15
 
16
+ # Import pour charger les données
17
  from module.Validation_Contract import ETL_Engine
18
 
19
  class HospitalDashboard:
 
26
 
27
  def load_data(self):
28
  """Charge les données via le cache ou l'ETL"""
29
+ # 1. Tenter de récupérer depuis le session_state
30
  if 'data_explorer' in st.session_state and hasattr(st.session_state.data_explorer, 'dataframes'):
31
  dfs = st.session_state.data_explorer.dataframes
32
  self.df_res = dfs.get('ressources_jour')
 
46
 
47
  def _get_kpis(self):
48
  """Calcule les indicateurs MACRO pour le bandeau"""
 
49
  last_date = pd.to_datetime(self.df_res['date']).max()
 
 
50
  df_today = self.df_res[pd.to_datetime(self.df_res['date']) == last_date]
51
  df_adm_today = self.df_adm[pd.to_datetime(self.df_adm['date']) == last_date]
52
 
 
 
53
  total_lits = df_today['lits_total'].sum()
54
  total_occupes = df_today['lits_occupes'].sum()
55
  taux_global = (total_occupes / total_lits * 100) if total_lits > 0 else 0
56
 
 
57
  nb_adm = df_adm_today['nb_admissions'].sum()
 
 
58
  nb_tension = df_today[df_today['taux_occupation'] > 0.85].shape[0]
59
 
 
60
  last_date_stock = pd.to_datetime(self.df_stock['date']).max()
61
  df_stock_today = self.df_stock[pd.to_datetime(self.df_stock['date']) == last_date_stock]
62
  nb_ruptures = df_stock_today[df_stock_today['alerte_stock'] == 1].shape[0]
 
70
  }
71
 
72
  def plot_main_chart(self):
73
+ """Graphique Hybride : Historique + Prédiction Standard"""
 
74
  fig = go.Figure()
75
 
76
+ # Historique
 
77
  df_hist = self.df_res.copy()
78
  df_hist['date'] = pd.to_datetime(df_hist['date'])
79
  daily_occ = df_hist.groupby('date')['taux_occupation'].mean() * 100
 
 
80
  daily_occ = daily_occ.tail(30)
81
 
82
  fig.add_trace(go.Scatter(
83
  x=daily_occ.index, y=daily_occ.values,
84
+ mode='lines+markers', name='Historique',
 
85
  line=dict(color='#58a6ff', width=3),
86
+ fill='tozeroy', fillcolor='rgba(88, 166, 255, 0.1)'
 
87
  ))
88
 
89
+ # Prédiction IA (si dispo)
 
90
  has_forecast = False
91
  if 'forecaster' in st.session_state:
92
  forecaster = st.session_state['forecaster']
 
 
 
93
  try:
94
+ # On génère 1 mois par défaut pour la vue supervision
95
  f_dates, f_values = forecaster.generate_future_forecast(months=1)
96
+ # Adaptation d'échelle si nécessaire (si le modèle sort 0.8 au lieu de 80)
97
+ if np.mean(f_values) < 2: f_values = f_values * 100
 
 
98
 
99
  fig.add_trace(go.Scatter(
100
  x=f_dates, y=f_values,
101
+ mode='lines', name='Tendance IA',
 
102
  line=dict(color='#00E676', width=3, dash='dot')
103
  ))
104
  has_forecast = True
105
+ except: pass
 
106
 
 
107
  fig.add_hline(y=100, line_dash="dash", line_color="#FF5252", annotation_text="Saturation")
108
 
109
+ title = "Taux d'Occupation : Réalité vs IA" if has_forecast else "Taux d'Occupation (Historique)"
110
+ fig.update_layout(title=title, template="plotly_dark", height=350, margin=dict(l=20, r=20, t=40, b=20))
 
 
 
 
 
 
 
 
111
  return fig
112
 
113
  def plot_service_tension(self):
114
+ """Top Flop des services"""
115
  last_date = pd.to_datetime(self.df_res['date']).max()
116
  df_today = self.df_res[pd.to_datetime(self.df_res['date']) == last_date].copy()
 
117
  df_today['taux_pct'] = df_today['taux_occupation'] * 100
118
+ df_sorted = df_today.sort_values('taux_pct', ascending=True)
119
 
 
120
  colors = ['#FF5252' if x >= 100 else '#FFA726' if x >= 85 else '#66BB6A' for x in df_sorted['taux_pct']]
121
 
122
  fig = go.Figure(go.Bar(
123
+ x=df_sorted['taux_pct'], y=df_sorted['service_code'],
124
+ orientation='h', marker_color=colors,
125
+ text=df_sorted['taux_pct'].round(1).astype(str) + '%', textposition='auto'
 
 
 
126
  ))
127
+ fig.update_layout(title=f"État Services ({last_date.strftime('%d/%m')})", template="plotly_dark", height=350, margin=dict(l=20, r=20, t=40, b=20))
 
 
 
 
 
 
 
128
  return fig
129
 
130
+ def run_simulation(self, impact_admission, impact_lits):
131
+ """Calcul de la simulation (What-If Analysis)"""
132
+ if 'forecaster' not in st.session_state:
133
+ return None, None, None
134
+
135
+ forecaster = st.session_state['forecaster']
136
+
137
+ # 1. On récupère la prédiction de base (3 mois)
138
+ f_dates, f_values = forecaster.generate_future_forecast(months=3)
139
+
140
+ # Si le modèle prédit un taux (0-1), on met en %
141
+ if np.mean(f_values) < 2: f_values = f_values * 100
142
+
143
+ # 2. Application des modificateurs
144
+ # Facteur admission (Numérateur)
145
+ factor_adm = 1 + (impact_admission / 100)
146
+ # Facteur capacité (Dénominateur)
147
+ factor_capa = 1 + (impact_lits / 100)
148
+
149
+ simulated_values = (f_values * factor_adm) / factor_capa
150
+
151
+ return f_dates, f_values, simulated_values
152
+
153
 
154
  def show_dashboard(client, sheet_name):
155
  st.markdown("## 🚁 COCKPIT DE DIRECTION")
156
+ st.caption("Supervision Temps Réel & Simulation Stratégique")
157
 
158
  dashboard = HospitalDashboard(client, sheet_name)
159
 
160
+ # Chargement discret
161
+ if not dashboard.load_data():
162
+ st.error("Données indisponibles.")
163
+ return
 
164
 
165
+ # --- BANDEAU KPIs (Toujours visible) ---
166
  kpis = dashboard._get_kpis()
 
 
167
  k1, k2, k3, k4 = st.columns(4)
168
 
169
+ k1.metric("Taux Occupation", f"{kpis['taux_global']:.1f}%", delta="Critique" if kpis['taux_global'] > 90 else "OK", delta_color="inverse")
170
+ k2.metric("Admissions (J)", f"{int(kpis['nb_adm'])}")
171
+ k3.metric("Services Tendus", f"{kpis['nb_tension']}", delta="Alertes", delta_color="inverse")
172
+ k4.metric("Ruptures Stock", f"{kpis['nb_ruptures']}", delta="Alertes", delta_color="inverse")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
  st.divider()
 
 
 
 
 
 
 
 
 
 
 
175
 
176
+ # --- ONGLETS : SUPERVISION vs SIMULATION ---
177
+ tab_live, tab_sim = st.tabs(["📡 Supervision Temps Réel", "🎛️ Simulateur de Crise"])
178
+
179
+ # === ONGLET 1 : SUPERVISION ===
180
+ with tab_live:
181
+ col_main, col_side = st.columns([2, 1])
182
+ with col_main:
183
+ st.plotly_chart(dashboard.plot_main_chart(), use_container_width=True)
184
+ with col_side:
185
+ st.plotly_chart(dashboard.plot_service_tension(), use_container_width=True)
 
 
 
 
 
 
186
 
187
+ # --- FLASH REPORT DYNAMIQUE (RESTORÉ) ---
188
+ st.markdown("### ⚡ Flash Report & Recommandations")
189
+ col_msg, col_act = st.columns([3, 1])
190
 
191
+ with col_msg:
192
+ # Logique d'IA Textuelle (Restaurée)
193
+ if kpis['taux_global'] > 95:
194
+ msg_status = "🔴 **CRITIQUE :** L'hôpital est proche de la saturation totale."
195
+ msg_action = "👉 **ACTION REQUISE :** Activer le Plan Blanc et délester les services d'Urgence."
196
+ elif kpis['taux_global'] > 85:
197
+ msg_status = "🟠 **ATTENTION :** Tensions fortes sur les capacités."
198
+ msg_action = "👉 **ACTION REQUISE :** Surveiller les sorties et préparer des lits d'aval."
199
+ else:
200
+ msg_status = "🟢 **NOMINAL :** Activité hospitalière sous contrôle."
201
+ msg_action = "✅ Aucune action critique requise à court terme."
202
+
203
+ st.info(f"{msg_status}\n\n{msg_action}")
204
 
205
+ with col_act:
206
+ if st.button("📢 Alerter Equipes"):
207
+ st.toast("Alerte envoyée aux chefs de service.", icon="📨")
208
+ if st.button("📊 Export PDF"):
209
+ st.toast("Rapport généré.", icon="📄")
210
+
211
+ # === ONGLET 2 : SIMULATEUR ===
212
+ with tab_sim:
213
+ st.markdown("### 🧪 Bac à Sable (What-If Analysis)")
214
+
215
  if 'forecaster' not in st.session_state:
216
+ st.warning("⚠️ Le module 'Simulateur' nécessite que l'IA soit activée.")
217
+ if st.button("Activer l'IA maintenant"):
218
+ st.error("Veuillez d'abord visiter l'onglet 'Forecasting' pour entraîner le modèle.")
219
+ else:
220
+ col_params, col_viz = st.columns([1, 3])
221
+
222
+ with col_params:
223
+ st.markdown("#### 1. Paramètres")
224
+ st.caption("Modifiez les variables pour voir l'impact futur.")
225
+
226
+ choc_adm = st.slider("🌊 Choc d'Admissions", -20, +50, 0, format="%+d%%", help="Ex: Grippe, Plan Blanc (+30%)")
227
+ choc_lits = st.slider("🛏️ Ouverture Lits", -20, +20, 0, format="%+d%%", help="Ex: Ouverture d'une aile (+10%)")
228
+
229
+ st.markdown("---")
230
+
231
+ # Calcul impact
232
+ dates, base, simu = dashboard.run_simulation(choc_adm, choc_lits)
233
+
234
+ if simu is not None:
235
+ delta_max = np.max(simu) - np.max(base)
236
+ st.metric("Impact Peak", f"{delta_max:+.1f} pts", delta="Risque" if delta_max > 0 else "Gain", delta_color="inverse")
237
+
238
+ with col_viz:
239
+ st.markdown("#### 2. Projection d'Impact (3 Mois)")
240
+ if simu is not None:
241
+ fig_sim = go.Figure()
242
+
243
+ # Courbe de base
244
+ fig_sim.add_trace(go.Scatter(x=dates, y=base, name="Scénario Actuel", line=dict(color='gray', dash='dot')))
245
+
246
+ # Courbe simulée
247
+ color_sim = '#FF5252' if np.max(simu) > 100 else '#00E676'
248
+ fig_sim.add_trace(go.Scatter(x=dates, y=simu, name="Scénario Simulé", line=dict(color=color_sim, width=3), fill='tonexty'))
249
+
250
+ # Seuil
251
+ fig_sim.add_hline(y=100, line_color="white", annotation_text="Capacité Max")
252
+
253
+ fig_sim.update_layout(template="plotly_dark", height=400, hovermode="x unified", title="Impact sur le Taux d'Occupation")
254
+ st.plotly_chart(fig_sim, use_container_width=True)
255
+
256
+ # Conclusion automatique de simulation
257
+ max_val = np.max(simu)
258
+ if max_val > 100:
259
+ st.error(f"🚨 **ALERTE CRITIQUE :** Ce scénario entraîne une saturation à **{max_val:.1f}%**. Il faut augmenter la capacité d'au moins **{max_val-100:.1f}%**.")
260
+ elif max_val > 85:
261
+ st.warning(f"⚠️ **TENSION :** Ce scénario provoque une tension forte ({max_val:.1f}%). Surveillance requise.")
262
+ else:
263
+ st.success(f"✅ **ROBUSTE :** L'hôpital tient le choc ({max_val:.1f}% max).")