ProjetData / src /module /Dashboard.py
jaimalklyde's picture
Update src/module/Dashboard.py
5374493 verified
Raw History Blame Contribute Delete
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)