Spaces:
Sleeping
Sleeping
Update src/module/Data_Exploration.py
Browse files- src/module/Data_Exploration.py +254 -133
src/module/Data_Exploration.py
CHANGED
|
@@ -1,200 +1,321 @@
|
|
| 1 |
"""
|
| 2 |
-
MODULE DATA EXPLORATION -
|
| 3 |
-
========================================
|
| 4 |
-
Responsabilité :
|
| 5 |
"""
|
| 6 |
import pandas as pd
|
| 7 |
import numpy as np
|
| 8 |
import streamlit as st
|
| 9 |
import plotly.express as px
|
| 10 |
import plotly.graph_objects as go
|
|
|
|
| 11 |
import gspread
|
| 12 |
-
from datetime import datetime
|
| 13 |
-
import json
|
| 14 |
-
import time
|
| 15 |
|
| 16 |
# --- DEBUG MARKER ---
|
| 17 |
-
print("✅ Module Data_Exploration
|
| 18 |
|
| 19 |
class DataExplorer:
|
| 20 |
-
"""
|
| 21 |
|
| 22 |
def __init__(self, gspread_client, sheet_name="bdd_mvp_faker"):
|
| 23 |
self.client = gspread_client
|
| 24 |
self.sheet_name = sheet_name
|
| 25 |
self.dataframes = {}
|
| 26 |
-
self.kpis = {}
|
| 27 |
|
| 28 |
def load_all_sheets(self):
|
| 29 |
-
"""Charge
|
| 30 |
try:
|
| 31 |
spreadsheet = self.client.open(self.sheet_name)
|
| 32 |
sheet_names = [worksheet.title for worksheet in spreadsheet.worksheets()]
|
| 33 |
|
| 34 |
-
|
|
|
|
| 35 |
|
| 36 |
-
for sheet_name in sheet_names:
|
| 37 |
-
# On ignore les feuilles de calcul temporaires ou masquées si besoin
|
| 38 |
worksheet = spreadsheet.worksheet(sheet_name)
|
| 39 |
data = worksheet.get_all_records()
|
| 40 |
|
| 41 |
if data:
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
|
|
|
| 45 |
return True
|
| 46 |
|
| 47 |
except Exception as e:
|
| 48 |
-
st.error(f"❌ Erreur GSheets : {e}")
|
| 49 |
return False
|
| 50 |
|
| 51 |
-
def
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
st.dataframe(df.head(50), use_container_width=True)
|
| 79 |
-
with tab2:
|
| 80 |
-
st.dataframe(df.describe(include='all'), use_container_width=True)
|
| 81 |
-
|
| 82 |
-
def analyze_patients(self):
|
| 83 |
-
df = self.get_dataframe('patients')
|
| 84 |
-
if df is None:
|
| 85 |
-
st.warning("Table 'patients' manquante")
|
| 86 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
with col1:
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
with col2:
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
else:
|
| 125 |
-
st.
|
| 126 |
-
|
| 127 |
-
def show_stock_alerts(self):
|
| 128 |
-
df_stock = self.get_dataframe('stock_materiel_jour') # Correction probable du nom ici selon tes fichiers précédents
|
| 129 |
-
# Si ça ne marche pas, remets 'materiel' si c'est le nom de ta table
|
| 130 |
-
if df_stock is None:
|
| 131 |
-
df_stock = self.get_dataframe('materiel')
|
| 132 |
-
|
| 133 |
-
if df_stock is not None and 'tension_stock' in df_stock.columns:
|
| 134 |
-
alerts = df_stock[df_stock['tension_stock'] > 0.8]
|
| 135 |
-
if not alerts.empty:
|
| 136 |
-
st.error(f"🚨 {len(alerts)} Matériels en rupture imminente")
|
| 137 |
-
st.dataframe(alerts, use_container_width=True)
|
| 138 |
-
else:
|
| 139 |
-
st.success("Stock sous contrôle.")
|
| 140 |
|
| 141 |
|
| 142 |
# ==============================================================================
|
| 143 |
-
# FONCTION PRINCIPALE
|
| 144 |
# ==============================================================================
|
| 145 |
def show_terminal(gspread_client, sheet_name="bdd_mvp_faker"):
|
| 146 |
-
"""
|
| 147 |
|
| 148 |
-
#
|
| 149 |
if 'data_explorer' not in st.session_state:
|
| 150 |
st.session_state.data_explorer = DataExplorer(gspread_client, sheet_name)
|
| 151 |
|
| 152 |
explorer = st.session_state.data_explorer
|
| 153 |
|
| 154 |
-
|
|
|
|
| 155 |
|
| 156 |
-
#
|
| 157 |
-
# Si les données ne sont pas chargées, on lance le chargement immédiatement
|
| 158 |
if not explorer.dataframes:
|
| 159 |
-
|
|
|
|
| 160 |
success = explorer.load_all_sheets()
|
| 161 |
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
|
| 170 |
-
#
|
| 171 |
else:
|
| 172 |
-
# Barre d'
|
| 173 |
-
|
| 174 |
-
with
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
with tabs[0]:
|
| 183 |
-
explorer.
|
| 184 |
-
explorer.calculate_kpis()
|
| 185 |
|
| 186 |
with tabs[1]:
|
| 187 |
-
explorer.
|
| 188 |
|
| 189 |
with tabs[2]:
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
explorer.analyze_data_quality()
|
| 193 |
-
with col2:
|
| 194 |
-
explorer.show_stock_alerts()
|
| 195 |
-
|
| 196 |
with tabs[3]:
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
MODULE DATA EXPLORATION - SENIOR DATA SCIENTIST EDITION
|
| 3 |
+
=======================================================
|
| 4 |
+
Responsabilité : EDA Avancée, Audit DQ, Analyse Temporelle & Stress-Tests
|
| 5 |
"""
|
| 6 |
import pandas as pd
|
| 7 |
import numpy as np
|
| 8 |
import streamlit as st
|
| 9 |
import plotly.express as px
|
| 10 |
import plotly.graph_objects as go
|
| 11 |
+
from plotly.subplots import make_subplots
|
| 12 |
import gspread
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
# --- DEBUG MARKER ---
|
| 15 |
+
print("✅ Module Data_Exploration (Senior Edition) chargé")
|
| 16 |
|
| 17 |
class DataExplorer:
|
| 18 |
+
"""Moteur d'analyse exploratoire avancé pour données hospitalières"""
|
| 19 |
|
| 20 |
def __init__(self, gspread_client, sheet_name="bdd_mvp_faker"):
|
| 21 |
self.client = gspread_client
|
| 22 |
self.sheet_name = sheet_name
|
| 23 |
self.dataframes = {}
|
|
|
|
| 24 |
|
| 25 |
def load_all_sheets(self):
|
| 26 |
+
"""Charge et pré-traite les données (typage dates, nettoyage basique)"""
|
| 27 |
try:
|
| 28 |
spreadsheet = self.client.open(self.sheet_name)
|
| 29 |
sheet_names = [worksheet.title for worksheet in spreadsheet.worksheets()]
|
| 30 |
|
| 31 |
+
progress_text = "Chargement des tables..."
|
| 32 |
+
my_bar = st.progress(0, text=progress_text)
|
| 33 |
|
| 34 |
+
for i, sheet_name in enumerate(sheet_names):
|
|
|
|
| 35 |
worksheet = spreadsheet.worksheet(sheet_name)
|
| 36 |
data = worksheet.get_all_records()
|
| 37 |
|
| 38 |
if data:
|
| 39 |
+
# Standardisation du nom (ex: "Ressources Jour" -> "ressources_jour")
|
| 40 |
+
std_name = sheet_name.lower().replace('é', 'e').replace(' ', '_').replace('-', '_').split('__')[0]
|
| 41 |
+
df = pd.DataFrame(data)
|
| 42 |
+
|
| 43 |
+
# --- PRÉ-TRAITEMENT AUTOMATIQUE ---
|
| 44 |
+
# 1. Conversion des dates
|
| 45 |
+
for col in df.columns:
|
| 46 |
+
if 'date' in col.lower():
|
| 47 |
+
df[col] = pd.to_datetime(df[col], errors='coerce')
|
| 48 |
+
|
| 49 |
+
# 2. Conversion numérique forcée pour les métriques clés
|
| 50 |
+
cols_numeric = ['age', 'lits_occupes', 'lits_total', 'tension_stock', 'nb_admissions', 'duree_sejour_jours']
|
| 51 |
+
for col in cols_numeric:
|
| 52 |
+
if col in df.columns:
|
| 53 |
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 54 |
+
|
| 55 |
+
self.dataframes[std_name] = df
|
| 56 |
+
|
| 57 |
+
my_bar.progress((i + 1) / len(sheet_names), text=f"Chargement : {sheet_name}")
|
| 58 |
|
| 59 |
+
my_bar.empty()
|
| 60 |
return True
|
| 61 |
|
| 62 |
except Exception as e:
|
| 63 |
+
st.error(f"❌ Erreur critique GSheets : {e}")
|
| 64 |
return False
|
| 65 |
|
| 66 |
+
def get_df(self, name):
|
| 67 |
+
"""Helper sécurisé pour récupérer un DF"""
|
| 68 |
+
# Gestion des variations de noms de fichiers (avec ou sans extension)
|
| 69 |
+
for key in self.dataframes.keys():
|
| 70 |
+
if name in key:
|
| 71 |
+
return self.dataframes[key]
|
| 72 |
+
return None
|
| 73 |
|
| 74 |
+
# ==========================================================================
|
| 75 |
+
# 1. AUDIT QUALITÉ (DQ) & INTÉGRITÉ
|
| 76 |
+
# ==========================================================================
|
| 77 |
+
def tab_audit_dq(self):
|
| 78 |
+
st.markdown("### 🛡️ Audit de Qualité & Intégrité des Données")
|
| 79 |
+
st.caption("Détection des anomalies structurelles et métier basées sur les hypothèses du projet.")
|
| 80 |
+
|
| 81 |
+
col1, col2 = st.columns(2)
|
| 82 |
+
|
| 83 |
+
# --- A. Analyse des Patients (Age & Doublons) ---
|
| 84 |
+
with col1:
|
| 85 |
+
st.markdown("#### 👥 Intégrité Patients")
|
| 86 |
+
df_pat = self.get_df('patients')
|
| 87 |
+
if df_pat is not None:
|
| 88 |
+
# 1. Doublons
|
| 89 |
+
doublons = df_pat.duplicated(subset=['nom', 'prenom', 'date_naissance']).sum()
|
| 90 |
+
|
| 91 |
+
# 2. Âges aberrants (Hypothèse: -5 ans ou > 120 ans)
|
| 92 |
+
age_aberrant = df_pat[(df_pat['age'] < 0) | (df_pat['age'] > 120)].shape[0]
|
| 93 |
+
|
| 94 |
+
# 3. Valeurs manquantes critiques
|
| 95 |
+
missing_ville = df_pat['ville'].isnull().sum()
|
| 96 |
+
|
| 97 |
+
# Viz KPI
|
| 98 |
+
kpi_col1, kpi_col2, kpi_col3 = st.columns(3)
|
| 99 |
+
kpi_col1.metric("Doublons stricts", doublons, delta_color="inverse")
|
| 100 |
+
kpi_col2.metric("Âges Aberrants", age_aberrant, delta_color="inverse")
|
| 101 |
+
kpi_col3.metric("Ville Manquante", missing_ville, delta_color="inverse")
|
| 102 |
+
|
| 103 |
+
# Graphique Distribution Age
|
| 104 |
+
fig_age = px.histogram(df_pat, x='age', nbins=50, title="Distribution des Âges (Détection Outliers)", color_discrete_sequence=['#58a6ff'])
|
| 105 |
+
st.plotly_chart(fig_age, use_container_width=True)
|
| 106 |
+
else:
|
| 107 |
+
st.warning("Table 'patients' introuvable.")
|
| 108 |
|
| 109 |
+
# --- B. Cohérence des Séjours ---
|
| 110 |
+
with col2:
|
| 111 |
+
st.markdown("#### 🏥 Cohérence Séjours")
|
| 112 |
+
df_sej = self.get_df('sejours')
|
| 113 |
+
if df_sej is not None:
|
| 114 |
+
# 1. Date Sortie < Date Entrée
|
| 115 |
+
incoherent_dates = df_sej[df_sej['date_sortie'] < df_sej['date_entree']].shape[0]
|
| 116 |
+
|
| 117 |
+
# 2. Gravité invalide (Hypothèse: 1-5, si >5 anomalie)
|
| 118 |
+
# On suppose que la colonne est 'gravite_1_5' ou similaire
|
| 119 |
+
gravite_col = next((c for c in df_sej.columns if 'gravit' in c), None)
|
| 120 |
+
gravite_issues = 0
|
| 121 |
+
if gravite_col:
|
| 122 |
+
gravite_issues = df_sej[(df_sej[gravite_col] < 1) | (df_sej[gravite_col] > 5)].shape[0]
|
| 123 |
+
|
| 124 |
+
st.metric("Dates Incohérentes (Sortie < Entrée)", incoherent_dates, delta_color="inverse")
|
| 125 |
+
st.metric("Score Gravité Hors Norme", gravite_issues, delta_color="inverse")
|
| 126 |
+
|
| 127 |
+
if incoherent_dates > 0:
|
| 128 |
+
st.error(f"🚨 Alerte : {incoherent_dates} séjours ont une date de fin antérieure au début !")
|
| 129 |
+
else:
|
| 130 |
+
st.warning("Table 'sejours' introuvable.")
|
| 131 |
+
|
| 132 |
+
# ==========================================================================
|
| 133 |
+
# 2. ANALYSE TEMPORELLE & FLUX (STL Like)
|
| 134 |
+
# ==========================================================================
|
| 135 |
+
def tab_temporal_flux(self):
|
| 136 |
+
st.markdown("### ⏳ Analyse Temporelle des Admissions")
|
| 137 |
+
st.caption("Décomposition de la tendance et de la saisonnalité pour identifier les cycles.")
|
| 138 |
|
| 139 |
+
df_adm = self.get_df('admissions')
|
| 140 |
+
if df_adm is None:
|
| 141 |
+
st.warning("Table 'admissions_jour' manquante.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
return
|
| 143 |
+
|
| 144 |
+
# Agrégation par jour (tous services confondus)
|
| 145 |
+
df_daily = df_adm.groupby('date')['nb_admissions'].sum().reset_index().sort_values('date')
|
| 146 |
+
|
| 147 |
+
# Calcul Moyenne Mobile (Trend)
|
| 148 |
+
df_daily['Trend (7j)'] = df_daily['nb_admissions'].rolling(window=7).mean()
|
| 149 |
+
df_daily['Trend (30j)'] = df_daily['nb_admissions'].rolling(window=30).mean()
|
| 150 |
+
|
| 151 |
+
# Viz 1: Flux Brut vs Tendance
|
| 152 |
+
fig_flux = go.Figure()
|
| 153 |
+
fig_flux.add_trace(go.Scatter(x=df_daily['date'], y=df_daily['nb_admissions'], mode='lines', name='Admissions Brutes', line=dict(color='rgba(88, 166, 255, 0.3)')))
|
| 154 |
+
fig_flux.add_trace(go.Scatter(x=df_daily['date'], y=df_daily['Trend (7j)'], mode='lines', name='Tendance Hebdo', line=dict(color='#00E676', width=2)))
|
| 155 |
+
fig_flux.add_trace(go.Scatter(x=df_daily['date'], y=df_daily['Trend (30j)'], mode='lines', name='Tendance Mensuelle', line=dict(color='#ff6b9d', width=2)))
|
| 156 |
+
|
| 157 |
+
fig_flux.update_layout(title="Flux d'Admissions & Lissage (Trend)", xaxis_title="Date", yaxis_title="Nb Admissions")
|
| 158 |
+
st.plotly_chart(fig_flux, use_container_width=True)
|
| 159 |
+
|
| 160 |
+
# Viz 2: Saisonnalité (Heatmap Jours/Mois)
|
| 161 |
+
col1, col2 = st.columns([1, 2])
|
| 162 |
+
with col1:
|
| 163 |
+
st.markdown("**Saisonnalité Hebdomadaire**")
|
| 164 |
+
df_daily['day_name'] = df_daily['date'].dt.day_name()
|
| 165 |
+
day_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
| 166 |
+
df_week = df_daily.groupby('day_name')['nb_admissions'].mean().reindex(day_order)
|
| 167 |
|
| 168 |
+
fig_week = px.bar(df_week, title="Moyenne par Jour", color=df_week.values, color_continuous_scale="Blues")
|
| 169 |
+
st.plotly_chart(fig_week, use_container_width=True)
|
| 170 |
+
|
| 171 |
+
# ==========================================================================
|
| 172 |
+
# 3. STRESS-TEST SCÉNARIOS & DMS
|
| 173 |
+
# ==========================================================================
|
| 174 |
+
def tab_stress_scenarios(self):
|
| 175 |
+
st.markdown("### 🌪️ Impact des Scénarios de Crise")
|
| 176 |
+
st.caption("Corrélation entre les types de scénarios (Covid, Grippe) et la tension hospitalière.")
|
| 177 |
+
|
| 178 |
+
df_res = self.get_df('ressources')
|
| 179 |
+
if df_res is None or 'scenario' not in df_res.columns:
|
| 180 |
+
st.warning("Données ressources ou scénarios manquantes.")
|
| 181 |
+
return
|
| 182 |
+
|
| 183 |
+
# 1. Boxplot Taux d'Occupation par Scénario
|
| 184 |
+
st.markdown("#### Saturation des Lits par Scénario")
|
| 185 |
+
# On filtre les scénarios 'Normal' pour voir le contraste
|
| 186 |
+
top_scenarios = df_res['scenario'].value_counts().head(5).index.tolist()
|
| 187 |
+
df_filtered = df_res[df_res['scenario'].isin(top_scenarios)]
|
| 188 |
+
|
| 189 |
+
fig_box = px.box(df_filtered, x='scenario', y='taux_occupation', color='scenario',
|
| 190 |
+
title="Distribution du Taux d'Occupation par Type de Crise",
|
| 191 |
+
points="outliers")
|
| 192 |
+
st.plotly_chart(fig_box, use_container_width=True)
|
| 193 |
+
|
| 194 |
+
# 2. Corrélation Tension Personnel vs Occupation
|
| 195 |
+
st.markdown("#### Corrélation : Occupation Lits vs Tension Personnel")
|
| 196 |
+
col1, col2 = st.columns([3, 1])
|
| 197 |
with col1:
|
| 198 |
+
fig_scatter = px.scatter(df_res, x='taux_occupation', y='tension_personnel',
|
| 199 |
+
color='service_code', size='lits_total',
|
| 200 |
+
hover_data=['date', 'scenario'],
|
| 201 |
+
title="Zone de Danger : Tension > 80%",
|
| 202 |
+
trendline="ols") # Nécessite statsmodels, sinon retirer trendline
|
| 203 |
+
|
| 204 |
+
# Ajout zones de danger
|
| 205 |
+
fig_scatter.add_hrect(y0=0.8, y1=1.5, line_width=0, fillcolor="red", opacity=0.1, annotation_text="Danger RH")
|
| 206 |
+
fig_scatter.add_vrect(x0=0.9, x1=1.5, line_width=0, fillcolor="orange", opacity=0.1, annotation_text="Saturation Lits")
|
| 207 |
+
|
| 208 |
+
st.plotly_chart(fig_scatter, use_container_width=True)
|
| 209 |
|
| 210 |
with col2:
|
| 211 |
+
st.info("""
|
| 212 |
+
**Interprétation :**
|
| 213 |
+
- **Cadran Haut-Droite** : Situation critique (Lits pleins + Manque personnel).
|
| 214 |
+
- **Points isolés** : Vérifier s'il s'agit d'événements exceptionnels ou d'erreurs de saisie.
|
| 215 |
+
""")
|
| 216 |
+
|
| 217 |
+
# ==========================================================================
|
| 218 |
+
# 4. LOGISTIQUE & LAG (STOCKS)
|
| 219 |
+
# ==========================================================================
|
| 220 |
+
def tab_logistics(self):
|
| 221 |
+
st.markdown("### 📦 Logistique & Ruptures de Stock")
|
| 222 |
+
st.caption("Identification des goulots d'étranglement matériels.")
|
| 223 |
+
|
| 224 |
+
df_stock = self.get_df('stock')
|
| 225 |
+
if df_stock is None:
|
| 226 |
+
st.warning("Table 'stock_materiel_jour' manquante.")
|
| 227 |
+
return
|
| 228 |
+
|
| 229 |
+
# Focus sur les matériels critiques
|
| 230 |
+
critical_items = ['CONSO_O2', 'EQP_RESP', 'CONSO_EPI']
|
| 231 |
+
df_crit = df_stock[df_stock['materiel_code'].isin(critical_items)]
|
| 232 |
+
|
| 233 |
+
# Viz : Tension Stock au cours du temps
|
| 234 |
+
fig_stock = px.line(df_crit, x='date', y='tension_stock', color='materiel_code',
|
| 235 |
+
title="Évolution de la Tension des Stocks Critiques",
|
| 236 |
+
facet_row='service_code', height=800)
|
| 237 |
+
|
| 238 |
+
# Seuil d'alerte visuel
|
| 239 |
+
fig_stock.add_hline(y=0.8, line_dash="dash", line_color="red", annotation_text="Seuil Critique (80%)")
|
| 240 |
+
|
| 241 |
+
st.plotly_chart(fig_stock, use_container_width=True)
|
| 242 |
+
|
| 243 |
+
# Top Alertes Actuelles
|
| 244 |
+
latest_date = df_stock['date'].max()
|
| 245 |
+
ruptures = df_stock[(df_stock['date'] == latest_date) & (df_stock['tension_stock'] > 0.9)]
|
| 246 |
+
|
| 247 |
+
if not ruptures.empty:
|
| 248 |
+
st.error(f"🚨 ALERTES CRITIQUES AU {latest_date.date()} :")
|
| 249 |
+
st.dataframe(ruptures[['service_code', 'materiel_code', 'stock_disponible', 'tension_stock']], use_container_width=True)
|
| 250 |
else:
|
| 251 |
+
st.success(f"✅ Aucun stock en tension critique au {latest_date.date()}.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
|
| 254 |
# ==============================================================================
|
| 255 |
+
# FONCTION PRINCIPALE (SHOW TERMINAL) - VERSION AUTO-LOAD
|
| 256 |
# ==============================================================================
|
| 257 |
def show_terminal(gspread_client, sheet_name="bdd_mvp_faker"):
|
| 258 |
+
"""Orchestrateur de l'interface Data Explorer"""
|
| 259 |
|
| 260 |
+
# Singleton Pattern pour éviter de recharger à chaque interaction
|
| 261 |
if 'data_explorer' not in st.session_state:
|
| 262 |
st.session_state.data_explorer = DataExplorer(gspread_client, sheet_name)
|
| 263 |
|
| 264 |
explorer = st.session_state.data_explorer
|
| 265 |
|
| 266 |
+
# 1. HEADER
|
| 267 |
+
st.markdown("## 🔬 DATA EXPLORER : CENTRE D'ANALYSE")
|
| 268 |
|
| 269 |
+
# 2. CHARGEMENT AUTOMATIQUE (Plus de bouton)
|
|
|
|
| 270 |
if not explorer.dataframes:
|
| 271 |
+
# On lance le chargement directement
|
| 272 |
+
with st.spinner("🔄 Initialisation et chargement des données..."):
|
| 273 |
success = explorer.load_all_sheets()
|
| 274 |
|
| 275 |
+
if success:
|
| 276 |
+
st.toast("Données chargées avec succès !", icon="✅")
|
| 277 |
+
st.rerun() # On rafraîchit la page pour afficher le dashboard
|
| 278 |
+
else:
|
| 279 |
+
st.error("❌ Échec du chargement automatique. Vérifiez la connexion GSheets.")
|
| 280 |
+
# On arrête l'exécution ici pour ne pas faire planter la suite
|
| 281 |
+
return
|
| 282 |
|
| 283 |
+
# 3. DASHBOARD ANALYTIQUE (S'affiche une fois les données chargées)
|
| 284 |
else:
|
| 285 |
+
# Barre d'actions
|
| 286 |
+
col_refresh, col_info = st.columns([1, 5])
|
| 287 |
+
with col_refresh:
|
| 288 |
+
# Le bouton reload permet de forcer un nouveau téléchargement si besoin
|
| 289 |
+
if st.button("🔄 Reload"):
|
| 290 |
+
explorer.dataframes = {}
|
| 291 |
+
st.rerun()
|
| 292 |
+
|
| 293 |
+
# NAVIGATION PAR ONGLETS MÉTIER
|
| 294 |
+
tabs = st.tabs([
|
| 295 |
+
"🛡️ 1. Audit Qualité (DQ)",
|
| 296 |
+
"⏳ 2. Analyse Temporelle",
|
| 297 |
+
"🌪️ 3. Stress-Tests Scénarios",
|
| 298 |
+
"📦 4. Logistique & Stocks",
|
| 299 |
+
"📋 Inspection Brute"
|
| 300 |
+
])
|
| 301 |
|
| 302 |
with tabs[0]:
|
| 303 |
+
explorer.tab_audit_dq()
|
|
|
|
| 304 |
|
| 305 |
with tabs[1]:
|
| 306 |
+
explorer.tab_temporal_flux()
|
| 307 |
|
| 308 |
with tabs[2]:
|
| 309 |
+
explorer.tab_stress_scenarios()
|
| 310 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
with tabs[3]:
|
| 312 |
+
explorer.tab_logistics()
|
| 313 |
+
|
| 314 |
+
with tabs[4]:
|
| 315 |
+
# Inspection brute simple
|
| 316 |
+
st.markdown("#### 🔎 Explorateur de Tables Brutes")
|
| 317 |
+
table_choice = st.selectbox("Choisir une table", list(explorer.dataframes.keys()))
|
| 318 |
+
if table_choice:
|
| 319 |
+
df_view = explorer.dataframes[table_choice]
|
| 320 |
+
st.write(f"Dimensions : {df_view.shape}")
|
| 321 |
+
st.dataframe(df_view, use_container_width=True)
|