jaimalklyde commited on
Commit
022dd4c
·
verified ·
1 Parent(s): 6a16398

Update src/module/Indicateurs_DQ.py

Browse files
Files changed (1) hide show
  1. src/module/Indicateurs_DQ.py +149 -87
src/module/Indicateurs_DQ.py CHANGED
@@ -10,6 +10,24 @@ import plotly.express as px
10
  import plotly.graph_objects as go
11
  from Function.Validation_Contract import ETL_Engine
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  def show_dq_dashboard(client, sheet_name):
14
  st.markdown("## 🛡️ CENTRE DE CONTRÔLE DATA QUALITY")
15
 
@@ -38,49 +56,59 @@ def show_dq_dashboard(client, sheet_name):
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()
@@ -91,7 +119,7 @@ def show_dq_dashboard(client, sheet_name):
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
 
@@ -102,14 +130,17 @@ def show_dq_dashboard(client, sheet_name):
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 :**")
@@ -119,9 +150,11 @@ def show_dq_dashboard(client, sheet_name):
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()
@@ -139,12 +172,14 @@ def show_dq_dashboard(client, sheet_name):
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()
@@ -161,9 +196,11 @@ def show_dq_dashboard(client, sheet_name):
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()
@@ -173,6 +210,8 @@ def show_dq_dashboard(client, sheet_name):
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
 
@@ -182,7 +221,7 @@ def show_dq_dashboard(client, sheet_name):
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
 
@@ -191,14 +230,20 @@ def show_dq_dashboard(client, sheet_name):
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
 
@@ -210,29 +255,32 @@ def show_dq_dashboard(client, sheet_name):
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
 
@@ -242,41 +290,48 @@ def show_dq_dashboard(client, sheet_name):
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
 
@@ -303,18 +358,20 @@ def show_dq_dashboard(client, sheet_name):
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:
@@ -339,6 +396,8 @@ def show_dq_dashboard(client, sheet_name):
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()
@@ -349,15 +408,17 @@ def show_dq_dashboard(client, sheet_name):
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:
@@ -365,7 +426,8 @@ def show_dq_dashboard(client, sheet_name):
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:
 
10
  import plotly.graph_objects as go
11
  from Function.Validation_Contract import ETL_Engine
12
 
13
+ def safe_int(value):
14
+ """Convertit une valeur en int de manière sûre"""
15
+ try:
16
+ if pd.isna(value):
17
+ return 0
18
+ return int(value)
19
+ except (TypeError, ValueError):
20
+ return 0
21
+
22
+ def safe_float(value):
23
+ """Convertit une valeur en float de manière sûre"""
24
+ try:
25
+ if pd.isna(value):
26
+ return 0.0
27
+ return float(value)
28
+ except (TypeError, ValueError):
29
+ return 0.0
30
+
31
  def show_dq_dashboard(client, sheet_name):
32
  st.markdown("## 🛡️ CENTRE DE CONTRÔLE DATA QUALITY")
33
 
 
56
  st.markdown("### 📊 Synthèse de Santé des Données")
57
  col1, col2, col3, col4 = st.columns(4)
58
 
59
+ # Calcul des indicateurs avec conversion sécurisée
60
+ if df_pat is not None and len(df_pat) > 0:
61
+ anomalies_patients = safe_int(df_pat['dq_is_anomaly'].sum())
62
+ doublons = safe_int(df_pat.duplicated(subset=['nom', 'prenom']).sum())
63
+ ages_negatifs = safe_int((df_pat['age'] < 0).sum())
64
+ ages_aberrants = safe_int((df_pat['age'] > 120).sum())
65
+ missing_birth = safe_int(df_pat['date_naissance'].isnull().sum())
66
+ missing_ville = safe_int(df_pat['ville'].isnull().sum())
67
+ total_patients = len(df_pat)
68
  else:
69
+ anomalies_patients = doublons = ages_negatifs = ages_aberrants = missing_birth = missing_ville = total_patients = 0
70
 
71
+ if df_sej is not None and len(df_sej) > 0:
72
+ anomalies_sejours = safe_int(df_sej['dq_is_anomaly'].sum())
73
+ dates_inversees = safe_int((df_sej['date_sortie'] < df_sej['date_entree']).sum())
74
+ gravite_aberrante = safe_int(((df_sej['gravite_1_5'] > 5) | (df_sej['gravite_1_5'] < 1)).sum())
75
+ equipement_manquant = safe_int(df_sej['equipement_principal_code'].isnull().sum())
76
+ total_sejours = len(df_sej)
77
  else:
78
+ anomalies_sejours = dates_inversees = gravite_aberrante = equipement_manquant = total_sejours = 0
79
 
80
+ if df_res is not None and len(df_res) > 0:
81
+ anomalies_ressources = safe_int(df_res['dq_is_anomaly'].sum())
82
  # Conversion de lits_occupes en numérique (peut être string)
83
  df_res['lits_occupes_num'] = pd.to_numeric(df_res['lits_occupes'], errors='coerce')
84
+ suroccupation = safe_int((df_res['lits_occupes_num'] > df_res['lits_total']).sum())
85
+ total_ressources = len(df_res)
86
  else:
87
+ anomalies_ressources = suroccupation = total_ressources = 0
88
 
89
+ if df_stock is not None and len(df_stock) > 0:
90
+ anomalies_stock = safe_int(df_stock['dq_is_anomaly'].sum())
91
+ total_stock = len(df_stock)
92
  else:
93
+ anomalies_stock = total_stock = 0
94
+
95
+ # Calcul des pourcentages
96
+ pct_anom_pat = (anomalies_patients / total_patients * 100) if total_patients > 0 else 0
97
+ pct_anom_sej = (anomalies_sejours / total_sejours * 100) if total_sejours > 0 else 0
98
+ pct_anom_res = (anomalies_ressources / total_ressources * 100) if total_ressources > 0 else 0
99
+ pct_anom_stock = (anomalies_stock / total_stock * 100) if total_stock > 0 else 0
100
 
101
  col1.metric("🔴 Anomalies Patients", f"{anomalies_patients:,}",
102
+ delta=f"{pct_anom_pat:.1f}%",
103
  delta_color="inverse")
104
  col2.metric("🔴 Anomalies Séjours", f"{anomalies_sejours:,}",
105
+ delta=f"{pct_anom_sej:.1f}%",
106
  delta_color="inverse")
107
  col3.metric("🔴 Anomalies Ressources", f"{anomalies_ressources:,}",
108
+ delta=f"{pct_anom_res:.2f}%",
109
  delta_color="inverse")
110
  col4.metric("🔴 Anomalies Stock", f"{anomalies_stock:,}",
111
+ delta=f"{pct_anom_stock:.2f}%",
112
  delta_color="inverse")
113
 
114
  st.divider()
 
119
  tab1, tab2, tab3, tab4 = st.tabs(["📋 Patients", "🏥 Séjours", "⚕️ Ressources", "📦 Stock"])
120
 
121
  with tab1:
122
+ if df_pat is not None and len(df_pat) > 0:
123
  st.markdown("##### Analyse de complétude - Table PATIENTS")
124
  col_a, col_b = st.columns(2)
125
 
 
130
  missing_df.columns = ['Colonne', 'Pourcentage Manquant']
131
  missing_df = missing_df.sort_values('Pourcentage Manquant', ascending=False)
132
 
133
+ if len(missing_df) > 0:
134
+ fig_miss = px.bar(missing_df, x='Colonne', y='Pourcentage Manquant',
135
+ title="Données manquantes - Patients",
136
+ color='Pourcentage Manquant',
137
+ color_continuous_scale='Reds',
138
+ text='Pourcentage Manquant')
139
+ fig_miss.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
140
+ fig_miss.update_layout(xaxis_tickangle=-45)
141
+ st.plotly_chart(fig_miss, use_container_width=True)
142
+ else:
143
+ st.success("✅ Aucune donnée manquante détectée")
144
 
145
  with col_b:
146
  st.markdown("**Métriques clés :**")
 
150
 
151
  if missing_birth > 0 or missing_ville > 0:
152
  st.warning(f"⚠️ {missing_birth + missing_ville:,} valeurs critiques manquantes")
153
+ else:
154
+ st.warning("Aucune donnée patient disponible")
155
 
156
  with tab2:
157
+ if df_sej is not None and len(df_sej) > 0:
158
  st.markdown("##### Analyse de complétude - Table SEJOURS")
159
  missing_sej = df_sej.isnull().mean() * 100
160
  missing_sej = missing_sej[missing_sej > 0].reset_index()
 
172
  st.plotly_chart(fig_miss_sej, use_container_width=True)
173
 
174
  st.metric("Équipements principaux manquants", f"{equipement_manquant:,}",
175
+ delta=f"{(equipement_manquant/total_sejours*100):.1f}%" if total_sejours > 0 else "N/A")
176
  else:
177
  st.success("✅ Aucune donnée manquante dans les colonnes critiques")
178
+ else:
179
+ st.warning("Aucune donnée séjour disponible")
180
 
181
  with tab3:
182
+ if df_res is not None and len(df_res) > 0:
183
  st.markdown("##### Analyse de complétude - Table RESSOURCES_JOUR")
184
  missing_res = df_res.isnull().mean() * 100
185
  missing_res = missing_res[missing_res > 0].reset_index()
 
196
  st.plotly_chart(fig_miss_res, use_container_width=True)
197
  else:
198
  st.success("✅ Aucune donnée manquante détectée")
199
+ else:
200
+ st.warning("Aucune donnée ressource disponible")
201
 
202
  with tab4:
203
+ if df_stock is not None and len(df_stock) > 0:
204
  st.markdown("##### Analyse de complétude - Table STOCK_MATERIEL_JOUR")
205
  missing_stock = df_stock.isnull().mean() * 100
206
  missing_stock = missing_stock[missing_stock > 0].reset_index()
 
210
  st.dataframe(missing_stock, use_container_width=True)
211
  else:
212
  st.success("✅ Aucune donnée manquante dans le stock")
213
+ else:
214
+ st.warning("Aucune donnée stock disponible")
215
 
216
  st.divider()
217
 
 
221
  tab_acc1, tab_acc2, tab_acc3 = st.tabs(["👤 Patients", "🏥 Séjours", "🛏️ Ressources"])
222
 
223
  with tab_acc1:
224
+ if df_pat is not None and len(df_pat) > 0:
225
  st.markdown("##### Détection d'outliers - PATIENTS")
226
  c1, c2 = st.columns(2)
227
 
 
230
  fig_age = px.box(df_pat, y='age',
231
  title="Distribution des Âges (Détection Outliers)",
232
  points="outliers")
233
+
234
+ age_max = safe_float(df_pat['age'].max())
235
+ age_min = safe_float(df_pat['age'].min())
236
+
237
+ if age_max > 120:
238
+ fig_age.add_hrect(y0=120, y1=age_max + 10,
239
+ fillcolor="red", opacity=0.2,
240
+ annotation_text="🚨 Âge > 120 ans",
241
+ annotation_position="top left")
242
+ if age_min < 0:
243
+ fig_age.add_hrect(y0=age_min - 10, y1=0,
244
+ fillcolor="red", opacity=0.2,
245
+ annotation_text="🚨 Âge négatif",
246
+ annotation_position="bottom left")
247
  fig_age.update_layout(height=400)
248
  st.plotly_chart(fig_age, use_container_width=True)
249
 
 
255
  if 'dq_anomaly_type' in df_pat.columns:
256
  anomalies_by_type = df_pat[df_pat['dq_is_anomaly'] == 1]['dq_anomaly_type'].value_counts()
257
 
258
+ if len(anomalies_by_type) > 0:
259
+ fig_anom = px.pie(values=anomalies_by_type.values,
260
+ names=anomalies_by_type.index,
261
+ title="Répartition des Types d'Anomalies Patients",
262
+ color_discrete_sequence=px.colors.sequential.Reds_r)
263
+ fig_anom.update_traces(textposition='inside', textinfo='percent+label')
264
+ st.plotly_chart(fig_anom, use_container_width=True)
265
+
266
+ # Affichage des détails
267
+ st.markdown("**Détail par type :**")
268
+ for anom_type, count in anomalies_by_type.items():
269
+ st.write(f"- **{anom_type}** : {safe_int(count):,} cas")
270
 
271
  # Top 10 des anomalies les plus critiques
272
+ if anomalies_patients > 0:
273
  st.markdown("##### 🔍 Top 10 Anomalies Patients")
274
  anomalies_patients_df = df_pat[df_pat['dq_is_anomaly'] == 1][
275
  ['patient_id', 'nom', 'prenom', 'age', 'date_naissance', 'ville',
276
  'dq_anomaly_type', 'dq_anomaly_detail']
277
  ].head(10)
278
  st.dataframe(anomalies_patients_df, use_container_width=True)
279
+ else:
280
+ st.warning("Aucune donnée patient disponible")
281
 
282
  with tab_acc2:
283
+ if df_sej is not None and len(df_sej) > 0:
284
  st.markdown("##### Détection d'anomalies - SEJOURS")
285
  c1, c2 = st.columns(2)
286
 
 
290
  title="Distribution Gravité (Échelle 1-5)",
291
  nbins=15,
292
  color_discrete_sequence=['#1f77b4'])
293
+
294
+ grav_max = safe_float(df_sej['gravite_1_5'].max())
295
+
296
  fig_grav.add_vrect(x0=-0.5, x1=0.5, fillcolor="red", opacity=0.2,
297
  annotation_text="Hors norme", annotation_position="top left")
298
+ if grav_max > 5:
299
+ fig_grav.add_vrect(x0=5.5, x1=grav_max + 1,
300
+ fillcolor="red", opacity=0.2,
301
+ annotation_text="Hors norme", annotation_position="top right")
302
  st.plotly_chart(fig_grav, use_container_width=True)
303
 
304
+ st.metric("Gravité < 1", safe_int((df_sej['gravite_1_5'] < 1).sum()))
305
+ st.metric("Gravité > 5", safe_int((df_sej['gravite_1_5'] > 5).sum()))
306
 
307
  with c2:
308
  # Types d'anomalies séjours
309
  if 'dq_anomaly_type' in df_sej.columns:
310
  anomalies_sej = df_sej[df_sej['dq_is_anomaly'] == 1]['dq_anomaly_type'].value_counts()
311
 
312
+ if len(anomalies_sej) > 0:
313
+ fig_anom_sej = px.bar(x=anomalies_sej.index, y=anomalies_sej.values,
314
+ title="Types d'Anomalies Séjours",
315
+ color=anomalies_sej.values,
316
+ color_continuous_scale='Reds',
317
+ text=anomalies_sej.values)
318
+ fig_anom_sej.update_traces(texttemplate='%{text}', textposition='outside')
319
+ fig_anom_sej.update_layout(showlegend=False, xaxis_title="Type", yaxis_title="Nombre")
320
+ st.plotly_chart(fig_anom_sej, use_container_width=True)
321
 
322
  # Anomalies critiques
323
+ if anomalies_sejours > 0:
324
  st.markdown("##### 🔍 Top 10 Anomalies Séjours")
325
  anomalies_sejours_df = df_sej[df_sej['dq_is_anomaly'] == 1][
326
  ['stay_id', 'patient_id', 'date_entree', 'date_sortie', 'duree_sejour_jours',
327
  'gravite_1_5', 'dq_anomaly_type', 'dq_anomaly_detail']
328
  ].head(10)
329
  st.dataframe(anomalies_sejours_df, use_container_width=True)
330
+ else:
331
+ st.warning("Aucune donnée séjour disponible")
332
 
333
  with tab_acc3:
334
+ if df_res is not None and len(df_res) > 0:
335
  st.markdown("##### Détection d'anomalies - RESSOURCES")
336
  c1, c2 = st.columns(2)
337
 
 
358
  st.success("✅ Aucune suroccupation détectée")
359
 
360
  # Types d'anomalies ressources
361
+ if 'dq_anomaly_type' in df_res.columns and anomalies_ressources > 0:
362
  st.markdown("**Types d'anomalies ressources :**")
363
  anom_res = df_res[df_res['dq_is_anomaly'] == 1]['dq_anomaly_type'].value_counts()
364
  for anom_type, count in anom_res.items():
365
+ st.write(f"- **{anom_type}** : {safe_int(count)} cas")
366
+ else:
367
+ st.warning("Aucune donnée ressource disponible")
368
 
369
  st.divider()
370
 
371
  # --- SECTION 3 : CONSISTENCY (Cohérence Temporelle) ---
372
  st.subheader("3️⃣ Cohérence Temporelle & Intégrité Référentielle")
373
 
374
+ if df_sej is not None and len(df_sej) > 0:
375
  st.markdown("##### Incohérences Temporelles - SEJOURS")
376
 
377
  if dates_inversees > 0:
 
396
  st.plotly_chart(fig_inv, use_container_width=True)
397
  else:
398
  st.success("✅ Aucune incohérence temporelle détectée dans les séjours.")
399
+ else:
400
+ st.warning("Aucune donnée séjour disponible pour l'analyse de cohérence")
401
 
402
  # --- SECTION 4 : RESUME GLOBAL ---
403
  st.divider()
 
408
  with col_resume1:
409
  st.markdown("##### 🎯 Complétude")
410
  total_missing = 0
411
+ if df_pat is not None and len(df_pat) > 0:
412
+ total_missing += safe_int(df_pat.isnull().sum().sum())
413
+ if df_sej is not None and len(df_sej) > 0:
414
+ total_missing += safe_int(df_sej.isnull().sum().sum())
415
+ if df_res is not None and len(df_res) > 0:
416
+ total_missing += safe_int(df_res.isnull().sum().sum())
417
 
418
  st.metric("Total valeurs manquantes", f"{total_missing:,}")
419
+
420
+ total_rows = total_patients + total_sejours + total_ressources
421
+ completeness_score = 100 - (total_missing / total_rows * 100) if total_rows > 0 else 0
422
  st.metric("Score de complétude", f"{completeness_score:.1f}%")
423
 
424
  with col_resume2:
 
426
  total_anomalies = anomalies_patients + anomalies_sejours + anomalies_ressources + anomalies_stock
427
  st.metric("Total anomalies détectées", f"{total_anomalies:,}")
428
 
429
+ total_all_rows = total_patients + total_sejours + total_ressources + total_stock
430
+ accuracy_score = 100 - (total_anomalies / total_all_rows * 100) if total_all_rows > 0 else 0
431
  st.metric("Score d'exactitude", f"{accuracy_score:.2f}%")
432
 
433
  with col_resume3: