import gradio as gr import pandas as pd import matplotlib.pyplot as plt from pathlib import Path from datetime import date import numpy as np import tempfile # Fichier CSV stocké dans le dossier du Space (/app/grades.csv) DATA_PATH = Path("grades.csv") def download_plot(student): """Génère un fichier PNG du graphique et le retourne pour téléchargement.""" fig = plot_student(student) # on récupère la figure existante tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png") fig.savefig(tmp.name, dpi=150, bbox_inches="tight") return tmp.name def load_data(): """Charge les données depuis grades.csv ou crée un DataFrame vide.""" if DATA_PATH.exists(): df = pd.read_csv(DATA_PATH) # Normalisation basique des noms de colonnes pour être robuste col_map = {} for c in df.columns: cl = c.lower() if cl in ("etudiant", "étudiant", "eleve", "élève"): col_map[c] = "eleve" elif cl in ("matiere", "matière", "devoir"): col_map[c] = "devoir" elif cl == "note": col_map[c] = "note" elif cl == "date": col_map[c] = "date" if col_map: df = df.rename(columns=col_map) # On garantit la présence des 4 colonnes for col in ["eleve", "date", "devoir", "note"]: if col not in df.columns: df[col] = pd.NA return df[["eleve", "date", "devoir", "note"]] else: return pd.DataFrame(columns=["eleve", "date", "devoir", "note"]) # DataFrame global en mémoire df = load_data() def get_eleve_list(df_local: pd.DataFrame): if df_local.empty: return [] return sorted(df_local["eleve"].dropna().unique().tolist()) def get_devoir_list(df_local: pd.DataFrame): if df_local.empty: return [] return sorted(df_local["devoir"].dropna().unique().tolist()) def add_grade(eleve, devoir, note, date_input): """Ajoute une note au DataFrame + sauvegarde CSV.""" global df eleve = (eleve or "").strip() devoir = (devoir or "").strip() if not eleve: return ( "Erreur : le nom de l'élève est obligatoire.", gr.update(), df, gr.update(), ) try: note_float = float(note) except Exception: return ( "Erreur : la note doit être un nombre.", gr.update(), df, gr.update(), ) try: if date_input: d = pd.to_datetime(date_input).date() else: d = date.today() except Exception: d = date.today() new_row = { "eleve": eleve, "date": d.isoformat(), "devoir": devoir if devoir else "Devoir", "note": note_float, } df = pd.concat([df, pd.DataFrame([new_row])], ignore_index=True) DATA_PATH.parent.mkdir(parents=True, exist_ok=True) df.to_csv(DATA_PATH, index=False) eleves = get_eleve_list(df) devoirs = get_devoir_list(df) msg = f"Note ajoutée pour {eleve} : {note_float} ({new_row['devoir']}, {d})." return msg, gr.update(choices=eleves, value=eleve), df, gr.update(choices=devoirs) def refresh_all(): """Recharge le CSV et met à jour tableau, élèves, devoirs.""" global df df = load_data() eleves = get_eleve_list(df) devoirs = get_devoir_list(df) return df, gr.update(choices=eleves), gr.update(choices=devoirs) def plot_student(eleve): """Retourne une figure matplotlib avec l'évolution des notes d'un élève.""" global df fig, ax = plt.subplots() if not eleve or df.empty: ax.set_title("Pas encore de notes") ax.set_xlabel("Date") ax.set_ylabel("Note") return fig sub = df[df["eleve"] == eleve].copy() if sub.empty: ax.set_title(f"Aucune note pour {eleve}") ax.set_xlabel("Date") ax.set_ylabel("Note") return fig # Conversion robuste de la colonne date if not np.issubdtype(sub["date"].dtype, np.datetime64): sub["date"] = pd.to_datetime( sub["date"], format="mixed", errors="coerce", ) sub = sub.dropna(subset=["date"]) sub = sub.sort_values("date") if sub.empty: ax.set_title(f"Aucune date valide pour {eleve}") ax.set_xlabel("Date") ax.set_ylabel("Note") return fig ax.plot(sub["date"], sub["note"], marker="o") ax.set_xlabel("Date") ax.set_ylabel("Note") ax.set_title(f"Évolution des notes - {eleve}") ax.set_ylim(0, 20) # si tu notes sur 20 fig.autofmt_xdate() return fig #def compute_stats(devoir): # """Calcule la moyenne et le classement pour un devoir.""" # global df # if not devoir or df.empty: # empty = pd.DataFrame(columns=["rang", "eleve", "note"]) # return "Pas de données pour ce devoir.", empty # sub = df[df["devoir"] == devoir].copy() # if sub.empty: # empty = pd.DataFrame(columns=["rang", "eleve", "note"]) # return f"Aucune note trouvée pour le devoir « {devoir} ».", empty # sub = sub.dropna(subset=["note", "eleve"]) # if sub.empty: # empty = pd.DataFrame(columns=["rang", "eleve", "note"]) # return f"Aucune note exploitable pour le devoir « {devoir} ».", empty # sub = sub.sort_values("note", ascending=False) # sub["rang"] = range(1, len(sub) + 1) # moyenne = sub["note"].mean() # resume = f"Moyenne pour « {devoir} » : {moyenne:.2f} / 20 ({len(sub)} élèves)." # classement = sub[["rang", "eleve", "note"]] # return resume, classement def compute_stats(devoir): """Calcule stats pour un devoir : moyenne, écart-type, classement, histogramme.""" global df # Figure par défaut pour l'histogramme fig, ax = plt.subplots() if not devoir or df.empty: stats = "Pas de données pour ce devoir." ax.set_title("Aucune note à afficher") ax.set_xlabel("Note") ax.set_ylabel("Nombre d'élèves") return stats, pd.DataFrame(), fig # ✅ ici on filtre sur la colonne 'devoir' sub = df[df["devoir"] == devoir].copy() if sub.empty: stats = "Pas de données pour ce devoir." ax.set_title("Aucune note à afficher") ax.set_xlabel("Note") ax.set_ylabel("Nombre d'élèves") return stats, pd.DataFrame(), fig # ✅ colonnes 'note' (minuscule) n = len(sub) mean = sub["note"].mean() std = sub["note"].std() min_v = sub["note"].min() max_v = sub["note"].max() stats = ( f"Devoir : {devoir}\n" f"Nombre d'élèves : {n}\n" f"Moyenne : {mean:.2f}\n" f"Écart-type : {std:.2f}\n" f"Minimum : {min_v:.2f}\n" f"Maximum : {max_v:.2f}") # ✅ classement : colonnes 'eleve' et 'note' classement = sub[["eleve", "note"]].copy() classement = classement.sort_values("note", ascending=False).reset_index(drop=True) classement.index += 1 # classement commence à 1 # ✅ histogramme sur 'note' ax.hist(sub["note"], bins=10, range=(0, 20), edgecolor="black") ax.set_title(f"Répartition des notes - {devoir}") ax.set_xlabel("Note") ax.set_ylabel("Nombre d'élèves") return stats, classement, fig def import_csv(file_obj): """Importe un CSV et remplace les données courantes.""" global df if file_obj is None: return "Aucun fichier sélectionné.", df, gr.update(), gr.update() try: df_new = pd.read_csv(file_obj.name) except Exception as e: return f"Erreur lors de la lecture du CSV : {e}", df, gr.update(), gr.update() # Normalisation des colonnes col_map = {} for c in df_new.columns: cl = c.lower() if cl in ("etudiant", "étudiant", "eleve", "élève"): col_map[c] = "eleve" elif cl in ("matiere", "matière", "devoir"): col_map[c] = "devoir" elif cl == "note": col_map[c] = "note" elif cl == "date": col_map[c] = "date" if col_map: df_new = df_new.rename(columns=col_map) required = {"eleve", "date", "devoir", "note"} if not required.issubset(df_new.columns): msg = "Le fichier CSV doit contenir les colonnes : eleve, date, devoir, note." return msg, df, gr.update(), gr.update() df_new = df_new[["eleve", "date", "devoir", "note"]] df = df_new.reset_index(drop=True) DATA_PATH.parent.mkdir(parents=True, exist_ok=True) df.to_csv(DATA_PATH, index=False) eleves = get_eleve_list(df) devoirs = get_devoir_list(df) status = f"Fichier importé avec succès ({len(df)} lignes)." return status, df, gr.update(choices=eleves), gr.update(choices=devoirs) def get_csv_file(): """Retourne le chemin vers le CSV pour téléchargement.""" global df DATA_PATH.parent.mkdir(parents=True, exist_ok=True) df.to_csv(DATA_PATH, index=False) return str(DATA_PATH) with gr.Blocks(title="Suivi des notes") as demo: gr.Markdown( """ #Suivi des notes des élèves. Bienvenue :) """ ) # ---------- Onglet 1 : Ajouter une note ---------- with gr.Tab("Ajouter une note"): with gr.Row(): eleve_input = gr.Textbox( label="Nom de l'élève", ) devoir_input = gr.Textbox( label="Devoir / Matière", ) with gr.Row(): note_input = gr.Textbox( label="Note", ) date_input = gr.Textbox( label="Date (optionnel, AAAA-MM-JJ)", ) add_button = gr.Button("Ajouter la note ✅") status_output = gr.Textbox(label="Statut", interactive=False) with gr.Tab("Visualiser les notes"): refresh_button = gr.Button("🔄 Recharger les données") eleve_dropdown = gr.Dropdown( label="Choisir un élève", choices=get_eleve_list(df),) with gr.Row(): line_plot = gr.Plot(label="Évolution des notes") table = gr.Dataframe( label="Toutes les notes", value=df, interactive=False, ) # --- Télécharger le graphique --- download_plot_button = gr.Button("📥 Télécharger le graphique") download_plot_file = gr.File(label="Télécharger le PNG") download_plot_button.click( fn=download_plot, inputs=eleve_dropdown, outputs=download_plot_file, ) # ---------- Onglet 2 : Visualiser ---------- #with gr.Tab("Visualiser les notes"): # refresh_button = gr.Button("🔄 Recharger les données") # eleve_dropdown = gr.Dropdown( # label="Choisir un élève", # choices=get_eleve_list(df), # ) # with gr.Row(): # line_plot = gr.Plot(label="Évolution des notes") # table = gr.Dataframe( # label="Toutes les notes", # value=df, # interactive=False, # ) # ---------- Onglet 3 : Statistiques ---------- with gr.Tab("Statistiques par devoir"): devoir_dropdown = gr.Dropdown( label="Choisir un devoir", choices=get_devoir_list(df), ) stats_text = gr.Textbox( label="Moyenne et informations", interactive=False, ) classement_table = gr.Dataframe( label="Classement sur ce devoir", interactive=False, ) hist_plot = gr.Plot( label="Répartition des notes (histogramme)", ) # ---------- Onglet 4 : Données ---------- with gr.Tab("Données (import / export)"): gr.Markdown( "### 📂 Importer / exporter les notes\n" "- Importer un fichier CSV existant pour charger des notes\n" "- Exporter le fichier CSV courant pour le sauvegarder sur votre ordinateur" ) csv_upload = gr.File(label="Importer un fichier CSV", file_types=[".csv"]) import_button = gr.Button("Charger ce CSV") import_status = gr.Textbox(label="Statut de l'import", interactive=False) export_button = gr.Button("Préparer le fichier CSV pour téléchargement") download_file = gr.File(label="Télécharger grades.csv") # ---------- Callbacks ---------- add_button.click( fn=add_grade, inputs=[eleve_input, devoir_input, note_input, date_input], outputs=[status_output, eleve_dropdown, table, devoir_dropdown], ) refresh_button.click( fn=refresh_all, inputs=None, outputs=[table, eleve_dropdown, devoir_dropdown], ) eleve_dropdown.change( fn=plot_student, inputs=eleve_dropdown, outputs=line_plot, ) devoir_dropdown.change(fn=compute_stats, inputs=devoir_dropdown, outputs=[stats_text, classement_table, hist_plot], ) import_button.click( fn=import_csv, inputs=csv_upload, outputs=[import_status, table, eleve_dropdown, devoir_dropdown], ) export_button.click( fn=get_csv_file, inputs=None, outputs=download_file, ) if __name__ == "__main__": demo.launch()