Spaces:
Sleeping
Sleeping
Update src/module/Indicateurs_DQ.py
Browse files- 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"{
|
| 75 |
delta_color="inverse")
|
| 76 |
col2.metric("🔴 Anomalies Séjours", f"{anomalies_sejours:,}",
|
| 77 |
-
delta=f"{
|
| 78 |
delta_color="inverse")
|
| 79 |
col3.metric("🔴 Anomalies Ressources", f"{anomalies_ressources:,}",
|
| 80 |
-
delta=f"{
|
| 81 |
delta_color="inverse")
|
| 82 |
col4.metric("🔴 Anomalies Stock", f"{anomalies_stock:,}",
|
| 83 |
-
delta=f"{
|
| 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 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
| 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/
|
| 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 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
|
|
|
| 224 |
|
| 225 |
# Top 10 des anomalies les plus critiques
|
| 226 |
-
if
|
| 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 |
-
|
| 248 |
-
|
| 249 |
-
|
|
|
|
| 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 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
|
|
|
| 268 |
|
| 269 |
# Anomalies critiques
|
| 270 |
-
if
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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:
|