jaimalklyde commited on
Commit
c586e1e
·
verified ·
1 Parent(s): 0de2782

Update src/module/Indicateurs_DQ.py

Browse files
Files changed (1) hide show
  1. src/module/Indicateurs_DQ.py +356 -53
src/module/Indicateurs_DQ.py CHANGED
@@ -13,7 +13,7 @@ from Function.Validation_Contract import ETL_Engine
13
  def show_dq_dashboard(client, sheet_name):
14
  st.markdown("## 🛡️ CENTRE DE CONTRÔLE DATA QUALITY")
15
 
16
- # On charge via l'ETL mais on va surtout analyser les NaN et les outliers
17
  etl = ETL_Engine(client, sheet_name)
18
 
19
  # On vérifie si les données sont déjà en session, sinon on charge
@@ -21,8 +21,6 @@ def show_dq_dashboard(client, sheet_name):
21
  st.info("Chargement des données pour Audit...")
22
  success, data = etl.run_etl()
23
  if success:
24
- # Astuce: on stocke quand même dans le session_state global pour partager
25
- # Mais ici on s'en sert pour la DQ
26
  pass
27
  else:
28
  st.error(f"Erreur chargement : {data}")
@@ -30,67 +28,372 @@ def show_dq_dashboard(client, sheet_name):
30
  else:
31
  data = st.session_state.data_explorer.dataframes
32
 
 
 
 
 
 
 
33
  # --- KPI GLOBAL ---
34
  st.markdown("### 📊 Synthèse de Santé des Données")
35
  col1, col2, col3, col4 = st.columns(4)
36
 
37
- df_pat = data.get('patients')
38
- df_sej = data.get('sejours')
39
- df_res = data.get('ressources')
40
-
41
- # Calcul des indicateurs inspirés du notebook
42
- missing_birth = df_pat['date_naissance'].isnull().sum() if df_pat is not None else 0
43
- inverted_dates = df_sej[df_sej['date_sortie'] < df_sej['date_entree']].shape[0] if df_sej is not None else 0
44
- capa_overflow = df_res[df_res['lits_occupes'] > df_res['lits_total']].shape[0] if df_res is not None else 0
45
- duplicates = df_pat.duplicated(subset=['nom', 'prenom']).sum() if df_pat is not None else 0
46
-
47
- col1.metric("Doublons Patients", duplicates, delta=None, delta_color="inverse")
48
- col2.metric("Dates Inversées (Séjours)", inverted_dates, delta="Bloquant", delta_color="inverse")
49
- col3.metric("Violations Capacité", capa_overflow, delta="Critique", delta_color="inverse")
50
- col4.metric("Dates Naissance Manquantes", missing_birth, delta="Qualité", delta_color="inverse")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  st.divider()
53
 
54
  # --- SECTION 1 : COMPLETENESS (Remplissage) ---
55
- st.subheader("1. Complétude des Données (Missing Values)")
56
- if df_pat is not None:
57
- # Calcul du taux de remplissage par colonne
58
- missing_df = df_pat.isnull().mean() * 100
59
- missing_df = missing_df[missing_df > 0].reset_index()
60
- missing_df.columns = ['Colonne', 'Pourcentage Manquant']
61
-
62
- fig_miss = px.bar(missing_df, x='Colonne', y='Pourcentage Manquant',
63
- title="Données manquantes - Table Patients",
64
- color='Pourcentage Manquant', color_continuous_scale='Reds')
65
- st.plotly_chart(fig_miss, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
- # --- SECTION 2 : ACCURACY (Outliers) ---
68
- st.subheader("2. Exactitude & Valeurs Aberrantes")
69
- c1, c2 = st.columns(2)
 
70
 
71
- with c1:
72
  if df_pat is not None:
73
- # Visualisation des âges > 100 ans ou < 0
74
- fig_age = px.box(df_pat, y='age', title="Distribution des Âges (Détection Outliers)",
75
- points="all") # Affiche tous les points
76
- # Zone rouge pour les âges impossibles
77
- fig_age.add_hrect(y0=120, y1=200, fillcolor="red", opacity=0.1, annotation_text="Erreur > 120 ans")
78
- fig_age.add_hrect(y0=-10, y1=0, fillcolor="red", opacity=0.1, annotation_text="Négatif")
79
- st.plotly_chart(fig_age, use_container_width=True)
80
-
81
- with c2:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  if df_res is not None:
83
- # Visualisation Occupation > 100%
84
- df_res['taux_pct'] = df_res['taux_occupation'] * 100
85
- fig_capa = px.scatter(df_res, x='date', y='taux_pct', color='service_code',
86
- title="Anomalies de Capacité (>100%)")
87
- fig_capa.add_hline(y=100, line_dash="dash", line_color="red", annotation_text="Saturation Théorique")
88
- st.plotly_chart(fig_capa, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
- # --- SECTION 3 : CONSISTENCY (Intégrité) ---
91
- st.subheader("3. Cohérence Temporelle")
92
- if inverted_dates > 0:
93
- st.error(f"🚨 ALERT : {inverted_dates} séjours ont une date de sortie antérieure à la date d'entrée.")
94
- st.dataframe(df_sej[df_sej['date_sortie'] < df_sej['date_entree']].head(10), use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  else:
96
- st.success("✅ Aucune incohérence temporelle détectée dans les séjours.")
 
 
 
 
 
13
  def show_dq_dashboard(client, sheet_name):
14
  st.markdown("## 🛡️ CENTRE DE CONTRÔLE DATA QUALITY")
15
 
16
+ # On charge via l'ETL
17
  etl = ETL_Engine(client, sheet_name)
18
 
19
  # On vérifie si les données sont déjà en session, sinon on charge
 
21
  st.info("Chargement des données pour Audit...")
22
  success, data = etl.run_etl()
23
  if success:
 
 
24
  pass
25
  else:
26
  st.error(f"Erreur chargement : {data}")
 
28
  else:
29
  data = st.session_state.data_explorer.dataframes
30
 
31
+ # Récupération des DataFrames
32
+ df_pat = data.get('patients')
33
+ df_sej = data.get('sejours')
34
+ df_res = data.get('ressources_jour')
35
+ df_stock = data.get('stock_materiel_jour')
36
+
37
  # --- KPI GLOBAL ---
38
  st.markdown("### 📊 Synthèse de Santé des Données")
39
  col1, col2, col3, col4 = st.columns(4)
40
 
41
+ # Calcul des indicateurs
42
+ if df_pat is not None:
43
+ anomalies_patients = df_pat['dq_is_anomaly'].sum()
44
+ doublons = df_pat.duplicated(subset=['nom', 'prenom']).sum()
45
+ ages_negatifs = (df_pat['age'] < 0).sum()
46
+ ages_aberrants = (df_pat['age'] > 120).sum()
47
+ missing_birth = df_pat['date_naissance'].isnull().sum()
48
+ missing_ville = df_pat['ville'].isnull().sum()
49
+ else:
50
+ anomalies_patients = doublons = ages_negatifs = ages_aberrants = missing_birth = missing_ville = 0
51
+
52
+ if df_sej is not None:
53
+ anomalies_sejours = df_sej['dq_is_anomaly'].sum()
54
+ dates_inversees = (df_sej['date_sortie'] < df_sej['date_entree']).sum()
55
+ gravite_aberrante = ((df_sej['gravite_1_5'] > 5) | (df_sej['gravite_1_5'] < 1)).sum()
56
+ equipement_manquant = df_sej['equipement_principal_code'].isnull().sum()
57
+ else:
58
+ anomalies_sejours = dates_inversees = gravite_aberrante = equipement_manquant = 0
59
+
60
+ if df_res is not None:
61
+ anomalies_ressources = df_res['dq_is_anomaly'].sum()
62
+ # Conversion de lits_occupes en numérique (peut être string)
63
+ df_res['lits_occupes_num'] = pd.to_numeric(df_res['lits_occupes'], errors='coerce')
64
+ suroccupation = (df_res['lits_occupes_num'] > df_res['lits_total']).sum()
65
+ else:
66
+ anomalies_ressources = suroccupation = 0
67
+
68
+ if df_stock is not None:
69
+ anomalies_stock = df_stock['dq_is_anomaly'].sum()
70
+ else:
71
+ anomalies_stock = 0
72
+
73
+ col1.metric("🔴 Anomalies Patients", f"{anomalies_patients:,}",
74
+ delta=f"{(anomalies_patients/len(df_pat)*100):.1f}%" if df_pat is not None and len(df_pat) > 0 else "N/A",
75
+ delta_color="inverse")
76
+ col2.metric("🔴 Anomalies Séjours", f"{anomalies_sejours:,}",
77
+ delta=f"{(anomalies_sejours/len(df_sej)*100):.1f}%" if df_sej is not None and len(df_sej) > 0 else "N/A",
78
+ delta_color="inverse")
79
+ col3.metric("🔴 Anomalies Ressources", f"{anomalies_ressources:,}",
80
+ delta=f"{(anomalies_ressources/len(df_res)*100):.2f}%" if df_res is not None and len(df_res) > 0 else "N/A",
81
+ delta_color="inverse")
82
+ col4.metric("🔴 Anomalies Stock", f"{anomalies_stock:,}",
83
+ delta=f"{(anomalies_stock/len(df_stock)*100):.2f}%" if df_stock is not None and len(df_stock) > 0 else "N/A",
84
+ delta_color="inverse")
85
 
86
  st.divider()
87
 
88
  # --- SECTION 1 : COMPLETENESS (Remplissage) ---
89
+ st.subheader("1️⃣ Complétude des Données (Missing Values)")
90
+
91
+ tab1, tab2, tab3, tab4 = st.tabs(["📋 Patients", "🏥 Séjours", "⚕️ Ressources", "📦 Stock"])
92
+
93
+ with tab1:
94
+ if df_pat is not None:
95
+ st.markdown("##### Analyse de complétude - Table PATIENTS")
96
+ col_a, col_b = st.columns(2)
97
+
98
+ with col_a:
99
+ # Taux de remplissage par colonne
100
+ missing_df = df_pat.isnull().mean() * 100
101
+ missing_df = missing_df[missing_df > 0].reset_index()
102
+ missing_df.columns = ['Colonne', 'Pourcentage Manquant']
103
+ missing_df = missing_df.sort_values('Pourcentage Manquant', ascending=False)
104
+
105
+ fig_miss = px.bar(missing_df, x='Colonne', y='Pourcentage Manquant',
106
+ title="Données manquantes - Patients",
107
+ color='Pourcentage Manquant',
108
+ color_continuous_scale='Reds',
109
+ text='Pourcentage Manquant')
110
+ fig_miss.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
111
+ fig_miss.update_layout(xaxis_tickangle=-45)
112
+ st.plotly_chart(fig_miss, use_container_width=True)
113
+
114
+ with col_b:
115
+ st.markdown("**Métriques clés :**")
116
+ st.metric("Dates de naissance manquantes", f"{missing_birth:,}")
117
+ st.metric("Villes manquantes", f"{missing_ville:,}")
118
+ st.metric("Doublons nom+prénom", f"{doublons:,}", delta="⚠️ Risque identité")
119
+
120
+ if missing_birth > 0 or missing_ville > 0:
121
+ st.warning(f"⚠️ {missing_birth + missing_ville:,} valeurs critiques manquantes")
122
+
123
+ with tab2:
124
+ if df_sej is not None:
125
+ st.markdown("##### Analyse de complétude - Table SEJOURS")
126
+ missing_sej = df_sej.isnull().mean() * 100
127
+ missing_sej = missing_sej[missing_sej > 0].reset_index()
128
+ missing_sej.columns = ['Colonne', 'Pourcentage Manquant']
129
+ missing_sej = missing_sej.sort_values('Pourcentage Manquant', ascending=False)
130
+
131
+ if len(missing_sej) > 0:
132
+ fig_miss_sej = px.bar(missing_sej, x='Colonne', y='Pourcentage Manquant',
133
+ title="Données manquantes - Séjours",
134
+ color='Pourcentage Manquant',
135
+ color_continuous_scale='Oranges',
136
+ text='Pourcentage Manquant')
137
+ fig_miss_sej.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
138
+ fig_miss_sej.update_layout(xaxis_tickangle=-45)
139
+ st.plotly_chart(fig_miss_sej, use_container_width=True)
140
+
141
+ st.metric("Équipements principaux manquants", f"{equipement_manquant:,}",
142
+ delta=f"{(equipement_manquant/len(df_sej)*100):.1f}%")
143
+ else:
144
+ st.success("✅ Aucune donnée manquante dans les colonnes critiques")
145
+
146
+ with tab3:
147
+ if df_res is not None:
148
+ st.markdown("##### Analyse de complétude - Table RESSOURCES_JOUR")
149
+ missing_res = df_res.isnull().mean() * 100
150
+ missing_res = missing_res[missing_res > 0].reset_index()
151
+ missing_res.columns = ['Colonne', 'Pourcentage Manquant']
152
+
153
+ if len(missing_res) > 0:
154
+ fig_miss_res = px.bar(missing_res, x='Colonne', y='Pourcentage Manquant',
155
+ title="Données manquantes - Ressources",
156
+ color='Pourcentage Manquant',
157
+ color_continuous_scale='Blues',
158
+ text='Pourcentage Manquant')
159
+ fig_miss_res.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
160
+ fig_miss_res.update_layout(xaxis_tickangle=-45)
161
+ st.plotly_chart(fig_miss_res, use_container_width=True)
162
+ else:
163
+ st.success("✅ Aucune donnée manquante détectée")
164
+
165
+ with tab4:
166
+ if df_stock is not None:
167
+ st.markdown("##### Analyse de complétude - Table STOCK_MATERIEL_JOUR")
168
+ missing_stock = df_stock.isnull().mean() * 100
169
+ missing_stock = missing_stock[missing_stock > 0].reset_index()
170
+ missing_stock.columns = ['Colonne', 'Pourcentage Manquant']
171
+
172
+ if len(missing_stock) > 0:
173
+ st.dataframe(missing_stock, use_container_width=True)
174
+ else:
175
+ st.success("✅ Aucune donnée manquante dans le stock")
176
+
177
+ st.divider()
178
 
179
+ # --- SECTION 2 : ACCURACY (Outliers & Anomalies) ---
180
+ st.subheader("2️⃣ Exactitude & Valeurs Aberrantes")
181
+
182
+ tab_acc1, tab_acc2, tab_acc3 = st.tabs(["👤 Patients", "🏥 Séjours", "🛏️ Ressources"])
183
 
184
+ with tab_acc1:
185
  if df_pat is not None:
186
+ st.markdown("##### Détection d'outliers - PATIENTS")
187
+ c1, c2 = st.columns(2)
188
+
189
+ with c1:
190
+ # Distribution des âges avec zones d'alerte
191
+ fig_age = px.box(df_pat, y='age',
192
+ title="Distribution des Âges (Détection Outliers)",
193
+ points="outliers")
194
+ fig_age.add_hrect(y0=120, y1=df_pat['age'].max() + 10,
195
+ fillcolor="red", opacity=0.2,
196
+ annotation_text="🚨 Âge > 120 ans",
197
+ annotation_position="top left")
198
+ fig_age.add_hrect(y0=df_pat['age'].min() - 10, y1=0,
199
+ fillcolor="red", opacity=0.2,
200
+ annotation_text="🚨 Âge négatif",
201
+ annotation_position="bottom left")
202
+ fig_age.update_layout(height=400)
203
+ st.plotly_chart(fig_age, use_container_width=True)
204
+
205
+ st.metric("Âges négatifs", ages_negatifs, delta="🚨 Critique" if ages_negatifs > 0 else "✅")
206
+ st.metric("Âges > 120 ans", ages_aberrants, delta="🚨 Impossible" if ages_aberrants > 0 else "✅")
207
+
208
+ with c2:
209
+ # Répartition des types d'anomalies
210
+ if 'dq_anomaly_type' in df_pat.columns:
211
+ anomalies_by_type = df_pat[df_pat['dq_is_anomaly'] == 1]['dq_anomaly_type'].value_counts()
212
+
213
+ fig_anom = px.pie(values=anomalies_by_type.values,
214
+ names=anomalies_by_type.index,
215
+ title="Répartition des Types d'Anomalies Patients",
216
+ color_discrete_sequence=px.colors.sequential.Reds_r)
217
+ fig_anom.update_traces(textposition='inside', textinfo='percent+label')
218
+ st.plotly_chart(fig_anom, use_container_width=True)
219
+
220
+ # Affichage des détails
221
+ st.markdown("**Détail par type :**")
222
+ for anom_type, count in anomalies_by_type.items():
223
+ st.write(f"- **{anom_type}** : {count:,} cas")
224
+
225
+ # Top 10 des anomalies les plus critiques
226
+ if df_pat['dq_is_anomaly'].sum() > 0:
227
+ st.markdown("##### 🔍 Top 10 Anomalies Patients")
228
+ anomalies_patients_df = df_pat[df_pat['dq_is_anomaly'] == 1][
229
+ ['patient_id', 'nom', 'prenom', 'age', 'date_naissance', 'ville',
230
+ 'dq_anomaly_type', 'dq_anomaly_detail']
231
+ ].head(10)
232
+ st.dataframe(anomalies_patients_df, use_container_width=True)
233
+
234
+ with tab_acc2:
235
+ if df_sej is not None:
236
+ st.markdown("##### Détection d'anomalies - SEJOURS")
237
+ c1, c2 = st.columns(2)
238
+
239
+ with c1:
240
+ # Distribution de la gravité
241
+ fig_grav = px.histogram(df_sej, x='gravite_1_5',
242
+ title="Distribution Gravité (Échelle 1-5)",
243
+ nbins=15,
244
+ color_discrete_sequence=['#1f77b4'])
245
+ fig_grav.add_vrect(x0=-0.5, x1=0.5, fillcolor="red", opacity=0.2,
246
+ annotation_text="Hors norme", annotation_position="top left")
247
+ fig_grav.add_vrect(x0=5.5, x1=df_sej['gravite_1_5'].max() + 1,
248
+ fillcolor="red", opacity=0.2,
249
+ annotation_text="Hors norme", annotation_position="top right")
250
+ st.plotly_chart(fig_grav, use_container_width=True)
251
+
252
+ st.metric("Gravité < 1", (df_sej['gravite_1_5'] < 1).sum())
253
+ st.metric("Gravité > 5", (df_sej['gravite_1_5'] > 5).sum())
254
+
255
+ with c2:
256
+ # Types d'anomalies séjours
257
+ if 'dq_anomaly_type' in df_sej.columns:
258
+ anomalies_sej = df_sej[df_sej['dq_is_anomaly'] == 1]['dq_anomaly_type'].value_counts()
259
+
260
+ fig_anom_sej = px.bar(x=anomalies_sej.index, y=anomalies_sej.values,
261
+ title="Types d'Anomalies Séjours",
262
+ color=anomalies_sej.values,
263
+ color_continuous_scale='Reds',
264
+ text=anomalies_sej.values)
265
+ fig_anom_sej.update_traces(texttemplate='%{text}', textposition='outside')
266
+ fig_anom_sej.update_layout(showlegend=False, xaxis_title="Type", yaxis_title="Nombre")
267
+ st.plotly_chart(fig_anom_sej, use_container_width=True)
268
+
269
+ # Anomalies critiques
270
+ if df_sej['dq_is_anomaly'].sum() > 0:
271
+ st.markdown("##### 🔍 Top 10 Anomalies Séjours")
272
+ anomalies_sejours_df = df_sej[df_sej['dq_is_anomaly'] == 1][
273
+ ['stay_id', 'patient_id', 'date_entree', 'date_sortie', 'duree_sejour_jours',
274
+ 'gravite_1_5', 'dq_anomaly_type', 'dq_anomaly_detail']
275
+ ].head(10)
276
+ st.dataframe(anomalies_sejours_df, use_container_width=True)
277
+
278
+ with tab_acc3:
279
  if df_res is not None:
280
+ st.markdown("##### Détection d'anomalies - RESSOURCES")
281
+ c1, c2 = st.columns(2)
282
+
283
+ with c1:
284
+ # Taux d'occupation dans le temps
285
+ fig_tx = px.scatter(df_res, x='date', y='taux_occupation',
286
+ color='service_code',
287
+ title="Taux d'Occupation par Service",
288
+ opacity=0.6)
289
+ fig_tx.add_hline(y=1.0, line_dash="dash", line_color="red",
290
+ annotation_text="⚠️ Saturation 100%",
291
+ annotation_position="right")
292
+ st.plotly_chart(fig_tx, use_container_width=True)
293
+
294
+ with c2:
295
+ # Cas de suroccupation
296
+ if suroccupation > 0:
297
+ st.error(f"🚨 {suroccupation} cas de SUROCCUPATION détectés !")
298
+ suroccup_df = df_res[df_res['lits_occupes_num'] > df_res['lits_total']][
299
+ ['date', 'service_code', 'lits_total', 'lits_occupes', 'taux_occupation']
300
+ ].head(10)
301
+ st.dataframe(suroccup_df, use_container_width=True)
302
+ else:
303
+ st.success("✅ Aucune suroccupation détectée")
304
+
305
+ # Types d'anomalies ressources
306
+ if 'dq_anomaly_type' in df_res.columns and df_res['dq_is_anomaly'].sum() > 0:
307
+ st.markdown("**Types d'anomalies ressources :**")
308
+ anom_res = df_res[df_res['dq_is_anomaly'] == 1]['dq_anomaly_type'].value_counts()
309
+ for anom_type, count in anom_res.items():
310
+ st.write(f"- **{anom_type}** : {count} cas")
311
 
312
+ st.divider()
313
+
314
+ # --- SECTION 3 : CONSISTENCY (Cohérence Temporelle) ---
315
+ st.subheader("3️⃣ Cohérence Temporelle & Intégrité Référentielle")
316
+
317
+ if df_sej is not None:
318
+ st.markdown("##### Incohérences Temporelles - SEJOURS")
319
+
320
+ if dates_inversees > 0:
321
+ st.error(f"🚨 ALERTE CRITIQUE : {dates_inversees} séjours ont une date de sortie antérieure à la date d'entrée !")
322
+
323
+ # Affichage des cas problématiques
324
+ inversions = df_sej[df_sej['date_sortie'] < df_sej['date_entree']][
325
+ ['stay_id', 'patient_id', 'service_code', 'date_entree', 'date_sortie', 'duree_sejour_jours']
326
+ ].head(20)
327
+
328
+ st.dataframe(inversions, use_container_width=True)
329
+
330
+ # Visualisation
331
+ inversions_temp = df_sej[df_sej['date_sortie'] < df_sej['date_entree']].copy()
332
+ inversions_temp['delta_jours'] = (inversions_temp['date_sortie'] - inversions_temp['date_entree']).dt.days
333
+
334
+ fig_inv = px.histogram(inversions_temp, x='delta_jours',
335
+ title="Distribution des Écarts Temporels (Dates Inversées)",
336
+ nbins=20,
337
+ color_discrete_sequence=['red'])
338
+ fig_inv.update_layout(xaxis_title="Écart en jours (négatif)", yaxis_title="Nombre de cas")
339
+ st.plotly_chart(fig_inv, use_container_width=True)
340
+ else:
341
+ st.success("✅ Aucune incohérence temporelle détectée dans les séjours.")
342
+
343
+ # --- SECTION 4 : RESUME GLOBAL ---
344
+ st.divider()
345
+ st.subheader("4️⃣ Résumé Exécutif Data Quality")
346
+
347
+ col_resume1, col_resume2, col_resume3 = st.columns(3)
348
+
349
+ with col_resume1:
350
+ st.markdown("##### 🎯 Complétude")
351
+ total_missing = 0
352
+ if df_pat is not None:
353
+ total_missing += df_pat.isnull().sum().sum()
354
+ if df_sej is not None:
355
+ total_missing += df_sej.isnull().sum().sum()
356
+ if df_res is not None:
357
+ total_missing += df_res.isnull().sum().sum()
358
+
359
+ st.metric("Total valeurs manquantes", f"{total_missing:,}")
360
+ completeness_score = 100 - (total_missing / (len(df_pat) + len(df_sej) + len(df_res)) * 100) if all([df_pat is not None, df_sej is not None, df_res is not None]) else 0
361
+ st.metric("Score de complétude", f"{completeness_score:.1f}%")
362
+
363
+ with col_resume2:
364
+ st.markdown("##### 🎯 Exactitude")
365
+ total_anomalies = anomalies_patients + anomalies_sejours + anomalies_ressources + anomalies_stock
366
+ st.metric("Total anomalies détectées", f"{total_anomalies:,}")
367
+
368
+ accuracy_score = 100 - (total_anomalies / (len(df_pat) + len(df_sej) + len(df_res) + len(df_stock)) * 100) if all([df_pat is not None, df_sej is not None, df_res is not None, df_stock is not None]) else 0
369
+ st.metric("Score d'exactitude", f"{accuracy_score:.2f}%")
370
+
371
+ with col_resume3:
372
+ st.markdown("##### 🎯 Cohérence")
373
+ consistency_issues = dates_inversees + suroccupation
374
+ st.metric("Violations de cohérence", f"{consistency_issues:,}")
375
+
376
+ if consistency_issues == 0:
377
+ st.success("✅ Données cohérentes")
378
+ else:
379
+ st.error(f"🚨 {consistency_issues} violations détectées")
380
+
381
+ # Score global
382
+ st.markdown("---")
383
+ global_score = (completeness_score + accuracy_score) / 2 if completeness_score > 0 and accuracy_score > 0 else 0
384
+
385
+ if global_score >= 95:
386
+ status = "🟢 EXCELLENT"
387
+ color = "green"
388
+ elif global_score >= 85:
389
+ status = "🟡 BON"
390
+ color = "orange"
391
+ elif global_score >= 70:
392
+ status = "🟠 MOYEN"
393
+ color = "orange"
394
  else:
395
+ status = "🔴 CRITIQUE"
396
+ color = "red"
397
+
398
+ st.markdown(f"### Score Global Data Quality : {global_score:.1f}% - {status}")
399
+ st.progress(global_score / 100)