jaimalklyde commited on
Commit
7f0ac6f
·
verified ·
1 Parent(s): 1c2c1a3

Update src/module/Data_Exploration.py

Browse files
Files changed (1) hide show
  1. src/module/Data_Exploration.py +254 -133
src/module/Data_Exploration.py CHANGED
@@ -1,200 +1,321 @@
1
  """
2
- MODULE DATA EXPLORATION - VORTEX SYSTEM
3
- ========================================
4
- Responsabilité : Analyse exploratoire des données (EDA) avec Google Sheets
5
  """
6
  import pandas as pd
7
  import numpy as np
8
  import streamlit as st
9
  import plotly.express as px
10
  import plotly.graph_objects as go
 
11
  import gspread
12
- from datetime import datetime
13
- import json
14
- import time
15
 
16
  # --- DEBUG MARKER ---
17
- print("✅ Module Data_Exploration chargé avec succès")
18
 
19
  class DataExplorer:
20
- """Classe pour l'exploration et l'analyse des données hospitalières"""
21
 
22
  def __init__(self, gspread_client, sheet_name="bdd_mvp_faker"):
23
  self.client = gspread_client
24
  self.sheet_name = sheet_name
25
  self.dataframes = {}
26
- self.kpis = {}
27
 
28
  def load_all_sheets(self):
29
- """Charge tous les onglets du fichier Google Sheets"""
30
  try:
31
  spreadsheet = self.client.open(self.sheet_name)
32
  sheet_names = [worksheet.title for worksheet in spreadsheet.worksheets()]
33
 
34
- st.toast(f"Connexion établie : {len(sheet_names)} onglets trouvés", icon="📡")
 
35
 
36
- for sheet_name in sheet_names:
37
- # On ignore les feuilles de calcul temporaires ou masquées si besoin
38
  worksheet = spreadsheet.worksheet(sheet_name)
39
  data = worksheet.get_all_records()
40
 
41
  if data:
42
- standardized_name = sheet_name.lower().replace('é', 'e').replace(' ', '_')
43
- self.dataframes[standardized_name] = pd.DataFrame(data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
 
45
  return True
46
 
47
  except Exception as e:
48
- st.error(f"❌ Erreur GSheets : {e}")
49
  return False
50
 
51
- def get_dataframe(self, sheet_key):
52
- return self.dataframes.get(sheet_key)
 
 
 
 
 
53
 
54
- def show_data_overview(self):
55
- st.markdown("### 📋 Aperçu Général")
56
- if not self.dataframes:
57
- st.info("Aucune donnée chargée.")
58
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- overview_data = []
61
- for name, df in self.dataframes.items():
62
- overview_data.append({
63
- "Table": name,
64
- "Lignes": df.shape[0],
65
- "Colonnes": df.shape[1],
66
- "Manquants": df.isnull().sum().sum()
67
- })
68
- st.dataframe(pd.DataFrame(overview_data), use_container_width=True)
69
-
70
- def show_detailed_inspection(self, sheet_key):
71
- df = self.get_dataframe(sheet_key)
72
- if df is None:
73
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- st.markdown(f"#### Inspection : `{sheet_key}`")
76
- tab1, tab2 = st.tabs(["Données", "Statistiques"])
77
- with tab1:
78
- st.dataframe(df.head(50), use_container_width=True)
79
- with tab2:
80
- st.dataframe(df.describe(include='all'), use_container_width=True)
81
-
82
- def analyze_patients(self):
83
- df = self.get_dataframe('patients')
84
- if df is None:
85
- st.warning("Table 'patients' manquante")
86
  return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
- col1, col2 = st.columns(2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  with col1:
90
- st.metric("Nombre de Patients", len(df))
91
- if 'sexe' in df.columns:
92
- fig = px.pie(df, names='sexe', title="Répartition Sexe", hole=0.4)
93
- st.plotly_chart(fig, use_container_width=True)
 
 
 
 
 
 
 
94
 
95
  with col2:
96
- if 'age' in df.columns:
97
- avg_age = df['age'].mean()
98
- st.metric("Âge Moyen", f"{avg_age:.1f} ans")
99
- fig = px.histogram(df, x='age', nbins=20, title="Distribution des Âges")
100
- st.plotly_chart(fig, use_container_width=True)
101
-
102
- def analyze_data_quality(self):
103
- st.markdown("#### 🛡️ Audit de Qualité")
104
- total_issues = 0
105
- for name, df in self.dataframes.items():
106
- if 'dq_is_anomaly' in df.columns:
107
- anomalies = df[df['dq_is_anomaly'] == 1]
108
- count = len(anomalies)
109
- if count > 0:
110
- total_issues += count
111
- with st.expander(f"⚠️ {name} : {count} anomalies détectées"):
112
- st.dataframe(anomalies, use_container_width=True)
113
-
114
- if total_issues == 0:
115
- st.success("Aucune anomalie marquée (dq_is_anomaly) détectée.")
116
-
117
- def calculate_kpis(self):
118
- st.markdown("#### 🚀 KPIs en temps réel")
119
- # Exemple simple
120
- df_res = self.get_dataframe('ressources_jour')
121
- if df_res is not None and 'lits_occupes' in df_res.columns:
122
- taux = df_res['lits_occupes'].sum() / df_res['lits_total'].sum() * 100
123
- st.metric("Taux d'Occupation Global", f"{taux:.1f}%")
 
 
 
 
 
 
 
 
 
 
 
124
  else:
125
- st.info("Données ressources insuffisantes pour le calcul des KPIs.")
126
-
127
- def show_stock_alerts(self):
128
- df_stock = self.get_dataframe('stock_materiel_jour') # Correction probable du nom ici selon tes fichiers précédents
129
- # Si ça ne marche pas, remets 'materiel' si c'est le nom de ta table
130
- if df_stock is None:
131
- df_stock = self.get_dataframe('materiel')
132
-
133
- if df_stock is not None and 'tension_stock' in df_stock.columns:
134
- alerts = df_stock[df_stock['tension_stock'] > 0.8]
135
- if not alerts.empty:
136
- st.error(f"🚨 {len(alerts)} Matériels en rupture imminente")
137
- st.dataframe(alerts, use_container_width=True)
138
- else:
139
- st.success("Stock sous contrôle.")
140
 
141
 
142
  # ==============================================================================
143
- # FONCTION PRINCIPALE AVEC CHARGEMENT AUTO
144
  # ==============================================================================
145
  def show_terminal(gspread_client, sheet_name="bdd_mvp_faker"):
146
- """Fonction principale du Dashboard"""
147
 
148
- # Initialisation unique de l'objet DataExplorer
149
  if 'data_explorer' not in st.session_state:
150
  st.session_state.data_explorer = DataExplorer(gspread_client, sheet_name)
151
 
152
  explorer = st.session_state.data_explorer
153
 
154
- st.markdown("## 🔬 DATA EXPLORER & MONITORING")
 
155
 
156
- # --- LOGIQUE DE CHARGEMENT AUTOMATIQUE ---
157
- # Si les données ne sont pas chargées, on lance le chargement immédiatement
158
  if not explorer.dataframes:
159
- with st.spinner("🔄 Connexion automatique à Google Sheets en cours..."):
 
160
  success = explorer.load_all_sheets()
161
 
162
- if success:
163
- st.success("✅ Données chargées avec succès !")
164
- time.sleep(0.5) # Petit délai pour voir le message
165
- st.rerun() # On recharge la page pour afficher le dashboard
166
- else:
167
- st.error("🛑 Le chargement a échoué. Vérifiez votre connexion ou la clé API.")
168
- return # On arrête l'exécution ici si ça plante
169
 
170
- # --- DASHBOARD (S'affiche seulement si les données sont là) ---
171
  else:
172
- # Barre d'outils
173
- col_actions, _ = st.columns([1, 4])
174
- with col_actions:
175
- if st.button("🔄 Rafraîchir Données"):
176
- explorer.dataframes = {} # On vide le cache
177
- st.rerun() # On relance, ce qui déclenchera le chargement auto ci-dessus
178
-
179
- # Onglets de navigation
180
- tabs = st.tabs(["Vue Globale", "Patients", "Qualité & Stock", "Inspection"])
 
 
 
 
 
 
 
181
 
182
  with tabs[0]:
183
- explorer.show_data_overview()
184
- explorer.calculate_kpis()
185
 
186
  with tabs[1]:
187
- explorer.analyze_patients()
188
 
189
  with tabs[2]:
190
- col1, col2 = st.columns(2)
191
- with col1:
192
- explorer.analyze_data_quality()
193
- with col2:
194
- explorer.show_stock_alerts()
195
-
196
  with tabs[3]:
197
- # Sélecteur de table sécurisé
198
- if explorer.dataframes:
199
- sheet = st.selectbox("Choisir une table", list(explorer.dataframes.keys()))
200
- explorer.show_detailed_inspection(sheet)
 
 
 
 
 
 
 
1
  """
2
+ MODULE DATA EXPLORATION - SENIOR DATA SCIENTIST EDITION
3
+ =======================================================
4
+ Responsabilité : EDA Avancée, Audit DQ, Analyse Temporelle & Stress-Tests
5
  """
6
  import pandas as pd
7
  import numpy as np
8
  import streamlit as st
9
  import plotly.express as px
10
  import plotly.graph_objects as go
11
+ from plotly.subplots import make_subplots
12
  import gspread
 
 
 
13
 
14
  # --- DEBUG MARKER ---
15
+ print("✅ Module Data_Exploration (Senior Edition) chargé")
16
 
17
  class DataExplorer:
18
+ """Moteur d'analyse exploratoire avancé pour données hospitalières"""
19
 
20
  def __init__(self, gspread_client, sheet_name="bdd_mvp_faker"):
21
  self.client = gspread_client
22
  self.sheet_name = sheet_name
23
  self.dataframes = {}
 
24
 
25
  def load_all_sheets(self):
26
+ """Charge et pré-traite les données (typage dates, nettoyage basique)"""
27
  try:
28
  spreadsheet = self.client.open(self.sheet_name)
29
  sheet_names = [worksheet.title for worksheet in spreadsheet.worksheets()]
30
 
31
+ progress_text = "Chargement des tables..."
32
+ my_bar = st.progress(0, text=progress_text)
33
 
34
+ for i, sheet_name in enumerate(sheet_names):
 
35
  worksheet = spreadsheet.worksheet(sheet_name)
36
  data = worksheet.get_all_records()
37
 
38
  if data:
39
+ # Standardisation du nom (ex: "Ressources Jour" -> "ressources_jour")
40
+ std_name = sheet_name.lower().replace('é', 'e').replace(' ', '_').replace('-', '_').split('__')[0]
41
+ df = pd.DataFrame(data)
42
+
43
+ # --- PRÉ-TRAITEMENT AUTOMATIQUE ---
44
+ # 1. Conversion des dates
45
+ for col in df.columns:
46
+ if 'date' in col.lower():
47
+ df[col] = pd.to_datetime(df[col], errors='coerce')
48
+
49
+ # 2. Conversion numérique forcée pour les métriques clés
50
+ cols_numeric = ['age', 'lits_occupes', 'lits_total', 'tension_stock', 'nb_admissions', 'duree_sejour_jours']
51
+ for col in cols_numeric:
52
+ if col in df.columns:
53
+ df[col] = pd.to_numeric(df[col], errors='coerce')
54
+
55
+ self.dataframes[std_name] = df
56
+
57
+ my_bar.progress((i + 1) / len(sheet_names), text=f"Chargement : {sheet_name}")
58
 
59
+ my_bar.empty()
60
  return True
61
 
62
  except Exception as e:
63
+ st.error(f"❌ Erreur critique GSheets : {e}")
64
  return False
65
 
66
+ def get_df(self, name):
67
+ """Helper sécurisé pour récupérer un DF"""
68
+ # Gestion des variations de noms de fichiers (avec ou sans extension)
69
+ for key in self.dataframes.keys():
70
+ if name in key:
71
+ return self.dataframes[key]
72
+ return None
73
 
74
+ # ==========================================================================
75
+ # 1. AUDIT QUALITÉ (DQ) & INTÉGRITÉ
76
+ # ==========================================================================
77
+ def tab_audit_dq(self):
78
+ st.markdown("### 🛡️ Audit de Qualité & Intégrité des Données")
79
+ st.caption("Détection des anomalies structurelles et métier basées sur les hypothèses du projet.")
80
+
81
+ col1, col2 = st.columns(2)
82
+
83
+ # --- A. Analyse des Patients (Age & Doublons) ---
84
+ with col1:
85
+ st.markdown("#### 👥 Intégrité Patients")
86
+ df_pat = self.get_df('patients')
87
+ if df_pat is not None:
88
+ # 1. Doublons
89
+ doublons = df_pat.duplicated(subset=['nom', 'prenom', 'date_naissance']).sum()
90
+
91
+ # 2. Âges aberrants (Hypothèse: -5 ans ou > 120 ans)
92
+ age_aberrant = df_pat[(df_pat['age'] < 0) | (df_pat['age'] > 120)].shape[0]
93
+
94
+ # 3. Valeurs manquantes critiques
95
+ missing_ville = df_pat['ville'].isnull().sum()
96
+
97
+ # Viz KPI
98
+ kpi_col1, kpi_col2, kpi_col3 = st.columns(3)
99
+ kpi_col1.metric("Doublons stricts", doublons, delta_color="inverse")
100
+ kpi_col2.metric("Âges Aberrants", age_aberrant, delta_color="inverse")
101
+ kpi_col3.metric("Ville Manquante", missing_ville, delta_color="inverse")
102
+
103
+ # Graphique Distribution Age
104
+ fig_age = px.histogram(df_pat, x='age', nbins=50, title="Distribution des Âges (Détection Outliers)", color_discrete_sequence=['#58a6ff'])
105
+ st.plotly_chart(fig_age, use_container_width=True)
106
+ else:
107
+ st.warning("Table 'patients' introuvable.")
108
 
109
+ # --- B. Cohérence des Séjours ---
110
+ with col2:
111
+ st.markdown("#### 🏥 Cohérence Séjours")
112
+ df_sej = self.get_df('sejours')
113
+ if df_sej is not None:
114
+ # 1. Date Sortie < Date Entrée
115
+ incoherent_dates = df_sej[df_sej['date_sortie'] < df_sej['date_entree']].shape[0]
116
+
117
+ # 2. Gravité invalide (Hypothèse: 1-5, si >5 anomalie)
118
+ # On suppose que la colonne est 'gravite_1_5' ou similaire
119
+ gravite_col = next((c for c in df_sej.columns if 'gravit' in c), None)
120
+ gravite_issues = 0
121
+ if gravite_col:
122
+ gravite_issues = df_sej[(df_sej[gravite_col] < 1) | (df_sej[gravite_col] > 5)].shape[0]
123
+
124
+ st.metric("Dates Incohérentes (Sortie < Entrée)", incoherent_dates, delta_color="inverse")
125
+ st.metric("Score Gravité Hors Norme", gravite_issues, delta_color="inverse")
126
+
127
+ if incoherent_dates > 0:
128
+ st.error(f"🚨 Alerte : {incoherent_dates} séjours ont une date de fin antérieure au début !")
129
+ else:
130
+ st.warning("Table 'sejours' introuvable.")
131
+
132
+ # ==========================================================================
133
+ # 2. ANALYSE TEMPORELLE & FLUX (STL Like)
134
+ # ==========================================================================
135
+ def tab_temporal_flux(self):
136
+ st.markdown("### ⏳ Analyse Temporelle des Admissions")
137
+ st.caption("Décomposition de la tendance et de la saisonnalité pour identifier les cycles.")
138
 
139
+ df_adm = self.get_df('admissions')
140
+ if df_adm is None:
141
+ st.warning("Table 'admissions_jour' manquante.")
 
 
 
 
 
 
 
 
142
  return
143
+
144
+ # Agrégation par jour (tous services confondus)
145
+ df_daily = df_adm.groupby('date')['nb_admissions'].sum().reset_index().sort_values('date')
146
+
147
+ # Calcul Moyenne Mobile (Trend)
148
+ df_daily['Trend (7j)'] = df_daily['nb_admissions'].rolling(window=7).mean()
149
+ df_daily['Trend (30j)'] = df_daily['nb_admissions'].rolling(window=30).mean()
150
+
151
+ # Viz 1: Flux Brut vs Tendance
152
+ fig_flux = go.Figure()
153
+ fig_flux.add_trace(go.Scatter(x=df_daily['date'], y=df_daily['nb_admissions'], mode='lines', name='Admissions Brutes', line=dict(color='rgba(88, 166, 255, 0.3)')))
154
+ fig_flux.add_trace(go.Scatter(x=df_daily['date'], y=df_daily['Trend (7j)'], mode='lines', name='Tendance Hebdo', line=dict(color='#00E676', width=2)))
155
+ fig_flux.add_trace(go.Scatter(x=df_daily['date'], y=df_daily['Trend (30j)'], mode='lines', name='Tendance Mensuelle', line=dict(color='#ff6b9d', width=2)))
156
+
157
+ fig_flux.update_layout(title="Flux d'Admissions & Lissage (Trend)", xaxis_title="Date", yaxis_title="Nb Admissions")
158
+ st.plotly_chart(fig_flux, use_container_width=True)
159
+
160
+ # Viz 2: Saisonnalité (Heatmap Jours/Mois)
161
+ col1, col2 = st.columns([1, 2])
162
+ with col1:
163
+ st.markdown("**Saisonnalité Hebdomadaire**")
164
+ df_daily['day_name'] = df_daily['date'].dt.day_name()
165
+ day_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
166
+ df_week = df_daily.groupby('day_name')['nb_admissions'].mean().reindex(day_order)
167
 
168
+ fig_week = px.bar(df_week, title="Moyenne par Jour", color=df_week.values, color_continuous_scale="Blues")
169
+ st.plotly_chart(fig_week, use_container_width=True)
170
+
171
+ # ==========================================================================
172
+ # 3. STRESS-TEST SCÉNARIOS & DMS
173
+ # ==========================================================================
174
+ def tab_stress_scenarios(self):
175
+ st.markdown("### 🌪️ Impact des Scénarios de Crise")
176
+ st.caption("Corrélation entre les types de scénarios (Covid, Grippe) et la tension hospitalière.")
177
+
178
+ df_res = self.get_df('ressources')
179
+ if df_res is None or 'scenario' not in df_res.columns:
180
+ st.warning("Données ressources ou scénarios manquantes.")
181
+ return
182
+
183
+ # 1. Boxplot Taux d'Occupation par Scénario
184
+ st.markdown("#### Saturation des Lits par Scénario")
185
+ # On filtre les scénarios 'Normal' pour voir le contraste
186
+ top_scenarios = df_res['scenario'].value_counts().head(5).index.tolist()
187
+ df_filtered = df_res[df_res['scenario'].isin(top_scenarios)]
188
+
189
+ fig_box = px.box(df_filtered, x='scenario', y='taux_occupation', color='scenario',
190
+ title="Distribution du Taux d'Occupation par Type de Crise",
191
+ points="outliers")
192
+ st.plotly_chart(fig_box, use_container_width=True)
193
+
194
+ # 2. Corrélation Tension Personnel vs Occupation
195
+ st.markdown("#### Corrélation : Occupation Lits vs Tension Personnel")
196
+ col1, col2 = st.columns([3, 1])
197
  with col1:
198
+ fig_scatter = px.scatter(df_res, x='taux_occupation', y='tension_personnel',
199
+ color='service_code', size='lits_total',
200
+ hover_data=['date', 'scenario'],
201
+ title="Zone de Danger : Tension > 80%",
202
+ trendline="ols") # Nécessite statsmodels, sinon retirer trendline
203
+
204
+ # Ajout zones de danger
205
+ fig_scatter.add_hrect(y0=0.8, y1=1.5, line_width=0, fillcolor="red", opacity=0.1, annotation_text="Danger RH")
206
+ fig_scatter.add_vrect(x0=0.9, x1=1.5, line_width=0, fillcolor="orange", opacity=0.1, annotation_text="Saturation Lits")
207
+
208
+ st.plotly_chart(fig_scatter, use_container_width=True)
209
 
210
  with col2:
211
+ st.info("""
212
+ **Interprétation :**
213
+ - **Cadran Haut-Droite** : Situation critique (Lits pleins + Manque personnel).
214
+ - **Points isolés** : Vérifier s'il s'agit d'événements exceptionnels ou d'erreurs de saisie.
215
+ """)
216
+
217
+ # ==========================================================================
218
+ # 4. LOGISTIQUE & LAG (STOCKS)
219
+ # ==========================================================================
220
+ def tab_logistics(self):
221
+ st.markdown("### 📦 Logistique & Ruptures de Stock")
222
+ st.caption("Identification des goulots d'étranglement matériels.")
223
+
224
+ df_stock = self.get_df('stock')
225
+ if df_stock is None:
226
+ st.warning("Table 'stock_materiel_jour' manquante.")
227
+ return
228
+
229
+ # Focus sur les matériels critiques
230
+ critical_items = ['CONSO_O2', 'EQP_RESP', 'CONSO_EPI']
231
+ df_crit = df_stock[df_stock['materiel_code'].isin(critical_items)]
232
+
233
+ # Viz : Tension Stock au cours du temps
234
+ fig_stock = px.line(df_crit, x='date', y='tension_stock', color='materiel_code',
235
+ title="Évolution de la Tension des Stocks Critiques",
236
+ facet_row='service_code', height=800)
237
+
238
+ # Seuil d'alerte visuel
239
+ fig_stock.add_hline(y=0.8, line_dash="dash", line_color="red", annotation_text="Seuil Critique (80%)")
240
+
241
+ st.plotly_chart(fig_stock, use_container_width=True)
242
+
243
+ # Top Alertes Actuelles
244
+ latest_date = df_stock['date'].max()
245
+ ruptures = df_stock[(df_stock['date'] == latest_date) & (df_stock['tension_stock'] > 0.9)]
246
+
247
+ if not ruptures.empty:
248
+ st.error(f"🚨 ALERTES CRITIQUES AU {latest_date.date()} :")
249
+ st.dataframe(ruptures[['service_code', 'materiel_code', 'stock_disponible', 'tension_stock']], use_container_width=True)
250
  else:
251
+ st.success(f"✅ Aucun stock en tension critique au {latest_date.date()}.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
 
254
  # ==============================================================================
255
+ # FONCTION PRINCIPALE (SHOW TERMINAL) - VERSION AUTO-LOAD
256
  # ==============================================================================
257
  def show_terminal(gspread_client, sheet_name="bdd_mvp_faker"):
258
+ """Orchestrateur de l'interface Data Explorer"""
259
 
260
+ # Singleton Pattern pour éviter de recharger à chaque interaction
261
  if 'data_explorer' not in st.session_state:
262
  st.session_state.data_explorer = DataExplorer(gspread_client, sheet_name)
263
 
264
  explorer = st.session_state.data_explorer
265
 
266
+ # 1. HEADER
267
+ st.markdown("## 🔬 DATA EXPLORER : CENTRE D'ANALYSE")
268
 
269
+ # 2. CHARGEMENT AUTOMATIQUE (Plus de bouton)
 
270
  if not explorer.dataframes:
271
+ # On lance le chargement directement
272
+ with st.spinner("🔄 Initialisation et chargement des données..."):
273
  success = explorer.load_all_sheets()
274
 
275
+ if success:
276
+ st.toast("Données chargées avec succès !", icon="✅")
277
+ st.rerun() # On rafraîchit la page pour afficher le dashboard
278
+ else:
279
+ st.error("❌ Échec du chargement automatique. Vérifiez la connexion GSheets.")
280
+ # On arrête l'exécution ici pour ne pas faire planter la suite
281
+ return
282
 
283
+ # 3. DASHBOARD ANALYTIQUE (S'affiche une fois les données chargées)
284
  else:
285
+ # Barre d'actions
286
+ col_refresh, col_info = st.columns([1, 5])
287
+ with col_refresh:
288
+ # Le bouton reload permet de forcer un nouveau téléchargement si besoin
289
+ if st.button("🔄 Reload"):
290
+ explorer.dataframes = {}
291
+ st.rerun()
292
+
293
+ # NAVIGATION PAR ONGLETS MÉTIER
294
+ tabs = st.tabs([
295
+ "🛡️ 1. Audit Qualité (DQ)",
296
+ "⏳ 2. Analyse Temporelle",
297
+ "🌪️ 3. Stress-Tests Scénarios",
298
+ "📦 4. Logistique & Stocks",
299
+ "📋 Inspection Brute"
300
+ ])
301
 
302
  with tabs[0]:
303
+ explorer.tab_audit_dq()
 
304
 
305
  with tabs[1]:
306
+ explorer.tab_temporal_flux()
307
 
308
  with tabs[2]:
309
+ explorer.tab_stress_scenarios()
310
+
 
 
 
 
311
  with tabs[3]:
312
+ explorer.tab_logistics()
313
+
314
+ with tabs[4]:
315
+ # Inspection brute simple
316
+ st.markdown("#### 🔎 Explorateur de Tables Brutes")
317
+ table_choice = st.selectbox("Choisir une table", list(explorer.dataframes.keys()))
318
+ if table_choice:
319
+ df_view = explorer.dataframes[table_choice]
320
+ st.write(f"Dimensions : {df_view.shape}")
321
+ st.dataframe(df_view, use_container_width=True)