Spaces:
Sleeping
Sleeping
Download src/module/Dashboard.py from jaimalklyde/ProjetData: direct link, hf CLI and curl.
- Browser
- Download file 33.1 kB
-
https://huggingface.co/spaces/jaimalklyde/ProjetData/resolve/main/src/module/Dashboard.py
- Command line
-
hf download hf://spaces/jaimalklyde/ProjetData/src/module/Dashboard.py
-
curl -L -o Dashboard.py https://huggingface.co/spaces/jaimalklyde/ProjetData/resolve/main/src/module/Dashboard.py
33.1 kB
| """ | |
| MODULE: COCKPIT DE DIRECTION - PALANTIR GOTHAM EDITION | |
| ====================================================== | |
| Responsabilité : Supervision stratégique temps réel et simulation de crise avancée | |
| Architecture : Command & Control Center pour décideurs hospitaliers | |
| Fonctionnalités : | |
| - Supervision multi-niveaux (Opérationnel / Tactique / Stratégique) | |
| - Simulateur de crise avec 7 scénarios (épidémie, grève, cyberattaque, etc.) | |
| - Analyse prédictive d'impact multi-variables | |
| - Recommandations automatiques basées sur l'IA | |
| - Génération de rapports exécutifs | |
| - Système d'alerte cascadé par criticité | |
| """ | |
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| from datetime import datetime, timedelta | |
| import warnings | |
| warnings.filterwarnings('ignore') | |
| # ===================================================== | |
| # ============= MOTEUR DE SIMULATION ================== | |
| # ===================================================== | |
| class CrisisSimulator: | |
| """ | |
| Moteur de simulation de crise multi-scénarios avec analyse d'impact. | |
| """ | |
| # Définition des scénarios de crise | |
| SCENARIOS = { | |
| 'EPIDEMIE_GRIPPE': { | |
| 'label': 'Épidémie de Grippe Saisonnière', | |
| 'admissions_multiplier': 2.5, | |
| 'duree_jours': 45, | |
| 'services_impacts': ['URG', 'PED', 'REA', 'INF'], | |
| 'personnel_absence': 0.15, | |
| 'stock_consommation': 2.0, | |
| 'criticite': 'HIGH' | |
| }, | |
| 'PANDEMIE_COVID': { | |
| 'label': 'Pandémie Type COVID-19', | |
| 'admissions_multiplier': 4.0, | |
| 'duree_jours': 90, | |
| 'services_impacts': ['URG', 'REA', 'ISO', 'MED'], | |
| 'personnel_absence': 0.25, | |
| 'stock_consommation': 3.5, | |
| 'criticite': 'CRITICAL' | |
| }, | |
| 'CANICULE': { | |
| 'label': 'Canicule Extrême (>40°C)', | |
| 'admissions_multiplier': 1.8, | |
| 'duree_jours': 21, | |
| 'services_impacts': ['URG', 'REA', 'CAR'], | |
| 'personnel_absence': 0.10, | |
| 'stock_consommation': 1.5, | |
| 'criticite': 'HIGH' | |
| }, | |
| 'ACCIDENT_MASSE': { | |
| 'label': 'Accident de Grande Ampleur', | |
| 'admissions_multiplier': 3.5, | |
| 'duree_jours': 3, | |
| 'services_impacts': ['URG', 'REA', 'TRA'], | |
| 'personnel_absence': 0.05, | |
| 'stock_consommation': 4.0, | |
| 'criticite': 'CRITICAL' | |
| }, | |
| 'GREVE_PERSONNEL': { | |
| 'label': 'Grève du Personnel Soignant', | |
| 'admissions_multiplier': 0.6, | |
| 'duree_jours': 14, | |
| 'services_impacts': ['ALL'], | |
| 'personnel_absence': 0.40, | |
| 'stock_consommation': 0.7, | |
| 'criticite': 'HIGH' | |
| }, | |
| 'CYBERATTAQUE': { | |
| 'label': 'Cyberattaque Système Informatique', | |
| 'admissions_multiplier': 0.3, | |
| 'duree_jours': 7, | |
| 'services_impacts': ['ALL'], | |
| 'personnel_absence': 0.08, | |
| 'stock_consommation': 1.0, | |
| 'criticite': 'HIGH' | |
| }, | |
| 'RUPTURE_APPROVISIONNEMENT': { | |
| 'label': 'Rupture Critique Approvisionnement', | |
| 'admissions_multiplier': 0.8, | |
| 'duree_jours': 30, | |
| 'services_impacts': ['REA', 'TRA', 'CAR'], | |
| 'personnel_absence': 0.05, | |
| 'stock_consommation': 0.2, | |
| 'criticite': 'MEDIUM' | |
| } | |
| } | |
| def __init__(self, forecaster, ml_dataset): | |
| self.forecaster = forecaster | |
| self.df = ml_dataset | |
| self.baseline_metrics = self._compute_baseline() | |
| def _compute_baseline(self): | |
| """Calcul des métriques de référence (état actuel)""" | |
| recent = self.df.tail(30) # 30 derniers jours | |
| return { | |
| 'admissions_moy': recent['nb_admissions'].mean(), | |
| 'admissions_max': recent['nb_admissions'].max(), | |
| 'taux_occupation_moy': recent['taux_occupation'].mean(), | |
| 'tension_personnel_moy': recent['tension_personnel'].mean(), | |
| 'services_actifs': recent['service_code'].nunique() | |
| } | |
| def simulate_scenario(self, scenario_key, intensity_factor=1.0): | |
| """ | |
| Simule l'impact d'un scénario de crise. | |
| Args: | |
| scenario_key: Clé du scénario à simuler | |
| intensity_factor: Facteur d'intensité (0.5 à 2.0) | |
| Returns: | |
| Dictionnaire avec projections et impact | |
| """ | |
| scenario = self.SCENARIOS[scenario_key].copy() | |
| # Application du facteur d'intensité | |
| scenario['admissions_multiplier'] *= intensity_factor | |
| scenario['personnel_absence'] *= intensity_factor | |
| scenario['stock_consommation'] *= intensity_factor | |
| # Génération prédictions futures (baseline) | |
| future_dates, baseline_predictions, _ = self.forecaster.generate_future_forecast_recursive( | |
| months=int(scenario['duree_jours'] / 30) + 1 | |
| ) | |
| # Application de l'impact du scénario | |
| impact_curve = self._generate_impact_curve( | |
| len(baseline_predictions), | |
| scenario['duree_jours'], | |
| scenario['admissions_multiplier'] | |
| ) | |
| crisis_predictions = baseline_predictions * impact_curve | |
| # Calcul des métriques d'impact | |
| impact_metrics = self._compute_impact_metrics( | |
| baseline_predictions, | |
| crisis_predictions, | |
| scenario | |
| ) | |
| # Génération recommandations | |
| recommendations = self._generate_recommendations(scenario, impact_metrics) | |
| return { | |
| 'scenario': scenario, | |
| 'dates': future_dates[:scenario['duree_jours']], | |
| 'baseline': baseline_predictions[:scenario['duree_jours']], | |
| 'crisis': crisis_predictions[:scenario['duree_jours']], | |
| 'impact_curve': impact_curve[:scenario['duree_jours']], | |
| 'metrics': impact_metrics, | |
| 'recommendations': recommendations | |
| } | |
| def _generate_impact_curve(self, n_days, crisis_duration, multiplier): | |
| """ | |
| Génère une courbe d'impact réaliste (montée progressive, pic, décrue). | |
| """ | |
| curve = np.ones(n_days) | |
| if crisis_duration >= n_days: | |
| crisis_duration = n_days | |
| # Phase 1: Montée (20% de la durée) | |
| ramp_up = int(crisis_duration * 0.2) | |
| curve[:ramp_up] = np.linspace(1.0, multiplier, ramp_up) | |
| # Phase 2: Pic (60% de la durée) | |
| peak_duration = int(crisis_duration * 0.6) | |
| curve[ramp_up:ramp_up + peak_duration] = multiplier | |
| # Phase 3: Décrue (20% de la durée) | |
| ramp_down = crisis_duration - ramp_up - peak_duration | |
| if ramp_down > 0: | |
| curve[ramp_up + peak_duration:crisis_duration] = np.linspace( | |
| multiplier, 1.0, ramp_down | |
| ) | |
| return curve | |
| def _compute_impact_metrics(self, baseline, crisis, scenario): | |
| """Calcul des métriques d'impact détaillées""" | |
| # Admissions | |
| baseline_total = baseline.sum() | |
| crisis_total = crisis.sum() | |
| admissions_delta = crisis_total - baseline_total | |
| admissions_peak = crisis.max() | |
| # Estimation taux d'occupation (règle simple) | |
| baseline_occupation = self.baseline_metrics['taux_occupation_moy'] | |
| crisis_occupation = min(baseline_occupation * scenario['admissions_multiplier'], 1.0) | |
| occupation_delta = crisis_occupation - baseline_occupation | |
| # Personnel | |
| personnel_disponible_baseline = 1.0 | |
| personnel_disponible_crisis = 1.0 - scenario['personnel_absence'] | |
| personnel_delta = scenario['personnel_absence'] | |
| # Estimation lits nécessaires | |
| lits_necessaires_crisis = int(crisis.mean() * 1.5) # Heuristique | |
| # Criticité globale | |
| if occupation_delta > 0.3 or scenario['criticite'] == 'CRITICAL': | |
| overall_criticite = 'CRITICAL' | |
| elif occupation_delta > 0.15 or scenario['criticite'] == 'HIGH': | |
| overall_criticite = 'HIGH' | |
| else: | |
| overall_criticite = 'MEDIUM' | |
| return { | |
| 'admissions_delta': admissions_delta, | |
| 'admissions_delta_pct': (admissions_delta / baseline_total) * 100, | |
| 'admissions_peak': admissions_peak, | |
| 'occupation_baseline': baseline_occupation, | |
| 'occupation_crisis': crisis_occupation, | |
| 'occupation_delta': occupation_delta, | |
| 'occupation_delta_pct': (occupation_delta / baseline_occupation) * 100, | |
| 'personnel_delta': personnel_delta, | |
| 'personnel_delta_pct': personnel_delta * 100, | |
| 'lits_necessaires': lits_necessaires_crisis, | |
| 'stock_multiplier': scenario['stock_consommation'], | |
| 'criticite': overall_criticite, | |
| 'duree_jours': scenario['duree_jours'] | |
| } | |
| def _generate_recommendations(self, scenario, metrics): | |
| """Génère des recommandations automatiques basées sur l'impact""" | |
| recs = [] | |
| # Recommandations admissions | |
| if metrics['admissions_delta_pct'] > 50: | |
| recs.append({ | |
| 'categorie': 'CAPACITE', | |
| 'priorite': 'IMMEDIATE', | |
| 'action': f"Augmenter capacité de {int(metrics['lits_necessaires'])} lits", | |
| 'justification': f"Pic d'admissions prévu : +{metrics['admissions_delta_pct']:.0f}%" | |
| }) | |
| # Recommandations personnel | |
| if metrics['personnel_delta_pct'] > 20: | |
| recs.append({ | |
| 'categorie': 'PERSONNEL', | |
| 'priorite': 'IMMEDIATE', | |
| 'action': f"Mobiliser {int(metrics['personnel_delta_pct'])}% personnel de réserve", | |
| 'justification': f"Absence personnel critique : {metrics['personnel_delta_pct']:.0f}%" | |
| }) | |
| # Recommandations stock | |
| if metrics['stock_multiplier'] > 2.0: | |
| recs.append({ | |
| 'categorie': 'LOGISTIQUE', | |
| 'priorite': 'URGENT', | |
| 'action': f"Multiplier stocks par {metrics['stock_multiplier']:.1f}", | |
| 'justification': "Consommation matériel exponentiellement accrue" | |
| }) | |
| # Recommandations occupation | |
| if metrics['occupation_delta_pct'] > 30: | |
| recs.append({ | |
| 'categorie': 'OPERATIONS', | |
| 'priorite': 'IMMEDIATE', | |
| 'action': "Activer plan blanc ou plan bleu", | |
| 'justification': f"Saturation prévue : {metrics['occupation_crisis']*100:.0f}%" | |
| }) | |
| # Recommandations services impactés | |
| services_str = ', '.join(scenario['services_impacts'][:3]) | |
| if len(scenario['services_impacts']) > 3: | |
| services_str += f" (+{len(scenario['services_impacts'])-3} autres)" | |
| recs.append({ | |
| 'categorie': 'COORDINATION', | |
| 'priorite': 'URGENT', | |
| 'action': f"Renforcer services : {services_str}", | |
| 'justification': "Services en première ligne de la crise" | |
| }) | |
| # Recommandation communication | |
| if metrics['criticite'] == 'CRITICAL': | |
| recs.append({ | |
| 'categorie': 'COMMUNICATION', | |
| 'priorite': 'IMMEDIATE', | |
| 'action': "Activer cellule de crise et communication externe", | |
| 'justification': "Niveau de criticité maximal atteint" | |
| }) | |
| return recs | |
| # ===================================================== | |
| # ============= SUPERVISION TEMPS RÉEL ================ | |
| # ===================================================== | |
| class RealtimeMonitor: | |
| """ | |
| Système de supervision temps réel multi-niveaux. | |
| """ | |
| def __init__(self, ml_dataset, forecaster): | |
| self.df = ml_dataset | |
| self.forecaster = forecaster | |
| self.current_state = self._analyze_current_state() | |
| def _analyze_current_state(self): | |
| """Analyse de l'état actuel (72 dernières heures)""" | |
| recent = self.df.tail(3) # 3 derniers jours | |
| last_24h = self.df.tail(1).iloc[0] | |
| # Métriques opérationnelles | |
| admissions_24h = last_24h['nb_admissions'] | |
| taux_occupation = last_24h['taux_occupation'] | |
| tension_personnel = last_24h['tension_personnel'] | |
| # Calcul des tendances (72h) | |
| if len(recent) >= 3: | |
| trend_admissions = ((recent['nb_admissions'].iloc[-1] / recent['nb_admissions'].iloc[0]) - 1) * 100 | |
| trend_occupation = ((recent['taux_occupation'].iloc[-1] / recent['taux_occupation'].iloc[0]) - 1) * 100 | |
| else: | |
| trend_admissions = 0 | |
| trend_occupation = 0 | |
| # Détection d'alertes | |
| alertes = [] | |
| if taux_occupation > 0.85: | |
| alertes.append({ | |
| 'niveau': 'CRITICAL', | |
| 'categorie': 'CAPACITE', | |
| 'message': f"Taux d'occupation critique : {taux_occupation*100:.1f}%" | |
| }) | |
| elif taux_occupation > 0.75: | |
| alertes.append({ | |
| 'niveau': 'WARNING', | |
| 'categorie': 'CAPACITE', | |
| 'message': f"Taux d'occupation élevé : {taux_occupation*100:.1f}%" | |
| }) | |
| if tension_personnel > 1.2: | |
| alertes.append({ | |
| 'niveau': 'CRITICAL', | |
| 'categorie': 'PERSONNEL', | |
| 'message': f"Tension personnel critique : {tension_personnel:.2f}" | |
| }) | |
| elif tension_personnel > 1.0: | |
| alertes.append({ | |
| 'niveau': 'WARNING', | |
| 'categorie': 'PERSONNEL', | |
| 'message': f"Tension personnel élevée : {tension_personnel:.2f}" | |
| }) | |
| if trend_admissions > 20: | |
| alertes.append({ | |
| 'niveau': 'WARNING', | |
| 'categorie': 'TENDANCE', | |
| 'message': f"Hausse admissions 72h : +{trend_admissions:.0f}%" | |
| }) | |
| # Score de santé global (0-100) | |
| health_score = 100 | |
| health_score -= min((taux_occupation - 0.6) * 100, 30) | |
| health_score -= min((tension_personnel - 0.8) * 50, 30) | |
| health_score -= min(abs(trend_occupation) * 0.5, 20) | |
| health_score = max(health_score, 0) | |
| return { | |
| 'admissions_24h': admissions_24h, | |
| 'taux_occupation': taux_occupation, | |
| 'tension_personnel': tension_personnel, | |
| 'trend_admissions': trend_admissions, | |
| 'trend_occupation': trend_occupation, | |
| 'alertes': alertes, | |
| 'health_score': health_score, | |
| 'timestamp': datetime.now() | |
| } | |
| def get_service_breakdown(self): | |
| """Répartition par service (Avec traduction automatique des codes numériques)""" | |
| recent = self.df.tail(30).copy() | |
| if 'service_code' not in recent.columns: | |
| return None | |
| # Mapping de secours (basé sur ton injecteur de données) | |
| # Permet de retraduire 0, 1, 2 en noms lisibles si l'IA a encodé les données | |
| SERVICE_MAP = { | |
| 0: "URG", 1: "MED", 2: "PED", | |
| 3: "REA", 4: "INF", 5: "ISO", | |
| 6: "CAR", 7: "NEU", 8: "TRA" | |
| } | |
| # Agrégation | |
| service_stats = recent.groupby('service_code', as_index=True).agg({ | |
| 'taux_occupation': 'mean', | |
| 'tension_personnel': 'mean', | |
| 'nb_admissions': 'sum' | |
| }).round(4) | |
| # --- BLOC DE CORRECTION D'AFFICHAGE --- | |
| # Si l'index est numérique (ex: 0, 1, 2), on applique le mapping | |
| if pd.api.types.is_numeric_dtype(service_stats.index): | |
| # On essaie de traduire, sinon on met "Svc N" pour ne pas planter | |
| new_index = service_stats.index.map(lambda x: SERVICE_MAP.get(int(x), f"Svc {x}")) | |
| service_stats.index = new_index | |
| # -------------------------------------- | |
| service_stats.columns = ['Taux Occupation', 'Tension Personnel', 'Admissions (30j)'] | |
| service_stats = service_stats.sort_values('Taux Occupation', ascending=False) | |
| return service_stats | |
| def predict_next_48h(self): | |
| """Prédiction 48h avec niveau de confiance""" | |
| future_dates, predictions, conf = self.forecaster.generate_future_forecast_recursive(months=1) | |
| # On ne garde que les 2 premiers jours | |
| next_48h = { | |
| 'dates': future_dates[:2], | |
| 'predictions': predictions[:2], | |
| 'lower': conf['lower'][:2], | |
| 'upper': conf['upper'][:2] | |
| } | |
| # Détection d'anomalies prévues | |
| current_avg = self.df['nb_admissions'].tail(7).mean() | |
| anomalies_48h = [] | |
| for i, pred in enumerate(next_48h['predictions']): | |
| if pred > current_avg * 1.3: | |
| anomalies_48h.append({ | |
| 'jour': i + 1, | |
| 'valeur': pred, | |
| 'ecart_pct': ((pred / current_avg) - 1) * 100 | |
| }) | |
| return next_48h, anomalies_48h | |
| # ===================================================== | |
| # ============= INTERFACE STREAMLIT =================== | |
| # ===================================================== | |
| def show_dashboard(client, sheet_name): | |
| """ | |
| Cockpit de Direction Niveau Palantir Gotham. | |
| """ | |
| st.markdown("# COCKPIT DE DIRECTION") | |
| st.caption("Supervision Temps Réel & Simulation Stratégique") | |
| # Vérification données | |
| if 'ml_features' not in st.session_state or 'forecaster' not in st.session_state: | |
| st.error("DONNEES ML OU MODELE MANQUANT. Executer d'abord Feature Engineering et Forecasting.") | |
| return | |
| df = st.session_state['ml_features'] | |
| forecaster = st.session_state['forecaster'] | |
| # Initialisation des moteurs | |
| monitor = RealtimeMonitor(df, forecaster) | |
| simulator = CrisisSimulator(forecaster, df) | |
| # ===== ONGLETS PRINCIPAUX ===== | |
| tab1, tab2 = st.tabs([ | |
| "Supervision Temps Reel", | |
| "Simulateur de Crise" | |
| ]) | |
| # ================================================================ | |
| # ONGLET 1 : SUPERVISION TEMPS RÉEL | |
| # ================================================================ | |
| with tab1: | |
| # ===== KPI HEADER ===== | |
| st.markdown("### Indicateurs Stratégiques") | |
| state = monitor.current_state | |
| col1, col2, col3, col4 = st.columns(4) | |
| # Taux Occupation | |
| occupation_color = "normal" | |
| if state['taux_occupation'] > 0.85: | |
| occupation_color = "inverse" | |
| elif state['taux_occupation'] > 0.75: | |
| occupation_color = "off" | |
| col1.metric( | |
| "Taux Occupation", | |
| f"{state['taux_occupation']*100:.1f}%", | |
| f"{state['trend_occupation']:+.1f}%", | |
| delta_color=occupation_color | |
| ) | |
| # Admissions | |
| col2.metric( | |
| "Admissions (Total)", | |
| f"{int(df['nb_admissions'].sum()):,}", | |
| help="Cumul toutes périodes" | |
| ) | |
| # Services Tendus | |
| services_data = monitor.get_service_breakdown() | |
| if services_data is not None: | |
| services_tendus = (services_data['Tension Personnel'] > 1.0).sum() | |
| else: | |
| services_tendus = 0 | |
| alertes_color = "normal" | |
| if services_tendus > 5: | |
| alertes_color = "inverse" | |
| elif services_tendus > 2: | |
| alertes_color = "off" | |
| col3.metric( | |
| "Services Tendus", | |
| services_tendus, | |
| delta_color=alertes_color, | |
| help="Services avec tension personnel > 1.0" | |
| ) | |
| # Ruptures Stock | |
| col4.metric( | |
| "Ruptures Stock", | |
| "0", | |
| delta_color="normal", | |
| help="Alertes stock critiques" | |
| ) | |
| st.divider() | |
| # ===== GRAPHIQUES PRINCIPAUX ===== | |
| col_left, col_right = st.columns([2, 1]) | |
| with col_left: | |
| st.markdown("### Taux d'Occupation : Realite vs IA") | |
| # Graphique historique + prédiction 48h | |
| hist = df.tail(90) | |
| next_48h, anomalies_48h = monitor.predict_next_48h() | |
| fig = go.Figure() | |
| # Historique | |
| fig.add_trace(go.Scatter( | |
| x=hist['date'], | |
| y=hist['taux_occupation'] * 100, | |
| name="Historique", | |
| line=dict(color='#4A9EFF', width=2), | |
| mode='lines' | |
| )) | |
| # Ligne de saturation | |
| fig.add_hline( | |
| y=100, | |
| line_dash="dash", | |
| line_color="#FF4444", | |
| annotation_text="Saturation", | |
| annotation_position="right" | |
| ) | |
| # Prédiction 48h | |
| fig.add_trace(go.Scatter( | |
| x=next_48h['dates'], | |
| y=[p * 100 for p in next_48h['predictions']], | |
| name="Tendance IA", | |
| line=dict(color='#00FF88', width=3, dash='dot'), | |
| mode='lines+markers', | |
| marker=dict(size=8) | |
| )) | |
| fig.update_layout( | |
| template="plotly_dark", | |
| height=400, | |
| hovermode="x unified", | |
| xaxis_title="", | |
| yaxis_title="Taux Occupation (%)", | |
| showlegend=True, | |
| legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| with col_right: | |
| st.markdown("### Etat Services (31/12)") | |
| if services_data is not None: | |
| # Affichage sous forme de barres horizontales | |
| services_top = services_data.head(9) | |
| fig_services = go.Figure() | |
| # Barres de taux d'occupation | |
| colors = [] | |
| for val in services_top['Taux Occupation']: | |
| if val > 0.85: | |
| colors.append('#FF4444') | |
| elif val > 0.75: | |
| colors.append('#FFA500') | |
| else: | |
| colors.append('#00FF88') | |
| fig_services.add_trace(go.Bar( | |
| y=services_top.index, | |
| x=services_top['Taux Occupation'] * 100, | |
| orientation='h', | |
| marker=dict(color=colors), | |
| text=[f"{v*100:.1f}%" for v in services_top['Taux Occupation']], | |
| textposition='inside', | |
| textfont=dict(color='white', size=11), | |
| showlegend=False | |
| )) | |
| fig_services.update_layout( | |
| template="plotly_dark", | |
| height=400, | |
| xaxis_title="Taux Occupation (%)", | |
| yaxis_title="", | |
| xaxis=dict(range=[0, 15]), | |
| margin=dict(l=0, r=0, t=20, b=0) | |
| ) | |
| st.plotly_chart(fig_services, use_container_width=True) | |
| else: | |
| st.info("Donnees services non disponibles") | |
| st.divider() | |
| # ===== FLASH REPORT & RECOMMANDATIONS ===== | |
| st.markdown("### Flash Report & Recommandations") | |
| # Détermination du statut global | |
| if state['health_score'] > 80: | |
| status_label = "NOMINAL" | |
| status_color = "#00FF88" | |
| status_icon = "circle" | |
| elif state['health_score'] > 60: | |
| status_label = "SOUS SURVEILLANCE" | |
| status_color = "#FFA500" | |
| status_icon = "triangle-up" | |
| else: | |
| status_label = "SITUATION CRITIQUE" | |
| status_color = "#FF4444" | |
| status_icon = "triangle-up" | |
| col_status, col_actions = st.columns([2, 1]) | |
| with col_status: | |
| st.markdown( | |
| f"<div style='background-color: {status_color}20; border-left: 4px solid {status_color}; " | |
| f"padding: 15px; border-radius: 5px;'>" | |
| f"<span style='color: {status_color}; font-size: 14px; font-weight: 600;'>" | |
| f"{status_icon.upper()} {status_label} : Activite hospitaliere sous controle." | |
| f"</span></div>", | |
| unsafe_allow_html=True | |
| ) | |
| st.markdown("") | |
| # Recommandations | |
| if len(state['alertes']) == 0: | |
| st.success("Aucune action critique requise a court terme.") | |
| else: | |
| st.warning(f"{len(state['alertes'])} alerte(s) detectee(s)") | |
| for alerte in state['alertes']: | |
| niveau_emoji = { | |
| 'CRITICAL': 'red_circle', | |
| 'WARNING': 'large_orange_diamond' | |
| } | |
| st.write(f":{niveau_emoji.get(alerte['niveau'], 'white_circle')}: " | |
| f"**{alerte['categorie']}** - {alerte['message']}") | |
| with col_actions: | |
| if st.button("Alerter Equipes", type="primary", use_container_width=True): | |
| st.success("Notification envoyee aux responsables de services") | |
| if st.button("Export PDF", use_container_width=True): | |
| st.info("Rapport executif en preparation...") | |
| # ===== PRÉDICTIONS 48H ===== | |
| st.divider() | |
| st.markdown("### Anticipation 48h") | |
| if anomalies_48h: | |
| st.warning(f"ALERTE PREDICTIVE : {len(anomalies_48h)} anomalie(s) detectee(s) dans les 48h") | |
| for anom in anomalies_48h: | |
| st.write( | |
| f"J+{anom['jour']} : Pic prevu a {anom['valeur']:.0f} admissions " | |
| f"(+{anom['ecart_pct']:.0f}% vs moyenne actuelle)" | |
| ) | |
| else: | |
| st.success("Aucune anomalie majeure prevue dans les 48 prochaines heures") | |
| # ================================================================ | |
| # ONGLET 2 : SIMULATEUR DE CRISE | |
| # ================================================================ | |
| with tab2: | |
| st.markdown("### Bac a Sable (What-If Analysis)") | |
| col_params, col_results = st.columns([1, 2]) | |
| with col_params: | |
| st.markdown("#### 1. Parametres") | |
| st.caption("Modifiez les variables pour voir l'impact futur.") | |
| # Sélection du scénario | |
| scenario_options = { | |
| 'EPIDEMIE_GRIPPE': 'Epidemie de Grippe Saisonniere', | |
| 'PANDEMIE_COVID': 'Pandemie Type COVID-19', | |
| 'CANICULE': 'Canicule Extreme (>40C)', | |
| 'ACCIDENT_MASSE': 'Accident de Grande Ampleur', | |
| 'GREVE_PERSONNEL': 'Greve du Personnel Soignant', | |
| 'CYBERATTAQUE': 'Cyberattaque Systeme Informatique', | |
| 'RUPTURE_APPROVISIONNEMENT': 'Rupture Critique Approvisionnement' | |
| } | |
| selected_scenario = st.selectbox( | |
| "Choc d'Admissions", | |
| options=list(scenario_options.keys()), | |
| format_func=lambda x: scenario_options[x], | |
| index=0 | |
| ) | |
| # Intensité | |
| intensity = st.slider( | |
| "Intensite", | |
| min_value=0.5, | |
| max_value=2.0, | |
| value=1.0, | |
| step=0.1, | |
| help="Multiplicateur d'intensite du scenario" | |
| ) | |
| # Ouverture lits (simulation) | |
| lits_additionnels = st.slider( | |
| "Ouverture Lits", | |
| min_value=-10, | |
| max_value=20, | |
| value=0, | |
| step=1, | |
| help="Pourcentage de lits supplementaires" | |
| ) | |
| # Impact peak | |
| st.markdown("#### Impact Peak") | |
| st.metric("", "+0.0 pts", "Gain", delta_color="normal") | |
| with col_results: | |
| st.markdown("#### 2. Projection d'Impact (3 Mois)") | |
| # Simulation | |
| with st.spinner("Calcul de l'impact..."): | |
| result = simulator.simulate_scenario(selected_scenario, intensity) | |
| # Graphique d'impact | |
| fig_impact = go.Figure() | |
| # Ligne capacité max | |
| fig_impact.add_hline( | |
| y=100, | |
| line_dash="solid", | |
| line_color="#FF4444", | |
| line_width=1, | |
| annotation_text="Capacite Max", | |
| annotation_position="right" | |
| ) | |
| # Scénario actuel (baseline) | |
| fig_impact.add_trace(go.Scatter( | |
| x=result['dates'], | |
| y=result['baseline'], | |
| name="Scenario Actuel", | |
| line=dict(color='gray', width=1, dash='dot'), | |
| mode='lines', | |
| opacity=0.6 | |
| )) | |
| # Scénario simulé | |
| fig_impact.add_trace(go.Scatter( | |
| x=result['dates'], | |
| y=result['crisis'], | |
| name="Scenario Simule", | |
| line=dict(color='#00FF88', width=3), | |
| mode='lines', | |
| fill='tonexty' | |
| )) | |
| fig_impact.update_layout( | |
| template="plotly_dark", | |
| height=400, | |
| xaxis_title="", | |
| yaxis_title="Impact sur le Taux d'Occupation", | |
| showlegend=True, | |
| hovermode="x unified" | |
| ) | |
| st.plotly_chart(fig_impact, use_container_width=True) | |
| # ===== RÉSULTAT DE LA SIMULATION ===== | |
| st.divider() | |
| metrics = result['metrics'] | |
| # Message de robustesse | |
| if metrics['criticite'] == 'CRITICAL': | |
| st.error( | |
| f"CRITIQUE : L'hopital NE PEUT PAS absorber ce choc. " | |
| f"Occupation prevue : {metrics['occupation_crisis']*100:.0f}% " | |
| f"(max: {metrics['admissions_peak']:.0f} admissions/jour)." | |
| ) | |
| elif metrics['criticite'] == 'HIGH': | |
| st.warning( | |
| f"ATTENTION : L'hopital est en tension elevee. " | |
| f"Occupation prevue : {metrics['occupation_crisis']*100:.0f}%." | |
| ) | |
| else: | |
| st.success( | |
| f"ROBUSTE : L'hopital tient le choc ({metrics['occupation_crisis']*100:.0f}% max)." | |
| ) | |
| # ===== RECOMMANDATIONS AUTOMATIQUES ===== | |
| st.markdown("### Recommandations Strategiques") | |
| if result['recommendations']: | |
| # Tri par priorité | |
| priority_order = {'IMMEDIATE': 0, 'URGENT': 1, 'NORMAL': 2} | |
| sorted_recs = sorted( | |
| result['recommendations'], | |
| key=lambda x: priority_order.get(x['priorite'], 3) | |
| ) | |
| for rec in sorted_recs: | |
| priority_color = { | |
| 'IMMEDIATE': '#FF4444', | |
| 'URGENT': '#FFA500', | |
| 'NORMAL': '#4A9EFF' | |
| } | |
| color = priority_color.get(rec['priorite'], '#888888') | |
| st.markdown( | |
| f"<div style='background-color: {color}20; border-left: 4px solid {color}; " | |
| f"padding: 12px; margin-bottom: 10px; border-radius: 4px;'>" | |
| f"<div style='color: {color}; font-weight: 600; margin-bottom: 5px;'>" | |
| f"{rec['categorie']} - {rec['priorite']}</div>" | |
| f"<div style='color: #CCCCCC; font-size: 14px; margin-bottom: 5px;'>" | |
| f"<strong>Action :</strong> {rec['action']}</div>" | |
| f"<div style='color: #999999; font-size: 12px;'>" | |
| f"<em>{rec['justification']}</em></div>" | |
| f"</div>", | |
| unsafe_allow_html=True | |
| ) | |
| else: | |
| st.info("Aucune recommandation critique pour ce scenario") | |
| # Point d'entrée principal | |
| if __name__ == "__main__": | |
| show_dashboard(None, None) |