Spaces:
Running
Running
| # IMPORTS | |
| import pandas as pd | |
| import numpy as np | |
| import matplotlib | |
| import matplotlib.pyplot as plt | |
| import matplotlib.ticker as mticker | |
| import seaborn as sns | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| import json | |
| import warnings | |
| import os | |
| from time import perf_counter, sleep | |
| from datetime import datetime | |
| from pathlib import Path | |
| warnings.filterwarnings("ignore") | |
| from pathlib import Path | |
| import json | |
| DATASET_PATH = "hf://datasets/ryanxely/medical_dataset_3M/medical_dataset.csv" | |
| # Config Section | |
| DATA_PATH = Path("/data") | |
| KPIS_PATH = DATA_PATH / "kpis_export.json" | |
| MOCK_KPIS_PATH = DATA_PATH / "kpis_export_mock.json" | |
| LOG_PATH = DATA_PATH / "trace.log" | |
| WORKING_DIR = DATA_PATH | |
| # In[3]: | |
| # Fonctions et variables utiles | |
| start_time = 0 | |
| def start(): | |
| global start_time | |
| start_time = perf_counter() | |
| def time_lapse(display=True): | |
| time_elapsed = perf_counter() - start_time | |
| hours, rem = divmod(time_elapsed, 3600) | |
| minutes, seconds = divmod(rem, 60) | |
| if display: | |
| return f"{int(hours)}h {int(minutes)}m {seconds:.2f}s" | |
| return int(hours), int(minutes), seconds | |
| def log(message): | |
| with open(LOG_PATH, "a", encoding="utf-8") as logger: | |
| logger.writelines([f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] : {message}"]) | |
| def extract_kpis(): | |
| start() | |
| log("KPIs Extraction started...") | |
| # Configuration globale des graphes matplotlib | |
| log("Configuration globale des graphes matplotlib...") | |
| matplotlib.rcParams.update({ | |
| "figure.dpi": 120, | |
| "figure.facecolor": "white", | |
| "axes.facecolor": "white", | |
| "axes.grid": True, | |
| "grid.alpha": 0.3, | |
| "font.size": 10, | |
| "axes.titlesize": 12, | |
| "axes.labelsize": 10, | |
| }) | |
| # Palette de couleurs cohérente pour tous les graphes | |
| PALETTE_PRINCIPALE = "steelblue" | |
| PALETTE_CATEGORIELLE = "tab10" | |
| PALETTE_DIVERGENTE = "coolwarm" | |
| PALETTE_SEQUENTIELLE = "YlOrRd" | |
| print("Imports effectues avec succes.") | |
| log("Imports effectues avec succes.") | |
| print(f"Pandas version : {pd.__version__}") | |
| print(f"NumPy version : {np.__version__}") | |
| # In[4]: | |
| # Chargement et Aperçu de la Dataset | |
| print(f"Chemin de la dataset : {DATASET_PATH}") | |
| print("Chargement du fichier...") | |
| log("Chargement de la dataset...") | |
| with open(KPIS_PATH, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| df = pd.read_csv(DATASET_PATH, low_memory=False) | |
| print(f"\nDataset charge avec succes.") | |
| log("Dataset chargée avec succes.") | |
| print(f"Dimensions : {df.shape[0]:,} lignes x {df.shape[1]} colonnes") | |
| # Apercu des premieres lignes | |
| print("\nApercu des 5 premieres lignes :") | |
| print(df.head()) | |
| # Types de colonnes | |
| print("\nTypes de donnees par colonne (extrait) :") | |
| print(df.dtypes.value_counts()) | |
| # Identification des colonnes symptomes | |
| # Les colonnes symptomes sont toutes celles hors des 4 colonnes descriptives | |
| COLONNES_META = ["disease", "age", "sex", "medical_history"] | |
| symptom_cols = [c for c in df.columns if c not in COLONNES_META] | |
| print(f"\nNombre de colonnes symptomes identifiees : {len(symptom_cols)}") | |
| print(f"Exemples : {symptom_cols[:5]}") | |
| # Résumé | |
| log("Affichage du résumé...") | |
| symptom_cols = df.columns[4:].tolist() | |
| meta = { | |
| "shape": df.shape, | |
| "diseases": df["disease"].nunique(), | |
| "disease_list": df["disease"].unique().tolist(), | |
| "age_range": [int(df["age"].min()), int(df["age"].max())], | |
| "sex_values": df["sex"].unique().tolist(), | |
| "medical_history_values": df["medical_history"].unique().tolist(), | |
| "symptom_count": len(symptom_cols), | |
| "symptom_columns": symptom_cols, | |
| "missing_values": df.isnull().sum()[df.isnull().sum() > 0].to_dict(), | |
| "missing_total_pct": round(df.isnull().sum().sum() / df.size * 100, 2), | |
| "disease_distribution_top20": df["disease"].value_counts().head(20).to_dict() | |
| } | |
| print("\nRésumé de la dataset", json.dumps(meta, indent=2, ensure_ascii=False)) | |
| with open(f"{WORKING_DIR}/dataset_summary.json", "w", encoding="utf-8") as f: | |
| json.dump(meta, f, indent=2, ensure_ascii=False) | |
| log("Résumé exporté avec succès.") | |
| # In[ ]: | |
| # KPI A1 — Inventaire general du dataset | |
| print("\n=== KPI A1 : Inventaire general ===\n") | |
| log("Chargement du KPI A1 : Inventaire general") | |
| nb_lignes = df.shape[0] | |
| nb_colonnes = df.shape[1] | |
| nb_symptomes = len(symptom_cols) | |
| memoire_mb = df.memory_usage(deep=True).sum() / 1024**2 # conversion en Mo | |
| inventaire = pd.DataFrame({ | |
| "Indicateur": [ | |
| "Nombre de lignes", | |
| "Nombre de colonnes total", | |
| "Colonnes descriptives patient", | |
| "Colonnes symptomes binaires", | |
| "Memoire consommee (Mo)" | |
| ], | |
| "Valeur": [ | |
| f"{nb_lignes:,}", | |
| nb_colonnes, | |
| len(COLONNES_META), | |
| nb_symptomes, | |
| f"{memoire_mb:.1f}" | |
| ] | |
| }) | |
| print(inventaire.to_string(index=False)) | |
| # In[ ]: | |
| # KPI A2 — Valeurs manquantes | |
| print("\n=== KPI A2 : Valeurs manquantes ===\n") | |
| log("Chargement du KPI A2 : Valeurs manquantes") | |
| # Calcul du taux de valeurs manquantes par colonne (uniquement colonnes avec manquants) | |
| manquants = df.isnull().sum() | |
| manquants = manquants[manquants > 0].sort_values(ascending=False) | |
| taux_manquants = (manquants / nb_lignes * 100).round(4) | |
| df_manquants = pd.DataFrame({ | |
| "Colonne": manquants.index, | |
| "Nb valeurs manquantes": manquants.values, | |
| "Taux (%)": taux_manquants.values | |
| }) | |
| print(df_manquants.to_string(index=False)) | |
| # Taux global de valeurs manquantes sur tout le dataset | |
| total_cellules = nb_lignes * nb_colonnes | |
| total_manquants = df.isnull().sum().sum() | |
| taux_global = total_manquants / total_cellules * 100 | |
| print(f"\nTaux global de valeurs manquantes : {taux_global:.4f}%") | |
| # Graphe — Barres horizontales des colonnes avec valeurs manquantes | |
| if len(df_manquants) > 0: | |
| fig, ax = plt.subplots(figsize=(8, max(2, len(df_manquants) * 0.5))) | |
| sns.barplot( | |
| data=df_manquants, y="Colonne", x="Taux (%)", | |
| color=PALETTE_PRINCIPALE, ax=ax | |
| ) | |
| ax.set_title("Taux de valeurs manquantes par colonne (%)") | |
| ax.set_xlabel("Taux (%)") | |
| ax.set_ylabel("") | |
| ax.set_xlim(0, 100) | |
| for bar, val in zip(ax.patches, df_manquants["Taux (%)"]): | |
| ax.text(bar.get_width() + 0.1, bar.get_y() + bar.get_height() / 2, | |
| f"{val:.2f}%", va="center", fontsize=9) | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_a2_valeurs_manquantes.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_a2_valeurs_manquantes.png") | |
| # In[ ]: | |
| # KPI A3 — Integrite des colonnes symptomes | |
| print("\n=== KPI A3 : Integrite des colonnes symptomes ===\n") | |
| log("Chargement du KPI A3 : Integrite des colonnes symptomes") | |
| # Verification que chaque colonne symptome ne contient que 0 et 1 | |
| colonnes_corrompues = [] | |
| for col in symptom_cols: | |
| valeurs_uniques = set(df[col].dropna().unique()) | |
| if not valeurs_uniques.issubset({0, 1, 0.0, 1.0}): | |
| colonnes_corrompues.append((col, valeurs_uniques)) | |
| if len(colonnes_corrompues) == 0: | |
| print("Integrite binaire validee : toutes les colonnes symptomes contiennent uniquement 0 ou 1.") | |
| else: | |
| print(f"ATTENTION : {len(colonnes_corrompues)} colonne(s) avec des valeurs non binaires :") | |
| for col, vals in colonnes_corrompues[:10]: | |
| print(f" - {col} : {vals}") | |
| # Resume des types de colonnes | |
| types_resume = pd.DataFrame({ | |
| "Type": ["Variable cible (disease)", "Numerique (age)", "Categorielle binaire (sex)", | |
| "Categorielle (medical_history)", "Binaire symptomes (0/1)"], | |
| "Nb colonnes": [1, 1, 1, 1, nb_symptomes] | |
| }) | |
| print("\nRepartition des types de colonnes :") | |
| print(types_resume.to_string(index=False)) | |
| # In[ ]: | |
| # KPI B1 : Distribution des maladies | |
| print("\n=== KPI B1 : Distribution des maladies ===\n") | |
| log("Chargement du KPI B1 - Distribution des maladies...") | |
| counts_maladies = df["disease"].value_counts() | |
| nb_maladies_distinctes = counts_maladies.shape[0] | |
| print(f"Nombre de maladies distinctes : {nb_maladies_distinctes}") | |
| print(f"Statistiques des effectifs par maladie :") | |
| print(counts_maladies.describe().round(2)) | |
| # Top 20 maladies les plus frequentes | |
| top20 = counts_maladies.head(20).reset_index() | |
| top20.columns = ["Maladie", "Nb_cas"] | |
| fig, ax = plt.subplots(figsize=(12, 6)) | |
| sns.barplot(data=top20, x="Nb_cas", y="Maladie", color=PALETTE_PRINCIPALE, ax=ax) | |
| ax.set_title("Top 20 maladies les plus frequentes") | |
| ax.set_xlabel("Nombre de cas") | |
| ax.set_ylabel("") | |
| ax.set_xlim(top20["Nb_cas"].min() / 1.001, top20["Nb_cas"].max() * 1.001) | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_b1_top20_maladies.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_b1_top20_maladies.png") | |
| # Bottom 20 maladies les moins frequentes | |
| bottom20 = counts_maladies.tail(20).reset_index() | |
| bottom20.columns = ["Maladie", "Nb_cas"] | |
| fig, ax = plt.subplots(figsize=(12, 6)) | |
| sns.barplot(data=bottom20, x="Nb_cas", y="Maladie", color="salmon", ax=ax) | |
| ax.set_title("Bottom 20 maladies les moins frequentes") | |
| ax.set_xlabel("Nombre de cas") | |
| ax.set_ylabel("") | |
| ax.set_xlim(bottom20["Nb_cas"].min() / 1.001, bottom20["Nb_cas"].max() * 1.001) | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_b1_bottom20_maladies.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_b1_bottom20_maladies.png") | |
| # In[ ]: | |
| # KPI B2 : Coefficient de desequilibre | |
| print("\n=== KPI B2 : Coefficient de desequilibre ===\n") | |
| log("Chargement du KPI B2 - Coefficient de desequilibre...") | |
| ratio_desequilibre = counts_maladies.max() / counts_maladies.min() | |
| # Indice de Gini de desequilibre (1 = parfaitement desequilibre, 0 = parfaitement equilibre) | |
| N = nb_lignes | |
| proportions = counts_maladies / N | |
| gini = 1 - (proportions ** 2).sum() | |
| print(f"Effectif maximum (maladie la plus frequente) : {counts_maladies.max():,}") | |
| print(f"Effectif minimum (maladie la moins frequente) : {counts_maladies.min():,}") | |
| print(f"Ratio de desequilibre (max/min) : {ratio_desequilibre:.2f}") | |
| print(f"Indice de Gini d'equilibre : {gini:.4f}") | |
| if ratio_desequilibre < 1.5: | |
| message = "Notre dataset est bien équilibré et favorable pour l'entraînement ML." | |
| elif ratio_desequilibre < 3: | |
| message = "Notre dataset est en déséquilibre modéré. Il faut surveiller les classes minoritaires." | |
| else: | |
| message = "Notre dataset est en déséquilibre significatif. Nous devons envisager oversampling ou class_weight." | |
| print(f"\nSelon le ratio de déséquilibre, {message}") | |
| if gini == 1 : | |
| print("Selon l'indice de Gini, notre dataset est parfaitement equilibré.") | |
| elif gini == 0 : | |
| print("Selon l'indice de Gini, notre dataset est parfaitement déséquilibré.") | |
| # Histogramme de la distribution des effectifs par maladie | |
| fig, ax = plt.subplots(figsize=(10, 5)) | |
| ax.hist(counts_maladies.values, bins=50, color=PALETTE_PRINCIPALE, edgecolor="white") | |
| ax.set_title("Distribution des effectifs par maladie") | |
| ax.set_xlabel("Nombre de cas") | |
| ax.set_ylabel("Nombre de maladies") | |
| ax.axvline(counts_maladies.mean(), color="red", linestyle="--", | |
| label=f"Moyenne : {counts_maladies.mean():.0f}") | |
| ax.legend() | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_b2_distribution_effectifs.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_b2_distribution_effectifs.png") | |
| # In[ ]: | |
| # KPI C1 : Distribution de l'age | |
| print("\n=== KPI C1 : Distribution de l'age ===\n") | |
| log("Chargement du KPI C1 - Distribution de l'age...") | |
| # Verification des valeurs aberrantes | |
| nb_ages_invalides = ((df["age"] < 0) | (df["age"] > 100)).sum() | |
| print(f"Valeurs d'age hors plage [0, 100] : {nb_ages_invalides}") | |
| print("\nStatistiques descriptives de l'age :") | |
| print(df["age"].describe().round(2)) | |
| # Creation des tranches d'age — colonne utilisee dans les KPIs suivants | |
| tranches_bins = [0, 12, 18, 35, 50, 65, 100] | |
| tranches_labels = ["Enfant (0-12)", "Adolescent (13-18)", "Jeune adulte (19-35)", | |
| "Adulte (36-50)", "Senior (51-65)", "Tres age (66+)"] | |
| df["age_group"] = pd.cut(df["age"], bins=tranches_bins, labels=tranches_labels, right=True) | |
| # Distribution par tranche d'age | |
| dist_age = df["age_group"].value_counts().sort_index().reset_index() | |
| dist_age.columns = ["Tranche", "Nb_patients"] | |
| dist_age["Pct"] = (dist_age["Nb_patients"] / nb_lignes * 100).round(2) | |
| print("\nDistribution par tranche d'age :") | |
| print(dist_age.to_string(index=False)) | |
| # Histogramme interactif (plotly) | |
| fig_plotly = px.histogram( | |
| df, x="age", nbins=20, | |
| title="Distribution de l'age des patients", | |
| labels={"age": "Age", "count": "Nombre de patients"}, | |
| color_discrete_sequence=["steelblue"] | |
| ) | |
| fig_plotly.update_layout(bargap=0.05) | |
| fig_plotly.update_yaxes( | |
| range=[ | |
| int(df["age"].value_counts().min()), | |
| int(df["age"].value_counts().max()) * 1.00001 | |
| ] | |
| ) | |
| fig_plotly.write_html(f"{WORKING_DIR}/kpi_c1_age.html") | |
| fig_plotly.show() | |
| print("Fichier sauvegarde : kpi_c1_age.html\n") | |
| # Graphe barres par tranche | |
| fig, ax = plt.subplots(figsize=(10, 5)) | |
| sns.barplot(data=dist_age, x="Tranche", y="Nb_patients", color=PALETTE_PRINCIPALE, ax=ax) | |
| ax.set_title("Nombre de patients par tranche d'age") | |
| ax.set_xlabel("Tranche d'age") | |
| ax.set_ylabel("Nombre de patients") | |
| ax.tick_params(axis="x", rotation=20) | |
| for bar, row in zip(ax.patches, dist_age.itertuples()): | |
| ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 1000, | |
| f"{row.Pct}%", ha="center", fontsize=8) | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_c1_tranches_age.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_c1_tranches_age.png") | |
| # In[ ]: | |
| # KPI C2 : Repartition par sexe | |
| print("\n=== KPI C2 : Repartition par sexe ===\n") | |
| log("Chargement du KPI C2 - Repartition par sexe...") | |
| # Repartition globale | |
| dist_sexe = df["sex"].value_counts() | |
| dist_sexe_pct = (dist_sexe / nb_lignes * 100).round(2) | |
| print("Repartition globale par sexe :") | |
| for sexe, nb in dist_sexe.items(): | |
| print(f" {sexe} : {nb:,} ({dist_sexe_pct[sexe]}%)") | |
| # Pie chart global | |
| fig, ax = plt.subplots(figsize=(6, 6)) | |
| ax.pie(dist_sexe.values, labels=dist_sexe.index, autopct="%1.1f%%", | |
| colors=["#5b9bd5", "#ed7d31"], startangle=90) | |
| ax.set_title("Repartition globale par sexe") | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_c2_sexe_global.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_c2_sexe_global.png") | |
| # Ratio homme/femme par maladie | |
| sexe_par_maladie = df.groupby("disease")["sex"].value_counts(normalize=True).unstack(fill_value=0) | |
| # S'assurer que les deux colonnes existent | |
| for col in ["Female", "Male"]: | |
| if col not in sexe_par_maladie.columns: | |
| sexe_par_maladie[col] = 0.0 | |
| sexe_par_maladie["ratio_F_M"] = sexe_par_maladie.get("Female", 0) / \ | |
| (sexe_par_maladie.get("Male", 0) + 1e-9) | |
| # Top 10 maladies a dominante feminine (ratio F/M le plus eleve) | |
| top10_fem = sexe_par_maladie["ratio_F_M"].nlargest(10).reset_index() | |
| top10_fem.columns = ["Maladie", "Ratio_F_M"] | |
| print("\nTop 10 maladies a dominante feminine (ratio F/M) :") | |
| print(top10_fem.to_string(index=False)) | |
| # Top 10 maladies a dominante masculine (ratio F/M le plus faible) | |
| top10_masc = sexe_par_maladie["ratio_F_M"].nsmallest(10).reset_index() | |
| top10_masc.columns = ["Maladie", "Ratio_F_M"] | |
| print("\nTop 10 maladies a dominante masculine (ratio F/M) :") | |
| print(top10_masc.to_string(index=False)) | |
| # Analyse | |
| # Si ratio_F_M > 2 : la maladie touche majoritairement les femmes. | |
| # Si ratio_F_M < 0.5 : la maladie touche majoritairement les hommes. | |
| # Si ratio_F_M entre 0.8 et 1.2 : distribution equilibree entre les sexes. | |
| # Barres empilees pour les 20 maladies les plus frequentes | |
| top20_noms = list(top20["Maladie"]) | |
| sexe_top20 = sexe_par_maladie.loc[ | |
| sexe_par_maladie.index.isin(top20_noms), ["Female", "Male"] | |
| ].reset_index() | |
| fig, ax = plt.subplots(figsize=(12, 6)) | |
| sexe_top20.set_index("disease")[["Female", "Male"]].plot( | |
| kind="barh", stacked=True, ax=ax, | |
| color=["#5b9bd5", "#ed7d31"] | |
| ) | |
| ax.set_title("Repartition homme/femme pour les 20 maladies les plus frequentes (proportion)") | |
| ax.set_xlabel("Proportion") | |
| ax.set_ylabel("") | |
| ax.legend(["Femme", "Homme"], loc="lower right") | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_c2_sexe_top20_maladies.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_c2_sexe_top20_maladies.png") | |
| # In[ ]: | |
| sexe_par_maladie["ratio_F_M"] | |
| # In[ ]: | |
| # KPI C3 : Antecedents medicaux | |
| print("\n=== KPI C3 : Antecedents medicaux ===\n") | |
| log("Chargement du KPI C3 - Antecedents medicaux...") | |
| # Traiter les NaN comme une categorie explicite | |
| df["medical_history_filled"] = df["medical_history"].fillna("Aucun antecedent connu") | |
| dist_antecedents = df["medical_history_filled"].value_counts().reset_index() | |
| dist_antecedents.columns = ["Antecedent", "Nb_patients"] | |
| dist_antecedents["Pct"] = (dist_antecedents["Nb_patients"] / nb_lignes * 100).round(2) | |
| print("Distribution des antecedents medicaux :") | |
| print(dist_antecedents.to_string(index=False)) | |
| # Taux de NaN reel | |
| taux_nan_antecedents = df["medical_history"].isna().sum() / nb_lignes * 100 | |
| print(f"\nTaux de valeurs manquantes dans medical_history : {taux_nan_antecedents:.2f}%") | |
| # Graphe barres horizontales | |
| fig, ax = plt.subplots(figsize=(12, 6)) | |
| sns.barplot( | |
| data=dist_antecedents, y="Antecedent", x="Nb_patients", | |
| color=PALETTE_PRINCIPALE, ax=ax | |
| ) | |
| ax.set_title("Distribution des antecedents medicaux (y compris valeurs manquantes)") | |
| ax.set_xlabel("Nombre de patients") | |
| ax.set_ylabel("") | |
| ax.set_xlim(dist_antecedents["Nb_patients"].min() / 1.001, dist_antecedents["Nb_patients"].max() * 1.001) | |
| for bar, row in zip(ax.patches, dist_antecedents.itertuples()): | |
| ax.text(bar.get_width() + 1000, bar.get_y() + bar.get_height() / 2, | |
| f"{row.Pct}%", va="center", fontsize=9) | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_c3_antecedents.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_c3_antecedents.png") | |
| # In[ ]: | |
| # KPI C4 : Age moyen par maladie | |
| print("\n=== KPI C4 : Age moyen par maladie ===\n") | |
| log("Chargement du KPI C4 - Age moyen par maladie...") | |
| age_par_maladie = df.groupby("disease")["age"].agg(["mean", "median", "std"]).round(2) | |
| age_par_maladie.columns = ["Age_moyen", "Age_median", "Ecart_type"] | |
| # Top 10 maladies touchant les patients les plus jeunes | |
| top10_jeunes = age_par_maladie.nsmallest(10, "Age_moyen").reset_index() | |
| print("Top 10 maladies touchant les plus jeunes (age moyen) :") | |
| print(top10_jeunes.to_string(index=False)) | |
| # Top 10 maladies touchant les patients les plus ages | |
| top10_ages = age_par_maladie.nlargest(10, "Age_moyen").reset_index() | |
| print("\nTop 10 maladies touchant les plus ages (age moyen) :") | |
| print(top10_ages.to_string(index=False)) | |
| # Analyse | |
| # Commentons sur 10 maladies au hasard | |
| # print("\nCommentaires sur 10 maladies au hasard") | |
| # sample = age_par_maladie.sample(n=10, random_state=402) | |
| # for i in range(len(sample)): | |
| # record = sample.iloc[i] | |
| # if record["Ecart_type"] > 20: | |
| # print(f"\tLa maladie {record.name} touche toutes les tranches d'âge de manière relativement indiscriminée.") | |
| # elif record["Ecart_type"] < 10: | |
| # print(f"\tLa maladie {record.name} est fortement concentrée sur une tranche d'âge spécifique.") | |
| # else: | |
| # print(f"\tLa maladie {record.name} présente une dispersion modérée selon les tranches d'âge.") | |
| # Boxplot des 20 maladies les plus frequentes par age | |
| df_top20 = df[df["disease"].isin(top20_noms)] | |
| df_top20_only = ( | |
| df[df["disease"].isin(top20_noms)] | |
| .drop_duplicates(subset=["disease"]) | |
| ) | |
| fig, ax = plt.subplots(figsize=(14, 8)) | |
| sns.boxplot( | |
| data=df_top20, y="disease", x="age", | |
| order=top20_noms, palette="tab20", ax=ax, | |
| showfliers=True, whis=np.inf, | |
| ) | |
| ax.set_title("Distribution de l'age pour les 20 maladies les plus frequentes") | |
| ax.set_xlabel("Age") | |
| ax.set_ylabel("") | |
| ax.set_xlim(df_top20_only["age"].min()/1.001, df_top20_only["age"].max()*1.001) | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_c4_age_par_maladie.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_c4_age_par_maladie.png") | |
| # # Scatterplot des repartitions | |
| # disease_codes = { | |
| # disease: i | |
| # for i, disease in enumerate(df["disease"].unique()) | |
| # } | |
| # df["disease_index"] = df["disease"].map(disease_codes) | |
| # fig, ax = plt.subplots(figsize=(14, 8)) | |
| # sns.scatterplot( | |
| # data=df, | |
| # x="disease_index", | |
| # y="age", | |
| # alpha=0.5, | |
| # ax=ax | |
| # ) | |
| # ax.set_title("Répartition des âges selon les maladies") | |
| # ax.set_xlabel("Index des maladies") | |
| # ax.set_ylabel("Âge") | |
| # plt.tight_layout() | |
| # plt.show() | |
| # In[ ]: | |
| # KPI D1 : Prevalence globale des symptomes | |
| print("\n=== KPI D1 : Prevalence globale des symptomesKPI ===\n") | |
| log("Chargement du KPI D1 - Prevalence globale des symptomes...") | |
| # Taux de presence de chaque symptome (en %) sur l'ensemble du dataset | |
| prevalence_symptomes = (df[symptom_cols].mean() * 100).sort_values(ascending=False) | |
| print(f"Nombre total de symptomes : {len(prevalence_symptomes)}") | |
| print(f"Symptome le plus frequent : {prevalence_symptomes.index[0]} ({prevalence_symptomes.iloc[0]:.2f}%)") | |
| print(f"Symptome le plus rare : {prevalence_symptomes.index[-1]} ({prevalence_symptomes.iloc[-1]:.2f}%)") | |
| # Symptomes quasi-absents (< 1%) et quasi-universels (> 90%) | |
| quasi_absents = prevalence_symptomes[prevalence_symptomes < 1] | |
| quasi_universels = prevalence_symptomes[prevalence_symptomes > 90] | |
| print(f"\nSymptomes avec prevalence < 1% : {len(quasi_absents)}") | |
| print(f"Symptomes avec prevalence > 90% : {len(quasi_universels)}") | |
| # Top 20 symptomes les plus frequents | |
| top20_symp = prevalence_symptomes.head(20).reset_index() | |
| top20_symp.columns = ["Symptome", "Prevalence_pct"] | |
| fig, ax = plt.subplots(figsize=(12, 7)) | |
| sns.barplot(data=top20_symp, x="Prevalence_pct", y="Symptome", | |
| color=PALETTE_PRINCIPALE, ax=ax) | |
| ax.set_title("Top 20 symptomes les plus frequents (% de patients concernes)") | |
| ax.set_xlabel("Prevalence (%)") | |
| ax.set_ylabel("") | |
| ax.set_xlim(top20_symp["Prevalence_pct"].min() / 1.01, top20_symp["Prevalence_pct"].max() * 1.01) | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_d1_top20_symptomes.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_d1_top20_symptomes.png") | |
| # Bottom 20 symptomes les plus rares | |
| bottom20_symp = prevalence_symptomes.tail(20).reset_index() | |
| bottom20_symp.columns = ["Symptome", "Prevalence_pct"] | |
| fig, ax = plt.subplots(figsize=(12, 7)) | |
| sns.barplot(data=bottom20_symp, x="Prevalence_pct", y="Symptome", | |
| color="salmon", ax=ax) | |
| ax.set_title("Bottom 20 symptomes les plus rares (% de patients concernes)") | |
| ax.set_xlabel("Prevalence (%)") | |
| ax.set_ylabel("") | |
| ax.set_xlim(bottom20_symp["Prevalence_pct"].min() / 1.001, bottom20_symp["Prevalence_pct"].max() * 1.001) | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_d1_bottom20_symptomes.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_d1_bottom20_symptomes.png") | |
| # In[ ]: | |
| # KPI D2 : Nombre de symptomes par patient | |
| print("\n=== KPI D2 : Nombre de symptomes par patient ===\n") | |
| log("Chargement du KPI D2 - Nombre de symptomes par patient...") | |
| # Calcul du nombre de symptomes presents par ligne patient | |
| df["nb_symptomes"] = df[symptom_cols].sum(axis=1) | |
| # Verification de coherence : lignes sans aucun symptome | |
| nb_sans_symptome = (df["nb_symptomes"] == 0).sum() | |
| print(f"Patients sans aucun symptome (valeur 0 partout) : {nb_sans_symptome}") | |
| if nb_sans_symptome > 0: | |
| print(" ATTENTION : ces lignes sont medicalement incoherentes et devront etre examinees.") | |
| print("\nStatistiques globales du nombre de symptomes par patient :") | |
| print(df["nb_symptomes"].describe().round(2)) | |
| # Histogramme de la distribution | |
| fig, ax = plt.subplots(figsize=(10, 5)) | |
| ax.hist(df["nb_symptomes"], bins=50, color=PALETTE_PRINCIPALE, edgecolor="white") | |
| ax.set_title("Distribution du nombre de symptomes par patient") | |
| ax.set_xlabel("Nombre de symptomes") | |
| ax.set_ylabel("Nombre de patients") | |
| ax.axvline(df["nb_symptomes"].mean(), color="red", linestyle="--", | |
| label=f"Moyenne : {df['nb_symptomes'].mean():.1f}") | |
| ax.legend() | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_d2_nb_symptomes_distribution.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_d2_nb_symptomes_distribution.png") | |
| # Complexite symptomatique par maladie | |
| complexite_par_maladie = df.groupby("disease")["nb_symptomes"].mean().sort_values() | |
| top10_simple = complexite_par_maladie.head(10).reset_index() | |
| top10_simple.columns = ["Maladie", "Nb_symptomes_moyen"] | |
| top10_complexe = complexite_par_maladie.tail(10).reset_index() | |
| top10_complexe.columns = ["Maladie", "Nb_symptomes_moyen"] | |
| print("\nTop 10 maladies avec le moins de symptomes (les plus simples) :") | |
| print(top10_simple.to_string(index=False)) | |
| print("\nTop 10 maladies avec le plus de symptomes (les plus complexes) :") | |
| print(top10_complexe.to_string(index=False)) | |
| # Graphe comparatif | |
| fig, axes = plt.subplots(1, 2, figsize=(14, 6)) | |
| sns.barplot(data=top10_simple, x="Nb_symptomes_moyen", y="Maladie", | |
| color="steelblue", ax=axes[0]) | |
| axes[0].set_title("10 maladies les plus simples (symptomes)") | |
| axes[0].set_xlabel("Nb moyen de symptomes") | |
| sns.barplot(data=top10_complexe, x="Nb_symptomes_moyen", y="Maladie", | |
| color="coral", ax=axes[1]) | |
| axes[1].set_title("10 maladies les plus complexes (symptomes)") | |
| axes[1].set_xlabel("Nb moyen de symptomes") | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_d2_complexite_maladies.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_d2_complexite_maladies.png") | |
| # In[ ]: | |
| # KPI D3 : Symptomes par maladie (taux d'occurrence) | |
| print("\n=== KPI D3 : Symptomes par maladie (taux d'occurrence) ===\n") | |
| print("Calcul en cours — operation longue sur 773 maladies x 328 symptomes...") | |
| log("Chargement du KPI D3 - Symptomes par maladie (taux d'occurrence)...") | |
| # DataFrame (773 x 328) : chaque cellule = % de patients de cette maladie avec ce symptome | |
| taux_symptomes_par_maladie = df.groupby("disease")[symptom_cols].mean() * 100 | |
| taux_symptomes_par_maladie = taux_symptomes_par_maladie.round(2) | |
| print(f"Tableau calcule : {taux_symptomes_par_maladie.shape}") | |
| print("Apercu (5 maladies x 5 symptomes) :") | |
| print(taux_symptomes_par_maladie.iloc[:5, :5]) | |
| # Heatmap limitee aux 20 maladies les plus frequentes x 30 symptomes les plus discriminants | |
| # (les symptomes discriminants sont determines au KPI D4 — on les pre-calcule ici) | |
| variance_symptomes = taux_symptomes_par_maladie.var(axis=0).sort_values(ascending=False) | |
| top30_discriminants = variance_symptomes.head(30).index.tolist() | |
| # Sous-tableau pour la heatmap | |
| heatmap_data = taux_symptomes_par_maladie.loc[ | |
| taux_symptomes_par_maladie.index.isin(top20_noms), | |
| top30_discriminants | |
| ] | |
| fig, ax = plt.subplots(figsize=(18, 10)) | |
| sns.heatmap( | |
| heatmap_data, | |
| annot=False, | |
| cmap=PALETTE_SEQUENTIELLE, | |
| linewidths=0.3, | |
| ax=ax, | |
| cbar_kws={"label": "Taux d'occurrence (%)"} | |
| ) | |
| ax.set_title("Taux d'occurrence des 30 symptomes les plus discriminants\npour les 20 maladies les plus frequentes (%)") | |
| ax.set_xlabel("Symptomes") | |
| ax.set_ylabel("Maladies") | |
| ax.tick_params(axis="x", rotation=45, labelsize=7) | |
| ax.tick_params(axis="y", rotation=0, labelsize=8) | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_d3_heatmap_symptomes_maladies.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_d3_heatmap_symptomes_maladies.png") | |
| # In[ ]: | |
| # KPI D4 : Symptomes discriminants (variance inter-maladies) | |
| print("\n=== KPI D4 : Symptomes discriminants (variance inter-maladies) ===\n") | |
| log("Chargement du KPI D4 - Symptomes discriminants (variance inter-maladies)...") | |
| # La variance inter-maladies est deja calculee dans la section D3 | |
| # (variable : variance_symptomes) | |
| top30_disc_df = pd.DataFrame({ | |
| "Symptome": top30_discriminants, | |
| "Variance_inter_maladies": variance_symptomes[top30_discriminants].values | |
| }).round(4) | |
| print("Top 30 symptomes les plus discriminants :") | |
| print(top30_disc_df.to_string(index=False)) | |
| # Analyse | |
| # Un symptome avec variance elevee est present dans certaines maladies et absent dans d'autres. | |
| # C'est un signal fort pour la classification ML — ces features auront probablement | |
| # une importance elevee dans un modele Random Forest ou XGBoost. | |
| # Un symptome avec variance proche de 0 est soit present partout soit absent partout | |
| # — il apporte peu d'information discriminante. | |
| fig, ax = plt.subplots(figsize=(12, 9)) | |
| sns.barplot( | |
| data=top30_disc_df, x="Variance_inter_maladies", y="Symptome", | |
| color=PALETTE_PRINCIPALE, ax=ax | |
| ) | |
| ax.set_title("Top 30 symptomes les plus discriminants (variance du taux d'occurrence entre maladies)") | |
| ax.set_xlabel("Variance inter-maladies") | |
| ax.set_ylabel("") | |
| ax.set_xlim(top30_disc_df["Variance_inter_maladies"].min() / 1.01, top30_disc_df["Variance_inter_maladies"].max() * 1.01) | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_d4_symptomes_discriminants.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_d4_symptomes_discriminants.png") | |
| # In[ ]: | |
| # KPI D5 : Correlation entre symptomes | |
| print("\n=== KPI D5 : Correlation entre symptomes ===\n") | |
| print("Calcul de la matrice de correlation sur les 30 symptomes discriminants...") | |
| log("Chargement du KPI D5 - Correlation entre symptomes...") | |
| # Matrice de correlation limitee aux 30 symptomes discriminants (328x328 = trop large) | |
| corr_matrix_30 = df[top30_discriminants].corr().round(3) | |
| # Paires avec correlation forte (r > 0.7 en valeur absolue, hors diagonale) | |
| mask_upper = np.triu(np.ones(corr_matrix_30.shape), k=1).astype(bool) | |
| corr_upper = corr_matrix_30.where(mask_upper) | |
| paires_fortes = corr_upper[corr_upper.abs() > 0.7].stack().reset_index() | |
| paires_fortes.columns = ["Symptome_A", "Symptome_B", "Correlation"] | |
| paires_fortes = paires_fortes.sort_values("Correlation", key=abs, ascending=False) | |
| print(f"Paires de symptomes avec |r| > 0.7 : {len(paires_fortes)}") | |
| if len(paires_fortes) > 0: | |
| print(paires_fortes.head(20).to_string(index=False)) | |
| # Heatmap de correlation | |
| fig, ax = plt.subplots(figsize=(16, 14)) | |
| mask_diag = np.eye(corr_matrix_30.shape[0], dtype=bool) | |
| sns.heatmap( | |
| corr_matrix_30, mask=mask_diag, | |
| cmap=PALETTE_DIVERGENTE, center=0, | |
| vmin=-1, vmax=1, | |
| annot=False, linewidths=0.2, | |
| ax=ax, cbar_kws={"label": "Coefficient de correlation"} | |
| ) | |
| ax.set_title("Matrice de correlation — 30 symptomes les plus discriminants") | |
| ax.tick_params(axis="x", rotation=45, labelsize=7) | |
| ax.tick_params(axis="y", rotation=0, labelsize=7) | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_d5_correlation_symptomes.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_d5_correlation_symptomes.png") | |
| # In[ ]: | |
| # KPI E1 : Maladies dominantes par tranche d'age | |
| print("\n=== KPI E1 : Maladies dominantes par tranche d'age ===\n") | |
| log("Chargement du KPI E1 - Maladies dominantes par tranche d'age...") | |
| # Top 5 maladies par tranche d'age | |
| top5_par_tranche = ( | |
| df.groupby(["age_group", "disease"], observed=True) | |
| .size() | |
| .reset_index(name="nb_cas") | |
| .sort_values(["age_group", "nb_cas"], ascending=[True, False]) | |
| .groupby("age_group", observed=True) | |
| .head(5) | |
| .reset_index(drop=True) | |
| ) | |
| print("Top 5 maladies par tranche d'age :") | |
| print(top5_par_tranche.to_string(index=False)) | |
| # Graphe interactif plotly (barres groupees par tranche) | |
| fig_plotly = px.bar( | |
| top5_par_tranche, | |
| x="disease", y="nb_cas", color="age_group", | |
| barmode="group", | |
| title="Top 5 maladies par tranche d'age", | |
| labels={"disease": "Maladie", "nb_cas": "Nombre de cas", "age_group": "Tranche d'age"} | |
| ) | |
| fig_plotly.update_layout(xaxis_tickangle=-40, bargap=0, bargroupgap=0) | |
| fig_plotly.write_html(f"{WORKING_DIR}/kpi_e1_maladies_par_age.html") | |
| fig_plotly.show() | |
| # In[ ]: | |
| # KPI E2 : Antecedents medicaux x maladies | |
| print("\n=== KPI E2 : Antecedents medicaux x maladies ===\n") | |
| log("Chargement du KPI E2 - Antecedents medicaux x maladies...") | |
| # Top 5 maladies associees a chaque antecedent | |
| top5_par_antecedent = ( | |
| df.groupby(["medical_history_filled", "disease"]) | |
| .size() | |
| .reset_index(name="nb_cas") | |
| .sort_values(["medical_history_filled", "nb_cas"], ascending=[True, False]) | |
| .groupby("medical_history_filled") | |
| .head(5) | |
| .reset_index(drop=True) | |
| ) | |
| print("Top 5 maladies par antecedent medical :") | |
| print(top5_par_antecedent.to_string(index=False)) | |
| # Graphe : une figure globale avec subplots par antecedent | |
| antecedents_liste = df["medical_history_filled"].unique().tolist() | |
| nb_antecedents = len(antecedents_liste) | |
| ncols = 2 | |
| nrows = (nb_antecedents + 1) // ncols | |
| fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=(16, nrows * 4)) | |
| axes = axes.flatten() | |
| for i, antecedent in enumerate(sorted(antecedents_liste)): | |
| sous_df = top5_par_antecedent[top5_par_antecedent["medical_history_filled"] == antecedent] | |
| sns.barplot( | |
| data=sous_df, x="nb_cas", y="disease", | |
| color=PALETTE_PRINCIPALE, ax=axes[i] | |
| ) | |
| axes[i].set_title(f"Antecedent : {antecedent}") | |
| axes[i].set_xlabel("Nb de cas") | |
| axes[i].set_ylabel("") | |
| # Masquer les axes inutilises si le nombre d'antecedents est impair | |
| for j in range(i + 1, len(axes)): | |
| axes[j].set_visible(False) | |
| plt.suptitle("Top 5 maladies par antecedent medical", fontsize=14, y=1.01) | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_e2_antecedents_maladies.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_e2_antecedents_maladies.png") | |
| # In[ ]: | |
| # KPI E3 : Nombre moyen de symptomes selon l'age et le sexe | |
| print("\n=== KPI E3 : Nombre moyen de symptomes selon l'age et le sexe ===\n") | |
| log("Chargement du KPI E3 - Nombre moyen de symptomes selon l'age et le sexe...") | |
| pivot_e3 = df.groupby(["age_group", "sex"], observed=True)["nb_symptomes"].mean().unstack() | |
| pivot_e3 = pivot_e3.round(2) | |
| print("Nombre moyen de symptomes par tranche d'age et sexe :") | |
| print(pivot_e3) | |
| # Analyse | |
| # Si les valeurs augmentent avec l'age : les patients plus ages presentent plus de symptomes | |
| # simultanement — coherent avec une comorbidite accrue chez les personnes agees. | |
| # Si les valeurs sont similaires entre hommes et femmes : pas de biais de genre dans | |
| # la complexite symptomatique. Sinon, signaler l'ecart. | |
| # Heatmap | |
| fig, ax = plt.subplots(figsize=(8, 6)) | |
| sns.heatmap( | |
| pivot_e3, annot=True, fmt=".2f", | |
| cmap=PALETTE_SEQUENTIELLE, ax=ax, | |
| cbar_kws={"label": "Nb moyen de symptomes"} | |
| ) | |
| ax.set_title("Nombre moyen de symptomes par tranche d'age et sexe") | |
| ax.set_xlabel("Sexe") | |
| ax.set_ylabel("Tranche d'age") | |
| plt.tight_layout() | |
| plt.savefig(f"{WORKING_DIR}/kpi_e3_symptomes_age_sexe.png") | |
| plt.show() | |
| print("Graphe sauvegarde : kpi_e3_symptomes_age_sexe.png") | |
| # In[ ]: | |
| # KPI F1 : Dashboard de synthese | |
| print("\n=== KPI F1 : Dashboard de synthese ===\n") | |
| log("Chargement du KPI F1 - Dashboard de synthese...") | |
| fig = plt.figure(figsize=(20, 24)) | |
| fig.suptitle("Dashboard KPI — Dataset Medical", fontsize=16, fontweight="bold", y=0.98) | |
| # Panneau 1 : Top 20 maladies | |
| ax1 = fig.add_subplot(3, 2, 1) | |
| sns.barplot(data=top20, x="Nb_cas", y="Maladie", color="steelblue", ax=ax1) | |
| ax1.set_title("Top 20 maladies (effectif)") | |
| ax1.set_xlabel("Nb de cas") | |
| ax1.set_ylabel("") | |
| # Panneau 2 : Repartition par sexe | |
| ax2 = fig.add_subplot(3, 2, 2) | |
| ax2.pie(dist_sexe.values, labels=dist_sexe.index, autopct="%1.1f%%", | |
| colors=["#5b9bd5", "#ed7d31"], startangle=90) | |
| ax2.set_title("Repartition par sexe") | |
| # Panneau 3 : Distribution de l'age | |
| ax3 = fig.add_subplot(3, 2, 3) | |
| ax3.hist(df["age"], bins=50, color="steelblue", edgecolor="white") | |
| ax3.set_title("Distribution de l'age") | |
| ax3.set_xlabel("Age") | |
| ax3.set_ylabel("Nb de patients") | |
| # Panneau 4 : Top 20 symptomes | |
| ax4 = fig.add_subplot(3, 2, 4) | |
| top20_symp_dash = prevalence_symptomes.head(20).reset_index() | |
| top20_symp_dash.columns = ["Symptome", "Prevalence"] | |
| sns.barplot(data=top20_symp_dash, x="Prevalence", y="Symptome", | |
| color="teal", ax=ax4) | |
| ax4.set_title("Top 20 symptomes (prevalence %)") | |
| ax4.set_xlabel("Prevalence (%)") | |
| ax4.set_ylabel("") | |
| # Panneau 5 : Antecedents medicaux | |
| ax5 = fig.add_subplot(3, 2, 5) | |
| sns.barplot(data=dist_antecedents, y="Antecedent", x="Nb_patients", | |
| color="mediumpurple", ax=ax5) | |
| ax5.set_title("Antecedents medicaux") | |
| ax5.set_xlabel("Nb de patients") | |
| ax5.set_ylabel("") | |
| # Panneau 6 : Distribution effectifs par maladie | |
| ax6 = fig.add_subplot(3, 2, 6) | |
| ax6.hist(counts_maladies.values, bins=50, color="coral", edgecolor="white") | |
| ax6.set_title("Distribution des effectifs par maladie") | |
| ax6.set_xlabel("Nb de cas") | |
| ax6.set_ylabel("Nb de maladies") | |
| plt.tight_layout(rect=[0, 0, 1, 0.97]) | |
| plt.savefig(f"{WORKING_DIR}/kpi_f1_dashboard_synthese.png", dpi=150, bbox_inches="tight") | |
| plt.show() | |
| print("Dashboard sauvegarde : kpi_f1_dashboard_synthese.png") | |
| # In[ ]: | |
| # KPI F2 : Export JSON | |
| print("\n=== KPI F2 : Export JSON ===\n") | |
| log("Exportation...") | |
| # ------------------------------------------------------------------------- | |
| # SECTION 1 : Données brutes (KPIs calculés — inchangé) | |
| # ------------------------------------------------------------------------- | |
| kpis_export = { | |
| "meta": { | |
| "nb_lignes": int(nb_lignes), | |
| "nb_colonnes": int(nb_colonnes), | |
| "nb_symptomes": int(nb_symptomes), | |
| "nb_maladies_distinctes": int(nb_maladies_distinctes), | |
| "taux_manquants_global_pct": round(float(taux_global), 4), | |
| "taux_manquants_medical_history_pct": round(float(taux_nan_antecedents), 2) | |
| }, | |
| "maladies": { | |
| "distribution_stats": { | |
| "min_cas": int(counts_maladies.min()), | |
| "max_cas": int(counts_maladies.max()), | |
| "moyenne_cas": round(float(counts_maladies.mean()), 2), | |
| "mediane_cas": round(float(counts_maladies.median()), 2), | |
| "ecart_type_cas": round(float(counts_maladies.std()), 2), | |
| "ratio_desequilibre": round(float(ratio_desequilibre), 4), | |
| "gini": round(float(gini), 4) | |
| }, | |
| "top20": top20.to_dict(orient="records"), | |
| "bottom20": bottom20.to_dict(orient="records") | |
| }, | |
| "demographie": { | |
| "age": { | |
| "min": float(df["age"].min()), | |
| "max": float(df["age"].max()), | |
| "moyenne": round(float(df["age"].mean()), 2), | |
| "mediane": round(float(df["age"].median()), 2), | |
| "ecart_type": round(float(df["age"].std()), 2), | |
| "par_tranche": dist_age.to_dict(orient="records") | |
| }, | |
| "sexe": { | |
| "global_pct": dist_sexe_pct.to_dict(), | |
| "top10_dominante_feminine": top10_fem.to_dict(orient="records"), | |
| "top10_dominante_masculine": top10_masc.to_dict(orient="records") | |
| }, | |
| "antecedents": { | |
| "distribution": dist_antecedents.to_dict(orient="records"), | |
| "taux_manquants_pct": round(float(taux_nan_antecedents), 2) | |
| }, | |
| "age_par_maladie": { | |
| "top10_plus_jeunes": top10_jeunes.to_dict(orient="records"), | |
| "top10_plus_ages": top10_ages.to_dict(orient="records") | |
| } | |
| }, | |
| "symptomes": { | |
| "nb_symptomes_par_patient": { | |
| "min": int(df["nb_symptomes"].min()), | |
| "max": int(df["nb_symptomes"].max()), | |
| "moyenne": round(float(df["nb_symptomes"].mean()), 2), | |
| "mediane": float(df["nb_symptomes"].median()), | |
| "patients_sans_symptome": int(nb_sans_symptome) | |
| }, | |
| "prevalence_globale": { | |
| "top20": top20_symp.to_dict(orient="records"), | |
| "bottom20": bottom20_symp.to_dict(orient="records"), | |
| "nb_quasi_absents_inf1pct": int(len(quasi_absents)), | |
| "nb_quasi_universels_sup90pct": int(len(quasi_universels)) | |
| }, | |
| "top30_discriminants": top30_disc_df.to_dict(orient="records"), | |
| "complexite_par_maladie": { | |
| "top10_simples": top10_simple.to_dict(orient="records"), | |
| "top10_complexes": top10_complexe.to_dict(orient="records") | |
| } | |
| }, | |
| "croisements": { | |
| "top5_maladies_par_tranche_age": top5_par_tranche.to_dict(orient="records"), | |
| "top5_maladies_par_antecedent": top5_par_antecedent.to_dict(orient="records"), | |
| "nb_symptomes_moyen_age_sexe": pivot_e3.reset_index().to_dict(orient="records") | |
| } | |
| } | |
| # ------------------------------------------------------------------------- | |
| # SECTION 2 : Données pré-formatées Chart.js (nouvelles clés) | |
| # | |
| # Ces clés sont consommées directement par les composants DataChart du | |
| # frontend Vue sans aucune transformation côté client. Chaque objet suit | |
| # exactement la structure { labels: [...], datasets: [{ label, data }] } | |
| # attendue par Chart.js v3+. | |
| # ------------------------------------------------------------------------- | |
| kpis_export["chartjs"] = { | |
| # --- DataChart B1 : Top 20 maladies — graphe en barres verticales --- | |
| # Colonne "Maladie" = labels de l'axe X | |
| # Colonne "Nb_cas" = valeurs de la série | |
| # [ACTION MANUELLE] : si le nom de colonne dans top20 diffère | |
| # ("disease" au lieu de "Maladie"), ajuster les clés ci-dessous. | |
| "top20_maladies": { | |
| "labels": top20["Maladie"].tolist(), | |
| "datasets": [{ | |
| "label": "Nombre de cas", | |
| "data": top20["Nb_cas"].tolist() | |
| }] | |
| }, | |
| # --- DataChart C2 : Répartition par sexe — graphe en anneau (doughnut) --- | |
| # dist_sexe_pct est un dict {"Female": 54.8, "Male": 45.2} | |
| # L'ordre des clés est préservé par Python 3.7+ (insertion order). | |
| "sexe_global": { | |
| "labels": list(dist_sexe_pct.keys()), | |
| "datasets": [{ | |
| "data": [round(float(v), 1) for v in dist_sexe_pct.values()] | |
| }] | |
| }, | |
| # --- DataChart D4 : Top 5 symptômes discriminants — barres horizontales --- | |
| # On limite volontairement à 5 pour la lisibilité du graphe frontend. | |
| # Le reste des 30 est disponible dans symptomes.top30_discriminants | |
| # pour un éventuel tableau détaillé ou une vue expandable. | |
| # [DISCUSSION] : Vérifier que la colonne "Score" correspond bien au | |
| # coefficient de discrimination calculé (chi2, mutual info, ou autre). | |
| # Si la colonne s'appelle autrement, adapter la clé ci-dessous. | |
| "top5_discriminants": { | |
| "labels": top30_disc_df["Symptome"].head(5).tolist(), | |
| "datasets": [{ | |
| "label": "Score discriminant", | |
| "data": top30_disc_df["Score"].head(5).round(4).tolist() | |
| }] | |
| }, | |
| # --- DataChart supplémentaire : Distribution par tranche d'âge --- | |
| # Graphe en barres — utile pour la section démographie de la page. | |
| # dist_age est un DataFrame avec colonnes "Tranche" et "Nb_patients". | |
| # [ACTION MANUELLE] : vérifier les noms de colonnes exacts dans dist_age. | |
| "age_par_tranche": { | |
| "labels": dist_age["Tranche"].tolist(), | |
| "datasets": [{ | |
| "label": "Nombre de patients", | |
| "data": dist_age["Nb_patients"].tolist() | |
| }] | |
| }, | |
| # --- DataChart supplémentaire : Antécédents médicaux — barres horizontales --- | |
| # dist_antecedents est un DataFrame avec colonnes "Antecedent" et "Nb_patients". | |
| "antecedents": { | |
| "labels": dist_antecedents["Antecedent"].tolist(), | |
| "datasets": [{ | |
| "label": "Nombre de patients", | |
| "data": dist_antecedents["Nb_patients"].tolist() | |
| }] | |
| }, | |
| # --- DataChart D5 : Top 20 symptômes les plus prévalents — barres horizontales --- | |
| # prevalence_symptomes est une Series indexée par nom de symptôme, valeurs en %. | |
| # On prend les 20 premiers triés par prévalence décroissante. | |
| "top20_symptomes": { | |
| "labels": prevalence_symptomes.head(20).index.tolist(), | |
| "datasets": [{ | |
| "label": "Prévalence (%)", | |
| "data": prevalence_symptomes.head(20).round(2).tolist() | |
| }] | |
| }, | |
| # --- DataChart E1 : Maladie dominante par tranche d'âge --- | |
| # top5_par_tranche est un DataFrame avec colonnes "Tranche_age", "Maladie", "Nb_cas". | |
| # On extrait la maladie N°1 par tranche (rang 1 après groupby + rank). | |
| # Le graphe est un bar chart simple : une barre par tranche, hauteur = Nb_cas, | |
| # label = nom de la maladie dominante affiché en tooltip. | |
| # [ACTION MANUELLE] : vérifier les noms de colonnes exacts dans top5_par_tranche. | |
| "maladies_par_tranche_age": (lambda df: { | |
| "labels": df["Tranche_age"].tolist(), | |
| "datasets": [{ | |
| "label": "Leading disease (case count)", | |
| "data": df["Nb_cas"].tolist(), | |
| "disease_names": df["Maladie"].tolist() | |
| }] | |
| })(top5_par_tranche[top5_par_tranche["Rang"] == 1].sort_values("Tranche_age")), | |
| # --- DataChart E2 : Maladie dominante par antécédent médical --- | |
| # top5_par_antecedent est un DataFrame avec colonnes "Antecedent", "Maladie", "Nb_cas". | |
| # On extrait la maladie N°1 par antécédent. | |
| # [ACTION MANUELLE] : vérifier les noms de colonnes exacts dans top5_par_antecedent. | |
| "maladies_par_antecedent": (lambda df: { | |
| "labels": df["Antecedent"].tolist(), | |
| "datasets": [{ | |
| "label": "Leading disease (case count)", | |
| "data": df["Nb_cas"].tolist(), | |
| "disease_names": df["Maladie"].tolist() | |
| }] | |
| })(top5_par_antecedent[top5_par_antecedent["Rang"] == 1]), | |
| # --- DataChart E3 : Nombre moyen de symptômes par tranche d'âge et sexe --- | |
| # pivot_e3 est un pivot table : index = tranche d'âge, colonnes = Female / Male. | |
| # On génère deux datasets (une série par sexe) pour un grouped bar chart. | |
| # [ACTION MANUELLE] : vérifier que pivot_e3.columns contient bien "Female" et "Male". | |
| "symptomes_age_sexe": { | |
| "labels": pivot_e3.index.tolist(), | |
| "datasets": [ | |
| { | |
| "label": "Female", | |
| "data": pivot_e3["Female"].round(2).tolist() | |
| }, | |
| { | |
| "label": "Male", | |
| "data": pivot_e3["Male"].round(2).tolist() | |
| } | |
| ] | |
| } | |
| } | |
| # ------------------------------------------------------------------------- | |
| # SECTION 3 : Export du fichier JSON | |
| # ------------------------------------------------------------------------- | |
| kpis_export["last_update"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| kpis_export["last_update_duration"] = time_lapse() | |
| with open(KPIS_PATH, "w", encoding="utf-8") as f: | |
| json.dump(kpis_export, f, ensure_ascii=False, indent=2) | |
| taille_json_kb = os.path.getsize(KPIS_PATH) / 1024 | |
| print(f"Export JSON termine : {KPIS_PATH}") | |
| print(f"Taille du fichier : {taille_json_kb:.1f} Ko") | |
| print(f"\nCles exportees :") | |
| for cle in kpis_export.keys(): | |
| if cle == "chartjs": | |
| for sous_cle in kpis_export["chartjs"].keys(): | |
| print(f" chartjs.{sous_cle}") | |
| else: | |
| print(f" {cle}") | |
| # ------------------------------------------------------------------------- | |
| # SECTION 4 : Verification rapide de coherence | |
| # Controle que les clés Chart.js ont bien la structure attendue avant | |
| # de copier le JSON dans le frontend. | |
| # ------------------------------------------------------------------------- | |
| print("\nVerification de coherence Chart.js :") | |
| for nom, obj in kpis_export["chartjs"].items(): | |
| nb_labels = len(obj["labels"]) | |
| nb_data = len(obj["datasets"][0]["data"]) | |
| statut = "OK" if nb_labels == nb_data else "ERREUR — longueurs differentes" | |
| print(f" {nom:30s} labels={nb_labels:3d} data={nb_data:3d} {statut}") | |
| print("\n=== Fin du notebook KPI Medical ===") | |
| print("\nFichiers generes :") | |
| fichiers_generes = [ | |
| "kpi_a2_valeurs_manquantes.png", | |
| "kpi_b1_top20_maladies.png", | |
| "kpi_b1_bottom20_maladies.png", | |
| "kpi_b2_distribution_effectifs.png", | |
| "kpi_c1_tranches_age.png", | |
| "kpi_c2_sexe_global.png", | |
| "kpi_c2_sexe_top20_maladies.png", | |
| "kpi_c3_antecedents.png", | |
| "kpi_c4_age_par_maladie.png", | |
| "kpi_d1_top20_symptomes.png", | |
| "kpi_d1_bottom20_symptomes.png", | |
| "kpi_d2_nb_symptomes_distribution.png", | |
| "kpi_d2_complexite_maladies.png", | |
| "kpi_d3_heatmap_symptomes_maladies.png", | |
| "kpi_d4_symptomes_discriminants.png", | |
| "kpi_d5_correlation_symptomes.png", | |
| "kpi_e1_maladies_par_age.html (plotly)", | |
| "kpi_e2_antecedents_maladies.png", | |
| "kpi_e3_symptomes_age_sexe.png", | |
| "kpi_f1_dashboard_synthese.png", | |
| "kpis_export.json" | |
| ] | |
| for f in fichiers_generes: | |
| print(f" {f}") | |
| print("Extraction des KPIs Terminée") | |
| log("Extraction des KPIs Terminée") | |
| return 0 | |