Spaces:
Sleeping
Sleeping
Download src/module/Data_Exploration.py from jaimalklyde/ProjetData: direct link, hf CLI and curl.
- Browser
- Download file 16.7 kB
-
https://huggingface.co/spaces/jaimalklyde/ProjetData/resolve/main/src/module/Data_Exploration.py
- Command line
-
hf download hf://spaces/jaimalklyde/ProjetData/src/module/Data_Exploration.py
-
curl -L -o Data_Exploration.py https://huggingface.co/spaces/jaimalklyde/ProjetData/resolve/main/src/module/Data_Exploration.py
16.7 kB
| """ | |
| MODULE: DATA EXPLORATION (ADVANCED EDA) - VERSION PROFESSIONNELLE | |
| ======================================================================== | |
| Analyses hospitalières : Démographie, Flux, Occupation, Stocks et Clustering | |
| """ | |
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from datetime import datetime | |
| # Vérification des dépendances statistiques | |
| try: | |
| from sklearn.cluster import KMeans | |
| from sklearn.preprocessing import StandardScaler | |
| from scipy import stats | |
| STATS_AVAILABLE = True | |
| except ImportError: | |
| STATS_AVAILABLE = False | |
| # Import de ton moteur ETL (Assure-toi que le chemin est correct) | |
| try: | |
| from module.Validation_Contract import ETL_Engine | |
| except ImportError: | |
| st.error("Le module ETL_Engine est introuvable. Vérifiez l'arborescence.") | |
| # ===================================================== | |
| # ============= CACHE ETL ============================= | |
| # ===================================================== | |
| def cached_run_etl(_gspread_client, sheet_name): | |
| etl = ETL_Engine(_gspread_client, sheet_name) | |
| return etl.run_etl() | |
| # ===================================================== | |
| # ============= ANALYSEUR AVANCÉ ====================== | |
| # ===================================================== | |
| class HospitalAdvancedAnalyzer: | |
| def __init__(self, gspread_client, sheet_name): | |
| self.client = gspread_client | |
| self.sheet_name = sheet_name | |
| self.dataframes_raw = {} | |
| self.dataframes_clean = {} | |
| def load_data(self): | |
| """Charge et prépare les données une seule fois""" | |
| success, result = cached_run_etl(self.client, self.sheet_name) | |
| if success: | |
| self.dataframes_raw = result.dataframes_raw | |
| self.dataframes_clean = result.dataframes_clean | |
| return True | |
| return False | |
| def get_df(self, keyword): | |
| """Récupération intelligente des tables Clean""" | |
| for k, df in self.dataframes_clean.items(): | |
| if keyword.lower() in k.lower(): | |
| return df.copy() | |
| return None | |
| # ========== 1. DÉMOGRAPHIE ========== | |
| def plot_pyramide_ages(self): | |
| st.markdown("### Pyramide des Âges") | |
| df = self.get_df('patients') | |
| if df is None: return | |
| df = df[(df['age'] >= 0) & (df['age'] <= 110)].copy() | |
| bins = [0, 18, 30, 45, 60, 75, 120] | |
| labels = ['0-18', '19-30', '31-45', '46-60', '61-75', '75+'] | |
| df['age_group'] = pd.cut(df['age'], bins=bins, labels=labels, right=False) | |
| pyramid = df.groupby(['age_group', 'sexe'], observed=False).size().reset_index(name='count') | |
| fig = go.Figure() | |
| # Hommes (valeurs négatives pour l'orientation à gauche) | |
| males = pyramid[pyramid['sexe'] == 'M'] | |
| fig.add_trace(go.Bar(y=males['age_group'], x=-males['count'], name='Hommes', | |
| orientation='h', marker_color='steelblue', hoverinfo='x+y')) | |
| # Femmes (à droite) | |
| females = pyramid[pyramid['sexe'] == 'F'] | |
| fig.add_trace(go.Bar(y=females['age_group'], x=females['count'], name='Femmes', | |
| orientation='h', marker_color='pink', hoverinfo='x+y')) | |
| fig.update_layout( | |
| title='Répartition par Sexe et Tranche d\'Âge', | |
| barmode='overlay', | |
| xaxis=dict(title='Nombre de Patients', tickmode='array', | |
| tickvals=[-100, -50, 0, 50, 100], # Ajuster selon volume | |
| ticktext=['100', '50', '0', '50', '100']), | |
| height=500 | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| def plot_geographic_distribution(self): | |
| st.markdown("### Répartition Géographique") | |
| df = self.get_df('patients') | |
| if df is None: return | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| top_villes = df['ville'].value_counts().head(10).reset_index() | |
| fig = px.bar(top_villes, x='ville', y='count', title="Top 10 Villes", color='count') | |
| st.plotly_chart(fig, use_container_width=True) | |
| with col2: | |
| fig_cp = px.treemap(df, path=[px.Constant("Total"), 'code_postal'], title="Répartition par CP") | |
| st.plotly_chart(fig_cp, use_container_width=True) | |
| # ========== 2. PATHOLOGIES ========== | |
| def plot_pathologies_analysis(self): | |
| st.markdown("### Analyse des Pathologies") | |
| df = self.get_df('sejours') | |
| if df is None: return | |
| stats_patho = df.groupby('pathologie_code').agg({ | |
| 'stay_id': 'count', | |
| 'gravite_1_5': 'mean', | |
| 'duree_sejour_jours': 'mean' | |
| }).reset_index().sort_values('stay_id', ascending=False).head(15) | |
| fig = px.scatter(stats_patho, x='gravite_1_5', y='duree_sejour_jours', size='stay_id', | |
| color='pathologie_code', title="Gravité vs Durée de séjour (Taille = Volume)") | |
| st.plotly_chart(fig, use_container_width=True) | |
| # ========== 3. FLUX & SANKEY ========== | |
| def plot_service_flow(self): | |
| st.markdown("### Flux Patients inter-services") | |
| df = self.get_df('sejours') | |
| if df is None: return | |
| df = df.sort_values(['patient_id', 'date_entree']) | |
| df['service_prev'] = df.groupby('patient_id')['service_code'].shift(1) | |
| transfers = df[df['service_prev'].notna() & (df['service_code'] != df['service_prev'])] | |
| if transfers.empty: | |
| st.info("Aucun transfert inter-services détecté.") | |
| return | |
| flow = transfers.groupby(['service_prev', 'service_code']).size().reset_index(name='value') | |
| nodes = list(pd.concat([flow['service_prev'], flow['service_code']]).unique()) | |
| node_map = {name: i for i, name in enumerate(nodes)} | |
| fig = go.Figure(go.Sankey( | |
| node=dict(pad=15, thickness=20, label=nodes), | |
| link=dict(source=flow['service_prev'].map(node_map), | |
| target=flow['service_code'].map(node_map), | |
| value=flow['value']) | |
| )) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # ========== 4. TEMPOREL & SAISONNALITÉ ========== | |
| def plot_temporal_evolution(self): | |
| st.markdown("### Évolution des Admissions") | |
| df = self.get_df('admissions_jour') | |
| if df is None: return | |
| df['date'] = pd.to_datetime(df['date']) | |
| df['nb_admissions'] = pd.to_numeric(df['nb_admissions'], errors='coerce') | |
| # Séries par mois | |
| df['mois'] = df['date'].dt.to_period('M').dt.to_timestamp() | |
| monthly = df.groupby(['mois', 'service_code'])['nb_admissions'].sum().reset_index() | |
| fig = px.line(monthly, x='mois', y='nb_admissions', color='service_code', markers=True) | |
| st.plotly_chart(fig, use_container_width=True) | |
| def plot_seasonality_analysis(self): | |
| st.markdown("### Saisonnalité Hebdomadaire") | |
| df = self.get_df('admissions_jour') | |
| if df is None: return | |
| df['date'] = pd.to_datetime(df['date']) | |
| df['day_num'] = df['date'].dt.weekday | |
| df['day_name'] = df['date'].dt.day_name() | |
| # Tri automatique Lundi -> Dimanche | |
| day_stats = df.groupby(['day_num', 'day_name'])['nb_admissions'].mean().reset_index().sort_values('day_num') | |
| fig = px.bar(day_stats, x='day_name', y='nb_admissions', color='nb_admissions', | |
| title="Moyenne d'admissions par jour") | |
| st.plotly_chart(fig, use_container_width=True) | |
| # ========== 5. RESSOURCES & TENSIONS ========== | |
| def plot_occupation_analysis(self): | |
| st.markdown("### Occupation des Lits") | |
| df = self.get_df('ressources_jour') | |
| if df is None: return | |
| df['taux_pct'] = pd.to_numeric(df['taux_occupation'], errors='coerce') * 100 | |
| avg_occ = df.groupby('service_code')['taux_pct'].mean().reset_index().sort_values('taux_pct', ascending=False) | |
| fig = px.bar(avg_occ, x='service_code', y='taux_pct', text_auto='.1f', | |
| title="Taux d'occupation moyen (%)", color='taux_pct', color_continuous_scale='Reds') | |
| fig.add_hline(y=100, line_dash="dash", line_color="red") | |
| st.plotly_chart(fig, use_container_width=True) | |
| def plot_workload_analysis(self): | |
| st.markdown("### Charge de Travail (Tension)") | |
| df = self.get_df('ressources_jour') | |
| if df is None: return | |
| fig = px.box(df, x='service_code', y='tension_personnel', color='service_code', | |
| title="Distribution de la tension du personnel (1.0 = Seuil critique)") | |
| st.plotly_chart(fig, use_container_width=True) | |
| # ========== 6. STOCKS ========== | |
| def plot_stock_analysis(self): | |
| st.markdown("### État des Stocks") | |
| df = self.get_df('stock_materiel_jour') | |
| if df is None: return | |
| stock_avg = df.groupby('materiel_code').agg({'stock_disponible': 'mean', 'tension_stock': 'mean'}).reset_index() | |
| fig = px.bar(stock_avg, x='materiel_code', y='stock_disponible', color='tension_stock', | |
| title="Stock Moyen vs Tension", color_continuous_scale='RdYlGn_r') | |
| st.plotly_chart(fig, use_container_width=True) | |
| # ========== 7. CLUSTERING & STATS ========== | |
| def plot_patient_clustering(self): | |
| st.markdown("### Segmentation Patients (K-Means)") | |
| if not STATS_AVAILABLE: | |
| st.warning("Bibliothèques scikit-learn manquantes.") | |
| return | |
| df_pat = self.get_df('patients') | |
| df_sej = self.get_df('sejours') | |
| if df_pat is None or df_sej is None: return | |
| # Préparation des features | |
| feat = df_sej.groupby('patient_id').agg({'stay_id': 'count', 'duree_sejour_jours': 'mean', 'gravite_1_5': 'mean'}).reset_index() | |
| feat = feat.merge(df_pat[['patient_id', 'age']], on='patient_id').dropna() | |
| if len(feat) < 10: return | |
| # Clustering | |
| X = feat[['age', 'stay_id', 'duree_sejour_jours', 'gravite_1_5']] | |
| scaler = StandardScaler() | |
| X_scaled = scaler.fit_transform(X) | |
| kmeans = KMeans(n_clusters=3, random_state=42, n_init=10) | |
| feat['cluster'] = kmeans.fit_predict(X_scaled).astype(str) | |
| fig = px.scatter_3d(feat, x='age', y='stay_id', z='gravite_1_5', color='cluster', | |
| title="Clusters de patients", labels={'stay_id': 'Nb Séjours'}) | |
| st.plotly_chart(fig, use_container_width=True) | |
| def plot_statistical_tests(self): | |
| st.markdown("### Analyse de Variance (ANOVA)") | |
| df = self.get_df('sejours') | |
| if df is None or not STATS_AVAILABLE: return | |
| # ANOVA: Durée de séjour entre les 4 plus gros services | |
| top_services = df['service_code'].value_counts().head(4).index | |
| groups = [df[df['service_code'] == s]['duree_sejour_jours'].dropna() for s in top_services] | |
| f_stat, p_val = stats.f_oneway(*groups) | |
| c1, c2 = st.columns(2) | |
| c1.metric("F-Statistic", f"{f_stat:.2f}") | |
| c2.metric("P-Value", f"{p_val:.4f}") | |
| if p_val < 0.05: | |
| st.success("Il existe une différence statistiquement significative entre les services.") | |
| else: | |
| st.info("Pas de différence significative démontrée.") | |
| # ========== 8. EXPORT DONNÉES ========== | |
| def get_all_metrics_dataframes(self): | |
| """Génère un dictionnaire de tous les tableaux d'analyse pour export""" | |
| exports = {} | |
| # 1. Démographie | |
| df_pat = self.get_df('patients') | |
| if df_pat is not None: | |
| # Répartition Villes | |
| exports['demographie_villes'] = df_pat['ville'].value_counts().reset_index(name='nombre_patients') | |
| # Répartition Ages (Simplifiée) | |
| exports['demographie_ages'] = df_pat[['age', 'sexe']].describe().reset_index() | |
| # 2. Pathologies (Stats agrégées) | |
| df_sej = self.get_df('sejours') | |
| if df_sej is not None: | |
| patho_stats = df_sej.groupby('pathologie_code').agg({ | |
| 'stay_id': 'count', | |
| 'gravite_1_5': 'mean', | |
| 'duree_sejour_jours': 'mean' | |
| }).reset_index() | |
| patho_stats.columns = ['Pathologie', 'Nb Séjours', 'Gravité Moyenne', 'Durée Moyenne'] | |
| exports['analyse_pathologies'] = patho_stats | |
| # 3. Admissions Mensuelles | |
| df_adm = self.get_df('admissions_jour') | |
| if df_adm is not None: | |
| df_adm['date'] = pd.to_datetime(df_adm['date']) | |
| df_adm['nb_admissions'] = pd.to_numeric(df_adm['nb_admissions'], errors='coerce') | |
| monthly = df_adm.set_index('date').resample('M')['nb_admissions'].sum().reset_index() | |
| exports['admissions_mensuelles'] = monthly | |
| # 4. Occupation & RH | |
| df_res = self.get_df('ressources_jour') | |
| if df_res is not None: | |
| rh_stats = df_res.groupby('service_code').agg({ | |
| 'taux_occupation': 'mean', | |
| 'tension_personnel': 'mean', | |
| 'lits_occupes': 'max' | |
| }).reset_index() | |
| exports['analyse_services_rh'] = rh_stats | |
| # 5. Stocks | |
| df_stock = self.get_df('stock_materiel_jour') | |
| if df_stock is not None: | |
| stock_stats = df_stock.groupby('materiel_code').agg({ | |
| 'stock_disponible': 'mean', | |
| 'tension_stock': 'mean', | |
| 'alerte_stock': 'sum' | |
| }).reset_index() | |
| exports['analyse_stocks'] = stock_stats | |
| return exports | |
| # ===================================================== | |
| # ============= INTERFACE STREAMLIT =================== | |
| # ===================================================== | |
| def show_terminal(client, sheet_name): | |
| st.markdown('## DATA LAB : EXPLORATION AVANCÉE') | |
| if 'hospital_analyzer' not in st.session_state: | |
| st.session_state.hospital_analyzer = HospitalAdvancedAnalyzer(client, sheet_name) | |
| analyzer = st.session_state.hospital_analyzer | |
| if not analyzer.dataframes_clean: | |
| with st.spinner('Chargement du Lab...'): | |
| if not analyzer.load_data(): | |
| st.error("Erreur de connexion aux données.") | |
| return | |
| st.rerun() | |
| # --- AJOUT DE L'ONGLET "📥 Export" --- | |
| tabs = st.tabs([ | |
| " Démographie", " Pathos", " Flux", " Temps", | |
| " Lits", " RH", " Stocks", " Clusters", " Stats", "📥 Export" | |
| ]) | |
| with tabs[0]: | |
| analyzer.plot_pyramide_ages() | |
| analyzer.plot_geographic_distribution() | |
| with tabs[1]: analyzer.plot_pathologies_analysis() | |
| with tabs[2]: analyzer.plot_service_flow() | |
| with tabs[3]: | |
| analyzer.plot_temporal_evolution() | |
| analyzer.plot_seasonality_analysis() | |
| with tabs[4]: analyzer.plot_occupation_analysis() | |
| with tabs[5]: analyzer.plot_workload_analysis() | |
| with tabs[6]: analyzer.plot_stock_analysis() | |
| with tabs[7]: analyzer.plot_patient_clustering() | |
| with tabs[8]: analyzer.plot_statistical_tests() | |
| # --- LOGIQUE DE L'ONGLET EXPORT --- | |
| with tabs[9]: | |
| st.markdown("### Téléchargement des Rapports d'Analyse") | |
| st.caption("Téléchargez les résultats agrégés calculés par le Data Lab au format CSV.") | |
| # Récupération des données | |
| metrics_data = analyzer.get_all_metrics_dataframes() | |
| if metrics_data: | |
| col1, col2 = st.columns(2) | |
| # Affichage en deux colonnes pour être propre | |
| for i, (name, df) in enumerate(metrics_data.items()): | |
| # Alternance des colonnes | |
| col = col1 if i % 2 == 0 else col2 | |
| with col: | |
| # Conversion en CSV | |
| csv = df.to_csv(index=False).encode('utf-8-sig') # utf-8-sig pour compatibilité Excel | |
| st.download_button( | |
| label=f" {name.replace('_', ' ').title()} (.csv)", | |
| data=csv, | |
| file_name=f"datalab_{name}_{datetime.now().strftime('%Y%m%d')}.csv", | |
| mime='text/csv', | |
| use_container_width=True | |
| ) | |
| # Petit aperçu (optionnel) | |
| with st.expander(f" Aperçu : {name}"): | |
| st.dataframe(df.head(5), use_container_width=True) | |
| else: | |
| st.warning("Aucune donnée d'analyse disponible pour l'export.") |