""" EMOTYC — Visualisation interactive des performances. Space Gradio avec : - Dropdown pour sélectionner une configuration (5 XLSX pré-chargés) - Tableau HTML de performances (F1, Précision, Rappel, FN/FP/TN/TP) - Cellules FN/FP/TN/TP cliquables → panneau d'instances concrétes """ from __future__ import annotations import json from dataclasses import dataclass, field from html import escape as html_escape from pathlib import Path from typing import Any import gradio as gr import numpy as np import pandas as pd # ═══════════════════════════════════════════════════════════════════════════ # CONSTANTS # ═══════════════════════════════════════════════════════════════════════════ BASE_DIR = Path(__file__).resolve().parent DATA_DIR = BASE_DIR / "data" ALL_LABELS = [ "Emo", "Comportementale", "Designee", "Montree", "Suggeree", "Base", "Complexe", "Admiration", "Autre", "Colere", "Culpabilite", "Degout", "Embarras", "Fierte", "Jalousie", "Joie", "Peur", "Surprise", "Tristesse", ] PRED_SUFFIX = "_pred_emotyc" DISPLAY_NAMES = { "Colere": "Colère", "Culpabilite": "Culpabilité", "Degout": "Dégoût", "Fierte": "Fierté", "Designee": "Désignée", "Montree": "Montrée", "Suggeree": "Suggérée", "Emo": "Émo", } OUTCOME_DISPLAY = { "tp": "✅ Vrais Positifs (TP)", "fp": "⚠️ Faux Positifs (FP)", "fn": "❌ Faux Négatifs (FN)", "tn": "✓ Vrais Négatifs (TN)", } # Configuration name → XLSX filename CONFIGS: dict[str, str] = { "CyberAggAdo 200": "CyberAggAdo200.parquet", "CyberAggAdo Global — Contexte": "CyberAggAdoGlobal_Context.parquet", "CyberAggAdo Global — Sans Contexte": "CyberAggAdoGlobal_SansContexte.parquet", "TextToKids — Contexte": "TextToKids_Context.parquet", "TextToKids — Sans Contexte": "TextToKids_SansContexte.parquet", } def display_name(label: str) -> str: return DISPLAY_NAMES.get(label, label) # ═══════════════════════════════════════════════════════════════════════════ # DATA STRUCTURES # ═══════════════════════════════════════════════════════════════════════════ @dataclass class LabelMetrics: label: str f1: float precision: float recall: float tp: int fp: int fn: int tn: int @dataclass class ConfigData: name: str df: pd.DataFrame labels: list[str] metrics: list[LabelMetrics] macro_f1: float # Index: label → outcome → list of row indices case_index: dict[str, dict[str, list[int]]] = field(default_factory=dict) # ═══════════════════════════════════════════════════════════════════════════ # LOADING & COMPUTATION # ═══════════════════════════════════════════════════════════════════════════ def load_config(name: str, xlsx_path: Path) -> ConfigData: """Load a single config parquet and compute all metrics + case indices.""" df = pd.read_parquet(xlsx_path) # Detect available labels (must have both gold and pred columns) available = [] for label in ALL_LABELS: pred_col = f"{label}{PRED_SUFFIX}" if label in df.columns and pred_col in df.columns: available.append(label) if not available: raise ValueError(f"No valid label pairs found in {xlsx_path.name}") # Compute metrics and case index metrics_list: list[LabelMetrics] = [] case_index: dict[str, dict[str, list[int]]] = {} for label in available: pred_col = f"{label}{PRED_SUFFIX}" gold = df[label].fillna(0).astype(int).values pred = df[pred_col].fillna(0).astype(int).values tp_mask = (gold == 1) & (pred == 1) fp_mask = (gold == 0) & (pred == 1) fn_mask = (gold == 1) & (pred == 0) tn_mask = (gold == 0) & (pred == 0) tp = int(tp_mask.sum()) fp = int(fp_mask.sum()) fn = int(fn_mask.sum()) tn = int(tn_mask.sum()) prec = tp / (tp + fp) if (tp + fp) > 0 else 0.0 rec = tp / (tp + fn) if (tp + fn) > 0 else 0.0 f1 = (2 * prec * rec / (prec + rec)) if (prec + rec) > 0 else 0.0 metrics_list.append(LabelMetrics( label=label, f1=round(f1, 3), precision=round(prec, 3), recall=round(rec, 3), tp=tp, fp=fp, fn=fn, tn=tn, )) case_index[label] = { "tp": np.where(tp_mask)[0].tolist(), "fp": np.where(fp_mask)[0].tolist(), "fn": np.where(fn_mask)[0].tolist(), "tn": np.where(tn_mask)[0].tolist(), } macro_f1 = round(float(np.mean([m.f1 for m in metrics_list])), 3) return ConfigData( name=name, df=df, labels=available, metrics=metrics_list, macro_f1=macro_f1, case_index=case_index, ) def load_all_configs() -> dict[str, ConfigData]: """Load all configurations at startup.""" configs: dict[str, ConfigData] = {} for name, filename in CONFIGS.items(): path = DATA_DIR / filename if path.exists(): print(f"Chargement : {name} ({filename})") configs[name] = load_config(name, path) print(f" → {len(configs[name].df)} lignes, {len(configs[name].labels)} labels") else: print(f"⚠️ Fichier manquant : {path}") return configs # ═══════════════════════════════════════════════════════════════════════════ # HTML TABLE GENERATION # ═══════════════════════════════════════════════════════════════════════════ def _metric_color(value: float) -> str: if value >= 0.8: return "#15803d" # green if value >= 0.5: return "#b45309" # orange return "#be123c" # red def generate_performance_html(config: ConfigData) -> str: """Generate an interactive HTML performance table with clickable cells.""" rows = [] for m in config.metrics: dname = html_escape(display_name(m.label)) canon = html_escape(m.label) f1_color = _metric_color(m.f1) prec_color = _metric_color(m.precision) rec_color = _metric_color(m.recall) row = f"""
| Label | F1 | Précision | Rappel | FN | FP | TN | TP |
|---|
Configuration non trouvée.
", pd.DataFrame() config = ALL_CONFIGS[config_name] html = generate_performance_html(config) return html, pd.DataFrame() def on_cell_click( cell_value: str, config_name: str ) -> tuple[str, pd.DataFrame, str]: """When user clicks a TP/FP/TN/FN cell.""" if not cell_value or "|" not in cell_value: return "", pd.DataFrame(), "" label, outcome = cell_value.split("|", 1) if config_name not in ALL_CONFIGS: return "Configuration introuvable.", pd.DataFrame(), "" config = ALL_CONFIGS[config_name] title, instances_df = get_instances(config, label, outcome) return title, instances_df, "" HEAD_JS = """ """ # ── Build Gradio interface ───────────────────────────────────────────── HEADER_MD = """ # 📊 EMOTYC — Visualisation des Performances Sélectionnez une configuration pour afficher le tableau de performances du modèle de détection des émotions. **Cliquez sur les cellules FN, FP, TN ou TP** pour explorer les instances concrètes. """ with gr.Blocks( title="EMOTYC — Performances", theme=gr.themes.Soft( primary_hue=gr.themes.colors.indigo, secondary_hue=gr.themes.colors.slate, neutral_hue=gr.themes.colors.slate, font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], ), css=""" .main-container { max-width: 1100px; margin: 0 auto; } #cell_click_input { display: none !important; } .instance-panel { margin-top: 8px; } footer { display: none !important; } """, head=HEAD_JS, ) as demo: with gr.Column(elem_classes="main-container"): gr.Markdown(HEADER_MD) with gr.Row(): config_dropdown = gr.Dropdown( choices=list(ALL_CONFIGS.keys()), value=DEFAULT_CONFIG, label="Configuration", interactive=True, scale=3, ) # Performance table (HTML) perf_html = gr.HTML(label="Tableau de performances") # Hidden textbox for JS → Python communication cell_click_input = gr.Textbox( value="", visible=False, elem_id="cell_click_input", ) # Instance panel with gr.Column(elem_classes="instance-panel", visible=True): instance_title = gr.Markdown("") instance_table = gr.Dataframe( value=pd.DataFrame(), label="Instances", interactive=False, wrap=True, max_height=500, ) # ── Events ───────────────────────────────────────────────────────── config_dropdown.change( fn=on_config_change, inputs=[config_dropdown], outputs=[perf_html, instance_table], ) cell_click_input.change( fn=on_cell_click, inputs=[cell_click_input, config_dropdown], outputs=[instance_title, instance_table, cell_click_input], ) # Load default config on startup demo.load( fn=on_config_change, inputs=[config_dropdown], outputs=[perf_html, instance_table], ) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)