ProjetData / src /module /Indicateurs_DQ.py
jaimalklyde's picture
Update src/module/Indicateurs_DQ.py
a719099 verified
Raw History Blame Contribute Delete
30 kB
"""
MODULE: DATA QUALITY DASHBOARD - VERSION CORRIGÉE
=================================================
Responsabilité : Audit complet, détection d'anomalies.
Correction : Gestion stricte des types numériques pour éviter les crashs (int/str).
"""
import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from datetime import datetime
import warnings
warnings.filterwarnings('ignore')
# =====================================================
# ================== OUTILS SÉCURITÉ ==================
# =====================================================
def safe_int(value):
"""Convertit une valeur en int de manière sûre"""
try:
if pd.isna(value):
return 0
return int(float(value)) # Double cast pour gérer les strings '1.0'
except (TypeError, ValueError, OverflowError):
return 0
def safe_float(value):
"""Convertit une valeur en float de manière sûre"""
try:
if pd.isna(value):
return 0.0
return float(value)
except (TypeError, ValueError, OverflowError):
return 0.0
def safe_pct(num, den):
"""Calcul sécurisé de pourcentage"""
try:
if den == 0:
return 0.0
return round((float(num) / float(den)) * 100, 2)
except Exception:
return 0.0
# =====================================================
# ================= CHARGEMENT DONNÉES ================
# =====================================================
@st.cache_data(ttl=60) # TTL court pour voir les nouvelles données injectées
def load_from_google_sheets(_client, sheet_name: str) -> dict:
"""Charge les données depuis Google Sheets SANS passer par ETL."""
sheets = {}
try:
sheet_names = [
'patients', 'sejours', 'ressources_jour',
'stock_materiel_jour', 'admissions_jour', 'scenarios_calendrier'
]
for name in sheet_names:
try:
worksheet = _client.open(sheet_name).worksheet(name)
data = worksheet.get_all_records()
df = pd.DataFrame(data)
# Conversion des dates
date_cols = ['date', 'date_entree', 'date_sortie', 'date_naissance']
for col in date_cols:
if col in df.columns:
df[col] = pd.to_datetime(df[col], errors='coerce')
sheets[name] = df
except Exception as e:
# On continue même si une feuille manque
continue
return sheets
except Exception as e:
st.error(f"Erreur lors du chargement depuis Google Sheets : {str(e)}")
return {}
@st.cache_data(ttl=3600)
def load_from_excel_file(file) -> dict:
"""Charge les données depuis un fichier Excel uploadé."""
sheets = {}
try:
sheets['patients'] = pd.read_excel(file, sheet_name='patients')
sheets['sejours'] = pd.read_excel(file, sheet_name='sejours')
sheets['ressources_jour'] = pd.read_excel(file, sheet_name='ressources_jour')
sheets['stock_materiel_jour'] = pd.read_excel(file, sheet_name='stock_materiel_jour')
sheets['admissions_jour'] = pd.read_excel(file, sheet_name='admissions_jour')
sheets['scenarios_calendrier'] = pd.read_excel(file, sheet_name='scenarios_calendrier')
for sheet_name, df in sheets.items():
for col in df.columns:
if 'date' in col.lower():
df[col] = pd.to_datetime(df[col], errors='coerce')
return sheets
except Exception as e:
st.error(f"Erreur lors du chargement du fichier Excel : {str(e)}")
return {}
# =====================================================
# ============ ANALYSES DATA QUALITY ==================
# =====================================================
class HospitalDataQualityAnalyzer:
"""Analyseur complet de qualité des données hospitalières"""
def __init__(self, dataframes: dict):
self.df_pat = dataframes.get('patients')
self.df_sej = dataframes.get('sejours')
self.df_res = dataframes.get('ressources_jour')
self.df_stock = dataframes.get('stock_materiel_jour')
self.df_adm = dataframes.get('admissions_jour')
self.df_scenarios = dataframes.get('scenarios_calendrier')
self.metrics = {}
self._compute_all_metrics()
def _force_numeric_anomaly(self, df):
"""Force la colonne dq_is_anomaly en numérique pour éviter les crashs"""
if df is not None and not df.empty and 'dq_is_anomaly' in df.columns:
# Conversion robuste : force tout en nombre, remplace les erreurs par 0
df['dq_is_anomaly'] = pd.to_numeric(df['dq_is_anomaly'], errors='coerce').fillna(0).astype(int)
return df
def _compute_all_metrics(self):
"""Calcul de toutes les métriques DQ"""
# NETTOYAGE PRÉALABLE : Conversion des types pour éviter le crash int+str
self.df_pat = self._force_numeric_anomaly(self.df_pat)
self.df_sej = self._force_numeric_anomaly(self.df_sej)
self.df_res = self._force_numeric_anomaly(self.df_res)
self.df_stock = self._force_numeric_anomaly(self.df_stock)
# ============= PATIENTS =============
if self.df_pat is not None and not self.df_pat.empty:
self.metrics['patients'] = {
'total': len(self.df_pat),
'anomalies': int(self.df_pat['dq_is_anomaly'].sum()),
'doublons': int(self.df_pat.duplicated(subset=['nom', 'prenom']).sum()),
'ages_negatifs': int((pd.to_numeric(self.df_pat['age'], errors='coerce') < 0).sum()),
'ages_aberrants': int((pd.to_numeric(self.df_pat['age'], errors='coerce') > 120).sum()),
'missing_birth': int(self.df_pat['date_naissance'].isnull().sum()),
'missing_ville': int(self.df_pat['ville'].isnull().sum()),
'missing_total': int(self.df_pat.isnull().sum().sum())
}
else:
self.metrics['patients'] = {k: 0 for k in ['total', 'anomalies', 'doublons', 'ages_negatifs',
'ages_aberrants', 'missing_birth', 'missing_ville', 'missing_total']}
# ============= SÉJOURS =============
if self.df_sej is not None and not self.df_sej.empty:
dates_inversees = int((self.df_sej['date_sortie'] < self.df_sej['date_entree']).sum())
gravite = pd.to_numeric(self.df_sej['gravite_1_5'], errors='coerce').fillna(3)
gravite_aberrante = int(((gravite > 5) | (gravite < 0)).sum())
self.metrics['sejours'] = {
'total': len(self.df_sej),
'anomalies': int(self.df_sej['dq_is_anomaly'].sum()),
'dates_inversees': dates_inversees,
'gravite_aberrante': gravite_aberrante,
'equipement_manquant': int(self.df_sej['equipement_principal_code'].isnull().sum()),
'missing_total': int(self.df_sej.isnull().sum().sum())
}
else:
self.metrics['sejours'] = {k: 0 for k in ['total', 'anomalies', 'dates_inversees',
'gravite_aberrante', 'equipement_manquant', 'missing_total']}
# ============= RESSOURCES =============
if self.df_res is not None and not self.df_res.empty:
df_res_temp = self.df_res.copy()
df_res_temp['lits_occupes_num'] = pd.to_numeric(df_res_temp['lits_occupes'], errors='coerce').fillna(0)
df_res_temp['lits_total_num'] = pd.to_numeric(df_res_temp['lits_total'], errors='coerce').fillna(1)
suroccupation = int((df_res_temp['lits_occupes_num'] > df_res_temp['lits_total_num']).sum())
self.metrics['ressources'] = {
'total': len(self.df_res),
'anomalies': int(self.df_res['dq_is_anomaly'].sum()),
'suroccupation': suroccupation,
'missing_total': int(self.df_res.isnull().sum().sum())
}
else:
self.metrics['ressources'] = {k: 0 for k in ['total', 'anomalies', 'suroccupation', 'missing_total']}
# ============= STOCK =============
if self.df_stock is not None and not self.df_stock.empty:
self.metrics['stock'] = {
'total': len(self.df_stock),
'anomalies': int(self.df_stock['dq_is_anomaly'].sum()),
'alertes': int((self.df_stock['alerte_stock'].astype(str) == '1').sum()),
'missing_total': int(self.df_stock.isnull().sum().sum())
}
else:
self.metrics['stock'] = {k: 0 for k in ['total', 'anomalies', 'alertes', 'missing_total']}
def get_anomalies_by_type(self, df_name: str):
"""Récupère le détail des anomalies par type"""
df = getattr(self, f'df_{df_name}', None)
if df is None or 'dq_anomaly_type' not in df.columns:
return pd.DataFrame()
anomalies = df[df['dq_is_anomaly'] == 1]
if anomalies.empty:
return pd.DataFrame()
anomaly_counts = anomalies['dq_anomaly_type'].value_counts().reset_index()
anomaly_counts.columns = ['Type Anomalie', 'Nombre']
return anomaly_counts
def get_temporal_anomalies(self, df_name: str):
"""Analyse temporelle des anomalies"""
df = getattr(self, f'df_{df_name}', None)
if df is None or 'date' not in df.columns or 'dq_is_anomaly' not in df.columns:
return None
# Groupement par date
temp_anomalies = df.groupby(df['date'].dt.to_period('M'))['dq_is_anomaly'].agg(['sum', 'count']).reset_index()
temp_anomalies['date'] = temp_anomalies['date'].dt.to_timestamp()
temp_anomalies['pourcentage'] = (temp_anomalies['sum'] / temp_anomalies['count']) * 100
return temp_anomalies
def get_service_quality_stats(self):
"""Statistiques de qualité par service"""
if self.df_sej is None:
return None
service_stats = self.df_sej.groupby('service_code').agg({
'stay_id': 'count',
'dq_is_anomaly': 'sum',
'duree_sejour_jours': 'mean',
'gravite_1_5': 'mean'
}).reset_index()
service_stats.columns = ['Service', 'Nb Séjours', 'Anomalies', 'Durée Moy', 'Gravité Moy']
service_stats['Taux Anomalie %'] = (service_stats['Anomalies'] / service_stats['Nb Séjours']) * 100
return service_stats.round(2)
def compute_global_score(self):
"""Calcul du score global DQ"""
# Complétude
total_rows = (self.metrics['patients']['total'] +
self.metrics['sejours']['total'] +
self.metrics['ressources']['total'])
total_missing = (self.metrics['patients']['missing_total'] +
self.metrics['sejours']['missing_total'] +
self.metrics['ressources']['missing_total'])
completeness_score = safe_pct(total_rows - total_missing, total_rows)
# Exactitude
total_all_rows = (self.metrics['patients']['total'] +
self.metrics['sejours']['total'] +
self.metrics['ressources']['total'] +
self.metrics['stock']['total'])
total_anomalies = (self.metrics['patients']['anomalies'] +
self.metrics['sejours']['anomalies'] +
self.metrics['ressources']['anomalies'] +
self.metrics['stock']['anomalies'])
accuracy_score = safe_pct(total_all_rows - total_anomalies, total_all_rows)
# Score global
global_score = round((completeness_score + accuracy_score) / 2, 2)
return {
'completeness': completeness_score,
'accuracy': accuracy_score,
'global': global_score
}
# =====================================================
# ============== VISUALISATIONS =======================
# =====================================================
def plot_anomalies_distribution(analyzer: HospitalDataQualityAnalyzer):
"""Graphique de distribution des anomalies par table"""
data = {
'Table': ['Patients', 'Séjours', 'Ressources', 'Stock'],
'Anomalies': [
analyzer.metrics['patients']['anomalies'],
analyzer.metrics['sejours']['anomalies'],
analyzer.metrics['ressources']['anomalies'],
analyzer.metrics['stock']['anomalies']
],
'Total': [
analyzer.metrics['patients']['total'],
analyzer.metrics['sejours']['total'],
analyzer.metrics['ressources']['total'],
analyzer.metrics['stock']['total']
]
}
df_plot = pd.DataFrame(data)
df_plot['Pourcentage'] = (df_plot['Anomalies'] / df_plot['Total']) * 100
fig = make_subplots(
rows=1, cols=2,
subplot_titles=('Nombre d\'Anomalies', 'Taux d\'Anomalies (%)'),
specs=[[{"type": "bar"}, {"type": "bar"}]]
)
# Nombre absolu
fig.add_trace(
go.Bar(
x=df_plot['Table'],
y=df_plot['Anomalies'],
text=df_plot['Anomalies'],
textposition='outside',
marker_color='crimson',
name='Anomalies'
),
row=1, col=1
)
# Pourcentage
fig.add_trace(
go.Bar(
x=df_plot['Table'],
y=df_plot['Pourcentage'],
text=[f"{x:.2f}%" for x in df_plot['Pourcentage']],
textposition='outside',
marker_color='orange',
name='Taux %'
),
row=1, col=2
)
fig.update_layout(
height=400,
showlegend=False,
title_text="📊 Distribution des Anomalies par Table"
)
return fig
def plot_temporal_anomalies(analyzer: HospitalDataQualityAnalyzer):
"""Évolution temporelle des anomalies pour séjours"""
temp_data = analyzer.get_temporal_anomalies('sej')
if temp_data is None or temp_data.empty:
return None
fig = go.Figure()
fig.add_trace(go.Scatter(
x=temp_data['date'],
y=temp_data['pourcentage'],
mode='lines+markers',
name='Taux Anomalies',
line=dict(color='red', width=2),
marker=dict(size=8)
))
fig.update_layout(
title="📈 Évolution Temporelle des Anomalies (Séjours)",
xaxis_title="Date",
yaxis_title="Taux d'Anomalies (%)",
height=400,
hovermode='x unified'
)
return fig
def plot_service_quality_heatmap(analyzer: HospitalDataQualityAnalyzer):
"""Heatmap de qualité par service"""
service_stats = analyzer.get_service_quality_stats()
if service_stats is None or service_stats.empty:
return None
fig = px.bar(
service_stats.sort_values('Taux Anomalie %', ascending=False).head(10),
x='Service',
y='Taux Anomalie %',
color='Taux Anomalie %',
color_continuous_scale='Reds',
title="🏥 Taux d'Anomalies par Service (Top 10)",
text='Taux Anomalie %'
)
fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
fig.update_layout(height=450)
return fig
def plot_completeness_radar(analyzer: HospitalDataQualityAnalyzer):
"""Radar chart de complétude par dimension"""
metrics = analyzer.metrics
# Calcul des complétudes
patient_completeness = 100 - safe_pct(metrics['patients']['missing_total'],
metrics['patients']['total'] * len(analyzer.df_pat.columns) if analyzer.df_pat is not None else 1)
sejour_completeness = 100 - safe_pct(metrics['sejours']['missing_total'],
metrics['sejours']['total'] * len(analyzer.df_sej.columns) if analyzer.df_sej is not None else 1)
ressource_completeness = 100 - safe_pct(metrics['ressources']['missing_total'],
metrics['ressources']['total'] * len(analyzer.df_res.columns) if analyzer.df_res is not None else 1)
stock_completeness = 100 - safe_pct(metrics['stock']['missing_total'],
metrics['stock']['total'] * len(analyzer.df_stock.columns) if analyzer.df_stock is not None else 1)
categories = ['Patients', 'Séjours', 'Ressources', 'Stock']
values = [patient_completeness, sejour_completeness, ressource_completeness, stock_completeness]
values.append(values[0]) # Fermeture
fig = go.Figure()
fig.add_trace(go.Scatterpolar(
r=values,
theta=categories + [categories[0]],
fill='toself',
name='Complétude',
line_color='cyan'
))
fig.update_layout(
polar=dict(
radialaxis=dict(
visible=True,
range=[0, 100]
)
),
showlegend=False,
title="🎯 Radar de Complétude des Données",
height=400
)
return fig
# =====================================================
# ================= DASHBOARD PRINCIPAL ===============
# =====================================================
def show_dq_dashboard(_client=None, sheet_name=None):
"""
Dashboard Data Quality complet avec support Google Sheets et Upload Fichier.
"""
st.markdown("## 🛡️ CENTRE DE CONTRÔLE DATA QUALITY")
st.caption("Analyse complète de la qualité des données hospitalières")
dataframes = None
# ================= MODE DE CHARGEMENT =================
if _client is not None and sheet_name is not None:
st.info("📊 **Mode : Google Sheets**")
with st.spinner("🔄 Chargement depuis Google Sheets..."):
dataframes = load_from_google_sheets(_client, sheet_name)
else:
st.sidebar.header("⚙️ Configuration")
uploaded_file = st.sidebar.file_uploader(
"Charger le fichier Excel",
type=['xlsx'],
help="Fichier bdd_mvp_faker.xlsx"
)
if uploaded_file is not None:
st.info("📁 **Mode : Fichier Excel Local**")
with st.spinner("🔄 Chargement du fichier Excel..."):
dataframes = load_from_excel_file(uploaded_file)
else:
st.info("👈 Veuillez charger le fichier Excel dans la barre latérale")
return
# ================= VÉRIFICATION CHARGEMENT =================
if not dataframes or len(dataframes) == 0:
st.error("❌ Erreur lors du chargement des données")
return
# ================= ANALYSE =================
analyzer = HospitalDataQualityAnalyzer(dataframes)
scores = analyzer.compute_global_score()
total_lignes = sum(m['total'] for m in analyzer.metrics.values())
st.success(f"✅ Données chargées : **{total_lignes:,}** lignes au total")
# ==================================================
# ================= KPI GLOBAUX ====================
# ==================================================
st.markdown("### 📊 Synthèse de Santé des Données")
col1, col2, col3, col4 = st.columns(4)
metrics = analyzer.metrics
col1.metric("🔴 Anomalies Patients", f"{metrics['patients']['anomalies']:,}", delta_color="inverse")
col2.metric("🔴 Anomalies Séjours", f"{metrics['sejours']['anomalies']:,}", delta_color="inverse")
col3.metric("🔴 Anomalies Ressources", f"{metrics['ressources']['anomalies']:,}", delta_color="inverse")
col4.metric("🔴 Anomalies Stock", f"{metrics['stock']['anomalies']:,}", delta_color="inverse")
st.plotly_chart(plot_anomalies_distribution(analyzer), use_container_width=True)
st.divider()
# ==================================================
# ========== 1️⃣ COMPLÉTUDE DES DONNÉES ============
# ==================================================
st.subheader("1️⃣ Complétude des Données (Missing Values)")
tab1, tab2, tab3, tab4 = st.tabs(["📋 Patients", "🏥 Séjours", "⚕️ Ressources", "📦 Stock"])
for tab, df_name, df in [(tab1, 'Patients', analyzer.df_pat),
(tab2, 'Séjours', analyzer.df_sej),
(tab3, 'Ressources', analyzer.df_res),
(tab4, 'Stock', analyzer.df_stock)]:
with tab:
if df is not None and not df.empty:
missing_df = (df.isnull().mean() * 100).reset_index()
missing_df.columns = ['Colonne', 'Pourcentage Manquant']
missing_df = missing_df[missing_df['Pourcentage Manquant'] > 0]
if not missing_df.empty:
st.caption(f"📊 {len(missing_df)} colonnes avec données manquantes")
fig = px.bar(missing_df.sort_values('Pourcentage Manquant', ascending=False),
x='Colonne', y='Pourcentage Manquant',
title=f"Données manquantes - {df_name}",
color='Pourcentage Manquant', color_continuous_scale='Reds', text='Pourcentage Manquant')
fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
fig.update_layout(xaxis_tickangle=-45, height=400)
st.plotly_chart(fig, use_container_width=True)
else:
st.success(f"✅ Aucune donnée manquante dans {df_name}")
else:
st.warning(f"⚠️ Aucune donnée {df_name} disponible")
st.plotly_chart(plot_completeness_radar(analyzer), use_container_width=True)
st.divider()
# ==================================================
# ========== 2️⃣ COHÉRENCE DES DONNÉES =============
# ==================================================
st.subheader("2️⃣ Cohérence et Validité des Données")
col1, col2 = st.columns(2)
with col1:
st.markdown("#### 👥 Cohérence Patients")
st.metric("Âges négatifs", metrics['patients']['ages_negatifs'], delta_color="inverse")
st.metric("Âges aberrants (>120 ans)", metrics['patients']['ages_aberrants'], delta_color="inverse")
st.metric("Doublons (nom+prénom)", metrics['patients']['doublons'], delta_color="inverse")
with col2:
st.markdown("#### 🏥 Cohérence Séjours")
st.metric("Dates inversées (sortie < entrée)", metrics['sejours']['dates_inversees'], delta_color="inverse")
st.metric("Gravité aberrante (<0 ou >5)", metrics['sejours']['gravite_aberrante'], delta_color="inverse")
st.metric("Équipement manquant", metrics['sejours']['equipement_manquant'], delta_color="inverse")
fig_service = plot_service_quality_heatmap(analyzer)
if fig_service: st.plotly_chart(fig_service, use_container_width=True)
st.divider()
# ==================================================
# ========== 3️⃣ ANALYSE DES ANOMALIES =============
# ==================================================
st.subheader("3️⃣ Analyse Détaillée des Anomalies")
tab1, tab2, tab3, tab4 = st.tabs(["📋 Patients", "🏥 Séjours", "⚕️ Ressources", "📦 Stock"])
for tab, df_label in [(tab1, 'pat'), (tab2, 'sej'), (tab3, 'res'), (tab4, 'stock')]:
with tab:
anomaly_details = analyzer.get_anomalies_by_type(df_label)
if not anomaly_details.empty:
st.dataframe(anomaly_details, use_container_width=True)
fig = px.pie(anomaly_details, values='Nombre', names='Type Anomalie', title=f"Répartition des Types d'Anomalies")
st.plotly_chart(fig, use_container_width=True)
else:
st.success(f"✅ Aucune anomalie détectée")
fig_temporal = plot_temporal_anomalies(analyzer)
if fig_temporal: st.plotly_chart(fig_temporal, use_container_width=True)
st.divider()
# ==================================================
# ========== 4️⃣ SPÉCIFICITÉS HOSPITALIÈRES ========
# ==================================================
st.subheader("4️⃣ Analyses Spécifiques Hospitalières")
col1, col2 = st.columns(2)
with col1:
st.markdown("#### 🛏️ Ressources")
if analyzer.df_res is not None:
st.metric("Cas de suroccupation", metrics['ressources']['suroccupation'], delta_color="inverse")
if 'service_code' in analyzer.df_res.columns and 'taux_occupation' in analyzer.df_res.columns:
taux_moy = analyzer.df_res.groupby('service_code')['taux_occupation'].mean().reset_index()
taux_moy.columns = ['Service', 'Taux Occupation Moyen']
fig = px.bar(taux_moy.head(10), x='Service', y='Taux Occupation Moyen', title="Top 10 Services - Taux Occupation", color='Taux Occupation Moyen', color_continuous_scale='RdYlGn_r')
st.plotly_chart(fig, use_container_width=True)
with col2:
st.markdown("#### 📦 Stock")
st.metric("Alertes stock actives", metrics['stock']['alertes'], delta_color="inverse")
if analyzer.df_stock is not None and 'alerte_stock' in analyzer.df_stock.columns:
alertes = analyzer.df_stock[analyzer.df_stock['alerte_stock'].astype(str) == '1']
if not alertes.empty and 'materiel_code' in alertes.columns:
alert_counts = alertes['materiel_code'].value_counts().reset_index()
alert_counts.columns = ['Matériel', 'Nombre Alertes']
fig = px.bar(alert_counts.head(10), x='Matériel', y='Nombre Alertes', title="Top 10 Matériels en Alerte", color='Nombre Alertes', color_continuous_scale='Oranges')
st.plotly_chart(fig, use_container_width=True)
st.divider()
# ==================================================
# ========== 5️⃣ RÉSUMÉ EXÉCUTIF ===================
# ==================================================
st.subheader("5️⃣ Résumé Exécutif Data Quality")
col1, col2, col3 = st.columns(3)
col1.metric("Score de Complétude", f"{scores['completeness']:.2f}%")
col2.metric("Score d'Exactitude", f"{scores['accuracy']:.2f}%")
global_score = scores['global']
if global_score >= 95: status, color = "🟢 EXCELLENT", "green"
elif global_score >= 85: status, color = "🟡 BON", "blue"
elif global_score >= 70: status, color = "🟠 MOYEN", "orange"
else: status, color = "🔴 CRITIQUE", "red"
col3.markdown(f"**Score Global**")
col3.markdown(f"<h2 style='color: {color};'>{global_score:.2f}%</h2>", unsafe_allow_html=True)
st.markdown(f"### {status}")
st.progress(global_score / 100)
st.markdown("#### 💡 Recommandations")
recommendations = []
if metrics['patients']['anomalies'] > 0: recommendations.append(f"⚠️ **{metrics['patients']['anomalies']} anomalies patients** détectées")
if metrics['sejours']['dates_inversees'] > 0: recommendations.append(f"🔴 **{metrics['sejours']['dates_inversees']} séjours avec dates inversées**")
if metrics['ressources']['suroccupation'] > 0: recommendations.append(f"🚨 **{metrics['ressources']['suroccupation']} cas de suroccupation**")
if metrics['stock']['alertes'] > 0: recommendations.append(f"📦 **{metrics['stock']['alertes']} alertes stock**")
if global_score < 85: recommendations.append("📊 **Score global < 85%** - Plan Data Quality requis")
if recommendations:
for rec in recommendations: st.markdown(rec)
else: st.success("✅ Qualité des données satisfaisante")
st.divider()
# ==================================================
# ========== 6️⃣ EXPORT DES RÉSULTATS ==============
# ==================================================
st.subheader("6️⃣ Export et Documentation")
report_data = {'Métrique': [], 'Valeur': []}
for category, cat_metrics in analyzer.metrics.items():
for metric_name, value in cat_metrics.items():
report_data['Métrique'].append(f"{category}_{metric_name}")
report_data['Valeur'].append(value)
report_data['Métrique'].extend(['score_completeness', 'score_accuracy', 'score_global'])
report_data['Valeur'].extend([scores['completeness'], scores['accuracy'], scores['global']])
report_df = pd.DataFrame(report_data)
col1, col2 = st.columns(2)
with col1:
csv = report_df.to_csv(index=False).encode('utf-8')
st.download_button(label="📥 Télécharger Rapport DQ (CSV)", data=csv, file_name=f'dq_report_{datetime.now().strftime("%Y%m%d")}.csv', mime='text/csv')
with col2:
if metrics['patients']['anomalies'] > 0 and analyzer.df_pat is not None:
anomalies_df = analyzer.df_pat[analyzer.df_pat['dq_is_anomaly'] == 1][['patient_id', 'nom', 'prenom', 'dq_anomaly_type']]
anomalies_csv = anomalies_df.to_csv(index=False).encode('utf-8')
st.download_button(label="📥 Télécharger Anomalies Patients (CSV)", data=anomalies_csv, file_name=f'anomalies_pat_{datetime.now().strftime("%Y%m%d")}.csv', mime='text/csv')
if __name__ == "__main__":
show_dq_dashboard()