jaimalklyde commited on
Commit
793a193
·
verified ·
1 Parent(s): 73e48b1

Update src/module/Data_Exploration.py

Browse files
Files changed (1) hide show
  1. src/module/Data_Exploration.py +94 -311
src/module/Data_Exploration.py CHANGED
@@ -8,22 +8,18 @@ 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
  from datetime import datetime
14
  import json
15
 
 
 
 
 
16
  class DataExplorer:
17
  """Classe pour l'exploration et l'analyse des données hospitalières"""
18
 
19
  def __init__(self, gspread_client, sheet_name="bdd_mvp_faker"):
20
- """
21
- Initialise l'explorateur de données
22
-
23
- Args:
24
- gspread_client: Client gspread authentifié
25
- sheet_name: Nom du fichier Google Sheets
26
- """
27
  self.client = gspread_client
28
  self.sheet_name = sheet_name
29
  self.dataframes = {}
@@ -35,370 +31,157 @@ class DataExplorer:
35
  spreadsheet = self.client.open(self.sheet_name)
36
  sheet_names = [worksheet.title for worksheet in spreadsheet.worksheets()]
37
 
38
- st.info(f"📊 Onglets détectés : {', '.join(sheet_names)}")
39
 
40
  for sheet_name in sheet_names:
 
41
  worksheet = spreadsheet.worksheet(sheet_name)
42
  data = worksheet.get_all_records()
43
 
44
  if data:
45
- # Standardisation du nom pour le dictionnaire
46
  standardized_name = sheet_name.lower().replace('é', 'e').replace(' ', '_')
47
  self.dataframes[standardized_name] = pd.DataFrame(data)
48
- st.success(f"✅ {sheet_name} chargé ({len(data)} lignes)")
49
- else:
50
- st.warning(f"⚠️ {sheet_name} est vide")
51
 
52
  return True
53
 
54
  except Exception as e:
55
- st.error(f"❌ Erreur lors du chargement : {e}")
56
  return False
57
-
58
  def get_dataframe(self, sheet_key):
59
- """Récupère un DataFrame spécifique par son nom standardisé"""
60
  return self.dataframes.get(sheet_key)
61
-
62
  def show_data_overview(self):
63
- """Affiche un aperçu général des données"""
64
- st.markdown("### 📋 Aperçu Général des Données")
65
-
66
  if not self.dataframes:
67
- st.warning("⚠️ Aucune donnée chargée. Veuillez d'abord charger les données.")
68
  return
69
-
70
- # Tableau récapitulatif
71
  overview_data = []
72
  for name, df in self.dataframes.items():
73
  overview_data.append({
74
- "Onglet": name,
75
- "Lignes": len(df),
76
- "Colonnes": len(df.columns),
77
- "Mémoire (KB)": round(df.memory_usage(deep=True).sum() / 1024, 2),
78
- "Valeurs manquantes": df.isnull().sum().sum()
79
  })
80
-
81
- overview_df = pd.DataFrame(overview_data)
82
- st.dataframe(overview_df, use_container_width=True)
83
-
84
  def show_detailed_inspection(self, sheet_key):
85
- """Affiche une inspection détaillée d'un DataFrame spécifique"""
86
  df = self.get_dataframe(sheet_key)
87
-
88
  if df is None:
89
- st.error(f"❌ Onglet '{sheet_key}' non trouvé")
90
  return
91
 
92
- st.markdown(f"### 🔍 Inspection Détaillée : `{sheet_key}`")
93
-
94
- # Onglets d'inspection
95
- tab1, tab2, tab3, tab4 = st.tabs(["📊 Aperçu", "📈 Statistiques", "🔎 Qualité", "🧹 Colonnes"])
96
-
97
  with tab1:
98
- st.markdown("#### Premières lignes")
99
- st.dataframe(df.head(10), use_container_width=True)
100
-
101
- st.markdown("#### Dernières lignes")
102
- st.dataframe(df.tail(5), use_container_width=True)
103
-
104
  with tab2:
105
- st.markdown("#### Statistiques Descriptives")
106
- st.dataframe(df.describe(include='all').T, use_container_width=True)
107
-
108
- st.markdown("#### Types de Données")
109
- types_df = pd.DataFrame({
110
- 'Colonne': df.columns,
111
- 'Type': df.dtypes.values,
112
- 'Non-Null': df.count().values,
113
- 'Null': df.isnull().sum().values
114
- })
115
- st.dataframe(types_df, use_container_width=True)
116
-
117
- with tab3:
118
- st.markdown("#### Analyse de Qualité")
119
-
120
- # Valeurs manquantes
121
- missing_data = df.isnull().sum()
122
- missing_percent = (missing_data / len(df)) * 100
123
-
124
- quality_df = pd.DataFrame({
125
- 'Colonne': missing_data.index,
126
- 'Valeurs Manquantes': missing_data.values,
127
- 'Pourcentage (%)': missing_percent.values.round(2)
128
- })
129
- quality_df = quality_df[quality_df['Valeurs Manquantes'] > 0].sort_values('Valeurs Manquantes', ascending=False)
130
-
131
- if not quality_df.empty:
132
- st.dataframe(quality_df, use_container_width=True)
133
-
134
- # Graphique
135
- fig = px.bar(quality_df, x='Colonne', y='Pourcentage (%)',
136
- title='Pourcentage de Valeurs Manquantes par Colonne',
137
- color='Pourcentage (%)',
138
- color_continuous_scale='Reds')
139
- st.plotly_chart(fig, use_container_width=True)
140
- else:
141
- st.success("✅ Aucune valeur manquante détectée !")
142
-
143
- # Doublons
144
- duplicates = df.duplicated().sum()
145
- st.metric("Lignes Dupliquées", duplicates)
146
-
147
- with tab4:
148
- st.markdown("#### Liste des Colonnes")
149
- cols_info = []
150
- for col in df.columns:
151
- unique_count = df[col].nunique()
152
- cols_info.append({
153
- 'Colonne': col,
154
- 'Type': str(df[col].dtype),
155
- 'Valeurs Uniques': unique_count,
156
- 'Exemple': str(df[col].iloc[0]) if len(df) > 0 else 'N/A'
157
- })
158
-
159
- st.dataframe(pd.DataFrame(cols_info), use_container_width=True)
160
-
161
  def analyze_patients(self):
162
- """Analyse spécifique des patients"""
163
- df_patients = self.get_dataframe('patients')
164
-
165
- if df_patients is None:
166
- st.warning("⚠️ Onglet 'patients' non disponible")
167
  return
168
-
169
- st.markdown("### 👥 Analyse Démographique des Patients")
170
-
171
- col1, col2, col3 = st.columns(3)
172
-
173
  with col1:
174
- total_patients = len(df_patients)
175
- st.metric("Total Patients", f"{total_patients:,}")
 
 
176
 
177
  with col2:
178
- if 'age' in df_patients.columns:
179
- avg_age = df_patients['age'].mean()
180
  st.metric("Âge Moyen", f"{avg_age:.1f} ans")
181
-
182
- with col3:
183
- if 'sexe' in df_patients.columns:
184
- gender_dist = df_patients['sexe'].value_counts()
185
- st.metric("Répartition H/F", f"{gender_dist.get('M', 0)}/{gender_dist.get('F', 0)}")
186
-
187
- # Pyramide des âges
188
- if 'age' in df_patients.columns:
189
- st.markdown("#### 📊 Pyramide des Âges")
190
-
191
- # Détection des outliers
192
- q1 = df_patients['age'].quantile(0.25)
193
- q3 = df_patients['age'].quantile(0.75)
194
- iqr = q3 - q1
195
- outlier_threshold = q3 + 1.5 * iqr
196
-
197
- outliers = df_patients[df_patients['age'] > outlier_threshold]
198
- if not outliers.empty:
199
- st.warning(f"⚠️ {len(outliers)} outliers détectés (âge > {outlier_threshold:.0f} ans)")
200
-
201
- fig = px.histogram(df_patients, x='age', nbins=30,
202
- title='Distribution des Âges',
203
- labels={'age': 'Âge', 'count': 'Nombre de patients'},
204
- color_discrete_sequence=['#58a6ff'])
205
- st.plotly_chart(fig, use_container_width=True)
206
-
207
- # Répartition par sexe
208
- if 'sexe' in df_patients.columns:
209
- st.markdown("#### ⚥ Répartition par Sexe")
210
-
211
- gender_counts = df_patients['sexe'].value_counts()
212
- fig = px.pie(values=gender_counts.values, names=gender_counts.index,
213
- title='Répartition Hommes/Femmes',
214
- color_discrete_sequence=['#58a6ff', '#ff6b9d'])
215
- st.plotly_chart(fig, use_container_width=True)
216
-
217
  def analyze_data_quality(self):
218
- """Analyse la qualité des données (colonnes dq_)"""
219
- st.markdown("### 🔬 Audit de Qualité des Données")
220
-
221
- dq_results = {}
222
-
223
  for name, df in self.dataframes.items():
224
  if 'dq_is_anomaly' in df.columns:
225
  anomalies = df[df['dq_is_anomaly'] == 1]
226
-
227
- dq_results[name] = {
228
- "total_rows": len(df),
229
- "anomaly_count": len(anomalies),
230
- "anomaly_rate": (len(anomalies) / len(df) * 100) if len(df) > 0 else 0
231
- }
232
-
233
- if 'dq_anomaly_type' in df.columns and not anomalies.empty:
234
- dq_results[name]["types"] = anomalies['dq_anomaly_type'].value_counts().to_dict()
235
-
236
- if not dq_results:
237
- st.info("ℹ️ Aucune colonne de qualité (dq_) détectée dans les données")
238
- return
239
-
240
- # Affichage des résultats
241
- for sheet_name, results in dq_results.items():
242
- with st.expander(f"📋 {sheet_name.upper()}", expanded=True):
243
- col1, col2, col3 = st.columns(3)
244
-
245
- with col1:
246
- st.metric("Total Lignes", f"{results['total_rows']:,}")
247
- with col2:
248
- st.metric("Anomalies", results['anomaly_count'])
249
- with col3:
250
- st.metric("Taux d'Anomalies", f"{results['anomaly_rate']:.2f}%")
251
-
252
- if "types" in results and results["types"]:
253
- st.markdown("**Types d'Anomalies :**")
254
- types_df = pd.DataFrame(list(results["types"].items()),
255
- columns=['Type', 'Count'])
256
- st.dataframe(types_df, use_container_width=True)
257
-
258
  def calculate_kpis(self):
259
- """Calcule les KPIs par service"""
260
- st.markdown("### 📊 Indicateurs de Performance (KPIs)")
261
-
262
- # Exemple de KPIs - à adapter selon vos données réelles
263
- kpis = {}
264
-
265
- # KPI 1: Taux d'occupation
266
- df_ressources = self.get_dataframe('ressources_jour')
267
- if df_ressources is not None and 'lits_total' in df_ressources.columns:
268
- total_lits = df_ressources['lits_total'].sum()
269
- lits_occupes = df_ressources['lits_occupes'].sum() if 'lits_occupes' in df_ressources.columns else 0
270
- kpis['taux_occupation'] = (lits_occupes / total_lits * 100) if total_lits > 0 else 0
271
-
272
- # KPI 2: Durée moyenne de séjour
273
- df_sejours = self.get_dataframe('sejours')
274
- if df_sejours is not None and 'duree_sejour' in df_sejours.columns:
275
- kpis['duree_moyenne_sejour'] = df_sejours['duree_sejour'].mean()
276
-
277
- # KPI 3: Nombre de patients
278
- df_patients = self.get_dataframe('patients')
279
- if df_patients is not None:
280
- kpis['total_patients'] = len(df_patients)
281
-
282
- # Affichage
283
- if kpis:
284
- cols = st.columns(len(kpis))
285
- for idx, (key, value) in enumerate(kpis.items()):
286
- with cols[idx]:
287
- st.metric(key.replace('_', ' ').title(), f"{value:.2f}")
288
-
289
- # Export JSON
290
- if st.button("💾 Exporter les KPIs (JSON)"):
291
- kpis_json = json.dumps(kpis, indent=2)
292
- st.download_button(
293
- label="Télécharger KPIs.json",
294
- data=kpis_json,
295
- file_name=f"kpis_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
296
- mime="application/json"
297
- )
298
  else:
299
- st.info("ℹ️ Impossible de calculer les KPIs avec les données actuelles")
300
-
301
  def show_stock_alerts(self):
302
- """Affiche les alertes de stock"""
303
  df_stock = self.get_dataframe('materiel')
304
-
305
- if df_stock is None:
306
- st.warning("⚠️ Onglet 'materiel' non disponible")
307
- return
308
-
309
- st.markdown("### 🚨 Alertes Stock & Matériel")
310
-
311
- if 'tension_stock' in df_stock.columns:
312
  alerts = df_stock[df_stock['tension_stock'] > 0.8]
313
-
314
  if not alerts.empty:
315
- st.error(f"⚠️ {len(alerts)} alertes de tension de stock détectées !")
316
-
317
- # Colonnes à afficher
318
- display_cols = [col for col in ['date', 'service_code', 'materiel_code', 'tension_stock']
319
- if col in alerts.columns]
320
- st.dataframe(alerts[display_cols], use_container_width=True)
321
  else:
322
- st.success("✅ Aucune alerte de stock")
323
- else:
324
- st.info("ℹ️ Colonne 'tension_stock' non trouvée dans les données")
325
 
326
 
327
- # ... (Le reste de la classe DataExplorer reste identique) ...
328
-
 
 
329
  def show_terminal(gspread_client, sheet_name="bdd_mvp_faker"):
330
- """
331
- Fonction d'affichage optimisée "Dashboard" pour Streamlit
332
- """
333
- # 1. Initialisation persistante
334
  if 'data_explorer' not in st.session_state:
335
  st.session_state.data_explorer = DataExplorer(gspread_client, sheet_name)
336
 
337
  explorer = st.session_state.data_explorer
338
 
339
- # 2. Auto-chargement des données (Si vide, on charge direct)
 
 
 
340
  if not explorer.dataframes:
341
- with st.spinner("🔄 Initialisation du Dashboard Data... Récupération GSheets..."):
342
- success = explorer.load_all_sheets()
343
- if not success:
344
- st.error("Impossible de récupérer les données.")
345
- return
346
-
347
- # 3. Interface DASHBOARD (Vue immédiate via Tabs)
348
- if explorer.dataframes:
349
- # Titre avec bouton de rafraîchissement discret à droite
350
- col_titre, col_refresh = st.columns([6, 1])
351
- with col_titre:
352
- st.markdown("## 🔬 MONITORING & EXPLORATION")
353
- with col_refresh:
354
- if st.button("🔄 Actualiser"):
355
- explorer.dataframes = {} # Force le reload
356
  st.rerun()
357
 
358
- # Création des onglets principaux
359
- tab_overview, tab_patients, tab_quality, tab_business = st.tabs([
360
- "📊 Vue Globale",
361
- "👥 Analyse Patients",
362
- "🛡️ Qualité & Santé Données",
363
- "💼 Business & KPIs"
364
- ])
365
 
366
- # --- ONGLET 1 : VUE GLOBALE ---
367
- with tab_overview:
368
- st.markdown("### 🩻 Radiographie des Données")
369
  explorer.show_data_overview()
 
370
 
371
- st.divider()
372
- st.caption("Sélectionnez un onglet ci-dessus pour entrer dans le détail.")
373
-
374
- # --- ONGLET 2 : PATIENTS ---
375
- with tab_patients:
376
  explorer.analyze_patients()
377
-
378
- # --- ONGLET 3 : QUALITÉ (Regroupement Qualité + Stock) ---
379
- with tab_quality:
380
- col_q1, col_q2 = st.columns(2)
381
 
382
- with col_q1:
383
- st.markdown("#### 📉 Anomalies Détectées")
 
384
  explorer.analyze_data_quality()
385
-
386
- with col_q2:
387
- st.markdown("#### 📦 Tensions Logistiques")
388
  explorer.show_stock_alerts()
389
-
390
- # --- ONGLET 4 : KPIs BUSINESS ---
391
- with tab_business:
392
- explorer.calculate_kpis()
393
-
394
- # Ajout d'une zone d'inspection manuelle ici si besoin
395
- st.divider()
396
- with st.expander("🔍 Inspection brute d'une table spécifique"):
397
- sheet_choice = st.selectbox("Choisir la table", list(explorer.dataframes.keys()))
398
- explorer.show_detailed_inspection(sheet_choice)
399
-
400
- else:
401
- # Fallback si le chargement a échoué mais sans erreur critique
402
- st.warning("Aucune donnée n'a pu être chargée pour le moment.")
403
- if st.button("Réessayer le chargement"):
404
- st.rerun()
 
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
 
15
+ # --- DEBUG MARKER ---
16
+ # Si cette ligne s'affiche dans les logs, le module est bien lu
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 = {}
 
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('materiel')
129
+ if df_stock is not None and 'tension_stock' in df_stock.columns:
 
 
 
 
 
 
 
130
  alerts = df_stock[df_stock['tension_stock'] > 0.8]
 
131
  if not alerts.empty:
132
+ st.error(f"🚨 {len(alerts)} Matériels en rupture imminente")
133
+ st.dataframe(alerts, use_container_width=True)
 
 
 
 
134
  else:
135
+ st.success("Stock sous contrôle.")
 
 
136
 
137
 
138
+ # ==============================================================================
139
+ # ⚠️ C'EST ICI QUE SE JOUE VOTRE PROBLÈME
140
+ # La fonction show_terminal DOIT être collée à gauche (pas d'indentation)
141
+ # ==============================================================================
142
  def show_terminal(gspread_client, sheet_name="bdd_mvp_faker"):
143
+ """Fonction principale du Dashboard"""
144
+
145
+ # Initialisation unique de l'objet DataExplorer
 
146
  if 'data_explorer' not in st.session_state:
147
  st.session_state.data_explorer = DataExplorer(gspread_client, sheet_name)
148
 
149
  explorer = st.session_state.data_explorer
150
 
151
+ # Layout Dashboard
152
+ st.markdown("## 🔬 DATA EXPLORER & MONITORING")
153
+
154
+ # Bouton de chargement intelligent (n'apparait que si vide)
155
  if not explorer.dataframes:
156
+ if st.button("📥 Initialiser la connexion aux données", type="primary"):
157
+ with st.spinner("Téléchargement des données Google Sheets..."):
158
+ explorer.load_all_sheets()
159
+ st.rerun()
160
+ else:
161
+ # Barre d'outils
162
+ col_actions, _ = st.columns([1, 4])
163
+ with col_actions:
164
+ if st.button("🔄 Rafraîchir"):
165
+ explorer.dataframes = {}
 
 
 
 
 
166
  st.rerun()
167
 
168
+ # Onglets de navigation
169
+ tabs = st.tabs(["Vue Globale", "Patients", "Qualité & Stock", "Inspection"])
 
 
 
 
 
170
 
171
+ with tabs[0]:
 
 
172
  explorer.show_data_overview()
173
+ explorer.calculate_kpis()
174
 
175
+ with tabs[1]:
 
 
 
 
176
  explorer.analyze_patients()
 
 
 
 
177
 
178
+ with tabs[2]:
179
+ col1, col2 = st.columns(2)
180
+ with col1:
181
  explorer.analyze_data_quality()
182
+ with col2:
 
 
183
  explorer.show_stock_alerts()
184
+
185
+ with tabs[3]:
186
+ sheet = st.selectbox("Choisir une table", list(explorer.dataframes.keys()))
187
+ explorer.show_detailed_inspection(sheet)