From 0bf0311f2f515d15647cbae6ea8aec56c7fa1c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Sat, 3 Feb 2024 10:46:14 +0100 Subject: [PATCH] =?UTF-8?q?M=C3=A9nage=20+=20Refactoring=20du=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_etudiant.py | 4 +- app/pe/pe_interclasstag.py | 25 +++-- app/pe/pe_jury.py | 162 ++++++++++++------------------ app/pe/pe_semtag.py | 68 +++++++------ app/pe/pe_tabletags.py | 194 +++++++++++++++++++++--------------- app/pe/pe_trajectoire.py | 88 ++++++---------- app/pe/pe_trajectoiretag.py | 12 ++- 7 files changed, 267 insertions(+), 286 deletions(-) diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index f4fc3fff5..bd0a3db40 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -349,8 +349,8 @@ class EtudiantsJuryPE: trajectoire = trajectoire_aggr[aggregat] if trajectoire: # Le semestre terminal de l'étudiant de l'aggrégat - fid = trajectoire.semestre_final.formsemestre_id - formsemestres_terminaux[fid] = trajectoire.semestre_final + fid = trajectoire.formsemestre_final.formsemestre_id + formsemestres_terminaux[fid] = trajectoire.formsemestre_final return formsemestres_terminaux def get_formsemestres(self, semestres_recherches=None): diff --git a/app/pe/pe_interclasstag.py b/app/pe/pe_interclasstag.py index b4da44cf1..428fe3e5f 100644 --- a/app/pe/pe_interclasstag.py +++ b/app/pe/pe_interclasstag.py @@ -1,5 +1,5 @@ from app.comp import moy_sem -from app.pe.pe_tabletags import TableTag +from app.pe.pe_tabletags import TableTag, MoyenneTag from app.pe.pe_etudiant import EtudiantsJuryPE from app.pe.pe_trajectoire import Trajectoire, TrajectoiresJuryPE from app.pe.pe_trajectoiretag import TrajectoireTag @@ -10,7 +10,6 @@ import numpy as np class AggregatInterclasseTag(TableTag): - # ------------------------------------------------------------------------------------------------------------------- def __init__( self, @@ -30,24 +29,30 @@ class AggregatInterclasseTag(TableTag): """ TableTag.__init__(self) - # Le nom self.aggregat = nom_aggregat + """Aggrégat de l'interclassement""" + self.nom = self.get_repr() """Les étudiants diplômés et leurs trajectoires (cf. trajectoires.suivis)""" # TODO self.diplomes_ids = etudiants.etudiants_diplomes self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids} # pour les exports sous forme de dataFrame - self.etudiants = {etudid: etudiants.identites[etudid].etat_civil for etudid in self.diplomes_ids} + self.etudiants = { + etudid: etudiants.identites[etudid].etat_civil + for etudid in self.diplomes_ids + } # Les trajectoires (et leur version tagguées), en ne gardant que celles associées à l'aggrégat self.trajectoires: dict[int, Trajectoire] = {} + """Ensemble des trajectoires associées à l'aggrégat""" for trajectoire_id in trajectoires_jury_pe.trajectoires: trajectoire = trajectoires_jury_pe.trajectoires[trajectoire_id] if trajectoire_id[0] == nom_aggregat: self.trajectoires[trajectoire_id] = trajectoire self.trajectoires_taggues: dict[int, Trajectoire] = {} + """Ensemble des trajectoires tagguées associées à l'aggrégat""" for trajectoire_id in self.trajectoires: self.trajectoires_taggues[trajectoire_id] = trajectoires_taggues[ trajectoire_id @@ -56,26 +61,27 @@ class AggregatInterclasseTag(TableTag): # Les trajectoires suivies par les étudiants du jury, en ne gardant que # celles associées aux diplomés self.suivi: dict[int, Trajectoire] = {} + """Association entre chaque étudiant et la trajectoire tagguée à prendre en + compte pour l'aggrégat""" for etudid in self.diplomes_ids: self.suivi[etudid] = trajectoires_jury_pe.suivi[etudid][nom_aggregat] - self.tags_sorted = self.do_taglist() """Liste des tags (triés par ordre alphabétique)""" # Construit la matrice de notes self.notes = self.compute_notes_matrice() + """Matrice des notes de l'aggrégat""" # Synthétise les moyennes/classements par tag - self.moyennes_tags = {} + self.moyennes_tags: dict[str, MoyenneTag] = {} for tag in self.tags_sorted: moy_gen_tag = self.notes[tag] - self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag) + self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag) # Est significatif ? (aka a-t-il des tags et des notes) self.significatif = len(self.tags_sorted) > 0 - def get_repr(self) -> str: """Une représentation textuelle""" return f"Aggrégat {self.aggregat}" @@ -118,7 +124,4 @@ class AggregatInterclasseTag(TableTag): etudids_communs, tags_communs ] - # Force les nan - df.fillna(np.nan) - return df diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 55752ff4a..f256073a2 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -65,35 +65,15 @@ import pandas as pd class JuryPE(object): - """Classe mémorisant toutes les informations nécessaires pour établir un jury de PE. - Modèle basé sur NotesTable. - - Attributs : - - * diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX) - * juryEtudDict : dictionnaire récapitulant les étudiants participant au jury PE (données administratives + - celles des semestres valides à prendre en compte permettant le calcul des moyennes ... - ``{'etudid : { 'nom', 'prenom', 'civilite', 'diplome', '', }}`` - a - Rq: il contient à la fois les étudiants qui vont être diplomés à la date prévue - et ceux qui sont éliminés (abandon, redoublement, ...) pour affichage alternatif - """ - - # Variables de classe décrivant les aggrégats, leur ordre d'apparition temporelle et - # leur affichage dans les avis latex - - # ------------------------------------------------------------------------------------------------------------------ def __init__(self, diplome): """ - Création d'une table PE sur la base d'un semestre selectionné. De ce semestre est déduit : + Classe mémorisant toutes les informations nécessaires pour établir un jury de PE, sur la base + d'une année de diplôme. De ce semestre est déduit : 1. l'année d'obtention du DUT, 2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés. Args: - sem_base: le FormSemestre donnant le semestre à la base du jury PE - semBase: le dictionnaire sem donnant la base du jury (CB: TODO: A supprimer à long term) - meme_programme: si True, impose un même programme pour tous les étudiants participant au jury, - si False, permet des programmes differents + diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX) """ self.diplome = diplome "L'année du diplome" @@ -101,7 +81,7 @@ class JuryPE(object): self.nom_export_zip = f"Jury_PE_{self.diplome}" "Nom du zip où ranger les fichiers générés" - # Chargement des étudiants à prendre en compte dans le jury + # Chargement des étudiants à prendre en compte Sydans le jury pe_affichage.pe_print( f"""*** Recherche et chargement des étudiants diplômés en { self.diplome}""" @@ -122,7 +102,6 @@ class JuryPE(object): self._gen_xls_synthese_jury_par_tag(zipfile) self._gen_xls_synthese_par_etudiant(zipfile) - # Fin !!!! Tada :) def _gen_xls_diplomes(self, zipfile: ZipFile): @@ -356,7 +335,9 @@ class JuryPE(object): for aggregat in aggregats: # Le dictionnaire par défaut des moyennes - donnees[etudid] |= get_defaut_dict_synthese_aggregat(aggregat, self.diplome) + donnees[etudid] |= get_defaut_dict_synthese_aggregat( + aggregat, self.diplome + ) # La trajectoire de l'étudiant sur l'aggrégat trajectoire = self.trajectoires.suivi[etudid][aggregat] @@ -364,16 +345,17 @@ class JuryPE(object): trajectoire_tagguee = self.trajectoires_tagguees[ trajectoire.trajectoire_id ] - else: - trajectoire_tagguee = None - - # L'interclassement - interclass = self.interclassements_taggues[aggregat] - - # Injection des données dans un dictionnaire - donnees[etudid] |= get_dict_synthese_aggregat(aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome) - + if tag in trajectoire_tagguee.moyennes_tags: + # La trajectoire tagguée + moy_trajectoire_tag = trajectoire_tagguee.moyennes_tags[tag] + if moy_trajectoire_tag.is_significatif(): + # L'interclassement + interclass = self.interclassements_taggues[aggregat] + # Injection des données dans un dictionnaire + donnees[etudid] |= get_dict_synthese_aggregat( + aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome + ) # Fin de l'aggrégat # Construction du dataFrame @@ -424,7 +406,9 @@ class JuryPE(object): for aggregat in aggregats: # Le dictionnaire par défaut des moyennes - donnees[tag] |= get_defaut_dict_synthese_aggregat(aggregat, self.diplome) + donnees[tag] |= get_defaut_dict_synthese_aggregat( + aggregat, self.diplome + ) # La trajectoire de l'étudiant sur l'aggrégat trajectoire = self.trajectoires.suivi[etudid][aggregat] @@ -432,26 +416,25 @@ class JuryPE(object): trajectoire_tagguee = self.trajectoires_tagguees[ trajectoire.trajectoire_id ] - else: - trajectoire_tagguee = None + if tag in trajectoire_tagguee.moyennes_tags: + # L'interclassement + interclass = self.interclassements_taggues[aggregat] - # L'interclassement - interclass = self.interclassements_taggues[aggregat] - - # Injection des données dans un dictionnaire - donnees[tag] |= get_dict_synthese_aggregat(aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome) + # Injection des données dans un dictionnaire + donnees[tag] |= get_dict_synthese_aggregat( + aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome + ) # Fin de l'aggrégat # Construction du dataFrame df = pd.DataFrame.from_dict(donnees, orient="index") # Tri par nom/prénom - df.sort_values( - by=[("", "", "tag")], inplace=True - ) + df.sort_values(by=[("", "", "tag")], inplace=True) return df + def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict: """Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés. Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire @@ -587,7 +570,7 @@ def get_dict_synthese_aggregat( interclassement_taggue: AggregatInterclasseTag, etudid: int, tag: str, - diplome: int + diplome: int, ): """Renvoie le dictionnaire (à intégrer au tableur excel de synthese) traduisant les résultats (moy/class) d'un étudiant à une trajectoire tagguée associée @@ -600,66 +583,43 @@ def get_dict_synthese_aggregat( note = np.nan # Les données de la trajectoire tagguée pour le tag considéré - if trajectoire_tagguee and tag in trajectoire_tagguee.moyennes_tags: - bilan = trajectoire_tagguee.moyennes_tags[tag] + moy_tag = trajectoire_tagguee.moyennes_tags[tag] - # La moyenne de l'étudiant - note = TableTag.get_note_for_df(bilan, etudid) + # Les données de l'étudiant + note = moy_tag.get_note_for_df(etudid) - # Statistiques sur le groupe - if not pd.isna(note) and note != np.nan: - # Les moyennes de cette trajectoire - donnees |= { + classement = moy_tag.get_class_for_df(etudid) + nmin = moy_tag.get_min_for_df() + nmax = moy_tag.get_max_for_df() + nmoy = moy_tag.get_moy_for_df() + + # Statistiques sur le groupe + if not pd.isna(note) and note != np.nan: + # Les moyennes de cette trajectoire + donnees |= { (descr, "", "note"): note, - ( - descr, - NOM_STAT_GROUPE, - "class.", - ): TableTag.get_class_for_df(bilan, etudid), - ( - descr, - NOM_STAT_GROUPE, - "min", - ): TableTag.get_min_for_df(bilan), - ( - descr, - NOM_STAT_GROUPE, - "moy", - ): TableTag.get_moy_for_df(bilan), - ( - descr, - NOM_STAT_GROUPE, - "max", - ): TableTag.get_max_for_df(bilan), + (descr, NOM_STAT_GROUPE, "class."): classement, + (descr, NOM_STAT_GROUPE, "min"): nmin, + (descr, NOM_STAT_GROUPE, "moy"): nmoy, + (descr, NOM_STAT_GROUPE, "max"): nmax, } # L'interclassement - if tag in interclassement_taggue.moyennes_tags: - bilan = interclassement_taggue.moyennes_tags[tag] + moy_tag = interclassement_taggue.moyennes_tags[tag] - if not pd.isna(note) and note != np.nan: - nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}" + classement = moy_tag.get_class_for_df(etudid) + nmin = moy_tag.get_min_for_df() + nmax = moy_tag.get_max_for_df() + nmoy = moy_tag.get_moy_for_df() + + if not pd.isna(note) and note != np.nan: + nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}" + + donnees |= { + (descr, nom_stat_promo, "class."): classement, + (descr, nom_stat_promo, "min"): nmin, + (descr, nom_stat_promo, "moy"): nmoy, + (descr, nom_stat_promo, "max"): nmax, + } - donnees |= { - ( - descr, - nom_stat_promo, - "class.", - ): TableTag.get_class_for_df(bilan, etudid), - ( - descr, - nom_stat_promo, - "min", - ): TableTag.get_min_for_df(bilan), - ( - descr, - nom_stat_promo, - "moy", - ): TableTag.get_moy_for_df(bilan), - ( - descr, - nom_stat_promo, - "max", - ): TableTag.get_max_for_df(bilan), - } return donnees diff --git a/app/pe/pe_semtag.py b/app/pe/pe_semtag.py index 165a49b04..00399845b 100644 --- a/app/pe/pe_semtag.py +++ b/app/pe/pe_semtag.py @@ -38,7 +38,7 @@ Created on Fri Sep 9 09:15:05 2016 import numpy as np import app.pe.pe_etudiant -from app import db, log +from app import db, log, ScoValueError from app.comp import res_sem, moy_ue, moy_sem from app.comp.moy_sem import comp_ranks_series from app.comp.res_compat import NotesTableCompat @@ -49,7 +49,7 @@ from app.models.moduleimpls import ModuleImpl from app.scodoc import sco_tag_module from app.scodoc.codes_cursus import UE_SPORT import app.pe.pe_affichage as pe_affichage -from app.pe.pe_tabletags import TableTag, TAGS_RESERVES +from app.pe.pe_tabletags import TableTag, TAGS_RESERVES, MoyenneTag import pandas as pd @@ -94,44 +94,51 @@ class SemestreTag(TableTag): # Les tags : ## Saisis par l'utilisateur - self.tags_personnalises = get_synthese_tags_personnalises_semestre( + tags_personnalises = get_synthese_tags_personnalises_semestre( self.nt.formsemestre ) ## Déduit des compétences - self.tags_competences = get_noms_competences_from_ues(self.nt.formsemestre) + dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre) - # Supprime les doublons dans les tags - tags_reserves = TAGS_RESERVES + list(self.tags_competences.values()) - for tag in self.tags_personnalises: - if tag in tags_reserves: - del self.tags_personnalises[tag] - pe_affichage.pe_print(f"Supprime le tag {tag}") + self.tags = ( + list(tags_personnalises.keys()) + + list(dict_ues_competences.values()) + + ["but"] + ) + """Tags du semestre taggué""" + + ## Vérifie l'unicité des tags + if len(set(self.tags)) != len(self.tags): + raise ScoValueError( + f"""Erreur dans le module PE : L'un des tags saisis dans le programme + fait parti des tags réservés (par ex. "comp. "). Modifiez les + tags de votre programme""" + ) # Calcul des moyennes & les classements de chaque étudiant à chaque tag self.moyennes_tags = {} - for tag in self.tags_personnalises: + for tag in tags_personnalises: # pe_affichage.pe_print(f" -> Traitement du tag {tag}") - moy_gen_tag = self.compute_moyenne_tag(tag) - self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag) + moy_gen_tag = self.compute_moyenne_tag(tag, tags_personnalises) + self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag) # Ajoute les moyennes générales de BUT pour le semestre considéré moy_gen_but = self.nt.etud_moy_gen - moy_gen_but = pd.to_numeric(moy_gen_but, errors="coerce") - self.moyennes_tags["but"] = self.comp_moy_et_stat(moy_gen_but) + self.moyennes_tags["but"] = MoyenneTag("but", moy_gen_but) # Ajoute les moyennes par compétence - for ue_id, competence in self.tags_competences.items(): + for ue_id, competence in dict_ues_competences.items(): moy_ue = self.nt.etud_moy_ue[ue_id] - self.moyennes_tags[competence] = self.comp_moy_et_stat(moy_ue) + self.moyennes_tags[competence] = MoyenneTag(competence, moy_ue) + + self.tags_sorted = self.get_all_tags() + """Tags (personnalisés+compétences) par ordre alphabétique""" # Synthétise l'ensemble des moyennes dans un dataframe - self.tags_sorted = sorted( - self.moyennes_tags - ) # les tags (personnalisés+compétences) par ordre alphabétique - self.notes = ( - self.df_notes() - ) # Le dataframe synthétique des notes (=moyennes par tag) + + self.notes = self.df_notes() + """Dataframe synthétique des notes par tag""" pe_affichage.pe_print( f" => Traitement des tags {', '.join(self.tags_sorted)}" @@ -141,9 +148,10 @@ class SemestreTag(TableTag): """Nom affiché pour le semestre taggué""" return app.pe.pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True) - def compute_moyenne_tag(self, tag: str) -> list: + def compute_moyenne_tag(self, tag: str, tags_infos: dict) -> pd.Series: """Calcule la moyenne des étudiants pour le tag indiqué, - pour ce SemestreTag. + pour ce SemestreTag, en ayant connaissance des informations sur + les tags (dictionnaire donnant les coeff de repondération) Sont pris en compte les modules implémentés associés au tag, avec leur éventuel coefficient de **repondération**, en utilisant les notes @@ -151,8 +159,8 @@ class SemestreTag(TableTag): Force ou non le calcul de la moyenne lorsque des notes sont manquantes. - Renvoie les informations sous la forme d'une liste - [ (moy, somme_coeff_normalise, etudid), ...] + Returns: + La série des moyennes """ """Adaptation du mask de calcul des moyennes au tag visé""" @@ -163,13 +171,13 @@ class SemestreTag(TableTag): """Désactive tous les modules qui ne sont pas pris en compte pour ce tag""" for i, modimpl in enumerate(self.formsemestre.modimpls_sorted): - if modimpl.moduleimpl_id not in self.tags_personnalises[tag]: + if modimpl.moduleimpl_id not in tags_infos[tag]: modimpls_mask[i] = False """Applique la pondération des coefficients""" modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy() - for modimpl_id in self.tags_personnalises[tag]: - ponderation = self.tags_personnalises[tag][modimpl_id]["ponderation"] + for modimpl_id in tags_infos[tag]: + ponderation = tags_infos[tag][modimpl_id]["ponderation"] modimpl_coefs_ponderes_df[modimpl_id] *= ponderation """Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)""" diff --git a/app/pe/pe_tabletags.py b/app/pe/pe_tabletags.py index 610e5693a..4f0618610 100644 --- a/app/pe/pe_tabletags.py +++ b/app/pe/pe_tabletags.py @@ -40,6 +40,7 @@ Created on Thu Sep 8 09:36:33 2016 import datetime import numpy as np +from app import ScoValueError from app.comp.moy_sem import comp_ranks_series from app.pe import pe_affichage from app.scodoc import sco_utils as scu @@ -48,63 +49,35 @@ import pandas as pd TAGS_RESERVES = ["but"] +class MoyenneTag(): -class TableTag(object): - def __init__(self): - """Classe centralisant différentes méthodes communes aux - SemestreTag, TrajectoireTag, AggregatInterclassTag + def __init__(self, tag: str, notes: pd.Series): + """Classe centralisant la synthèse des moyennes/classements d'une série + d'étudiants à un tag donné, en stockant un dictionnaire : + + `` + { + "notes": la Serie pandas des notes (float), + "classements": la Serie pandas des classements (float), + "min": la note minimum, + "max": la note maximum, + "moy": la moyenne, + "nb_inscrits": le nombre d'étudiants ayant une note, + } + `` + + Args: + tag: Un tag + note: Une série de notes (moyenne) sous forme d'un pd.Series() """ - pass + self.tag = tag + """Le tag associé à la moyenne""" + self.synthese = self.comp_moy_et_stat(notes) + """La synthèse des notes/classements/statistiques""" - # ----------------------------------------------------------------------------------------------------------- - def get_all_tags(self): - """Liste des tags de la table, triée par ordre alphabétique - - Returns: - Liste de tags triés par ordre alphabétique - """ - return sorted(self.moyennes_tags.keys()) - - def df_moyennes_et_classements(self): - """Renvoie un dataframe listant toutes les moyennes, - et les classements des étudiants pour tous les tags - """ - - etudiants = self.etudiants - df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"]) - - for tag in self.get_all_tags(): - df = df.join(self.moyennes_tags[tag]["notes"].rename(f"Moy {tag}")) - df = df.join(self.moyennes_tags[tag]["classements"].rename(f"Class {tag}")) - - return df - - def df_notes(self): - """Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags - - Returns: - Un dataframe etudids x tag (avec tag par ordre alphabétique) - """ - tags = self.get_all_tags() - if tags: - dict_series = {tag: self.moyennes_tags[tag]["notes"] for tag in tags} - df = pd.DataFrame(dict_series) - return df - else: - return None - - def str_tagtable(self): - """Renvoie une chaine de caractère listant toutes les moyennes, - les rangs des étudiants pour tous les tags.""" - - etudiants = self.etudiants - df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"]) - - for tag in self.get_all_tags(): - df = df.join(self.moyennes_tags[tag]["notes"].rename(f"moy {tag}")) - df = df.join(self.moyennes_tags[tag]["classements"].rename(f"class {tag}")) - - return df.to_csv(sep=";") + def __eq__(self, other): + """Egalité de deux MoyenneTag lorsque leur tag sont identiques""" + return self.tag == other.tag def comp_moy_et_stat(self, notes: pd.Series) -> dict: """Calcule et structure les données nécessaires au PE pour une série @@ -131,7 +104,7 @@ class TableTag(object): (_, class_gen_ue_non_nul) = comp_ranks_series(notes_non_nulles) # Les classements (toutes notes confondues, avec NaN si pas de notes) - class_gen_ue = pd.Series(np.nan, index=notes.index, dtype="Int64") + class_gen_ue = pd.Series(np.nan, index=notes.index) #, dtype="Int64") class_gen_ue[indices] = class_gen_ue_non_nul[indices] synthese = { @@ -140,42 +113,97 @@ class TableTag(object): "min": notes.min(), "max": notes.max(), "moy": notes.mean(), - "nb_inscrits": len(indices), + "nb_inscrits": sum(indices), } return synthese - @classmethod - def get_min_for_df(cls, bilan: dict) -> float: - """Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné, - revoie le min renseigné pour affichage dans un df""" - return round(bilan["min"], 2) + def get_note_for_df(self, etudid: int): + """Note d'un étudiant donné par son etudid""" + return round(self.synthese["notes"].loc[etudid], 2) - @classmethod - def get_max_for_df(cls, bilan: dict) -> float: - """Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné, - renvoie le max renseigné pour affichage dans un df""" - return round(bilan["max"], 2) + def get_min_for_df(self) -> float: + """Min renseigné pour affichage dans un df""" + return round(self.synthese["min"], 2) - @classmethod - def get_moy_for_df(cls, bilan: dict) -> float: - """Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné, - renvoie la moyenne renseignée pour affichage dans un df""" - return round(bilan["moy"], 2) + def get_max_for_df(self) -> float: + """Max renseigné pour affichage dans un df""" + return round(self.synthese["max"], 2) - @classmethod - def get_class_for_df(cls, bilan: dict, etudid: int) -> str: - """Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné, - renvoie le classement ramené au nombre d'inscrits, + def get_moy_for_df(self) -> float: + """Moyenne renseignée pour affichage dans un df""" + return round(self.synthese["moy"], 2) + + def get_class_for_df(self, etudid: int) -> str: + """Classement ramené au nombre d'inscrits, pour un étudiant donné par son etudid""" - classement = bilan['classements'].loc[etudid] + classement = self.synthese['classements'].loc[etudid] + nb_inscrit = self.synthese['nb_inscrits'] if not pd.isna(classement): - return f"{classement}/{bilan['nb_inscrits']}" + classement = int(classement) + return f"{classement}/{nb_inscrit}" else: return pe_affichage.SANS_NOTE - @classmethod - def get_note_for_df(cls, bilan: dict, etudid: int): - """Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné, - renvoie la note (moyenne) - pour un étudiant donné par son etudid""" - return round(bilan["notes"].loc[etudid], 2) + def is_significatif(self) -> bool: + """Indique si la moyenne est significative (c'est-à-dire à des notes)""" + return self.synthese["nb_inscrits"] > 0 + +class TableTag(object): + def __init__(self): + """Classe centralisant différentes méthodes communes aux + SemestreTag, TrajectoireTag, AggregatInterclassTag + """ + pass + + # ----------------------------------------------------------------------------------------------------------- + def get_all_tags(self): + """Liste des tags de la table, triée par ordre alphabétique, + extraite des clés du dictionnaire ``moyennes_tags`` connues (tags en doublon + possible). + + Returns: + Liste de tags triés par ordre alphabétique + """ + return sorted(list(self.moyennes_tags.keys())) + + def df_moyennes_et_classements(self) -> pd.DataFrame: + """Renvoie un dataframe listant toutes les moyennes, + et les classements des étudiants pour tous les tags. + + Est utilisé pour afficher le détail d'un tableau taggué + (semestres, trajectoires ou aggrégat) + + Returns: + Le dataframe des notes et des classements + """ + + etudiants = self.etudiants + df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"]) + + tags_tries = self.get_all_tags() + for tag in tags_tries: + moy_tag = self.moyennes_tags[tag] + df = df.join(moy_tag.synthese["notes"].rename(f"Moy {tag}")) + df = df.join(moy_tag.synthese["classements"].rename(f"Class {tag}")) + + return df + + def df_notes(self) -> pd.DataFrame | None: + """Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags + + Returns: + Un dataframe etudids x tag (avec tag par ordre alphabétique) + """ + tags_tries = self.get_all_tags() + if tags_tries: + dict_series = {} + for tag in tags_tries: + # Les moyennes associés au tag + moy_tag = self.moyennes_tags[tag] + dict_series[tag] = moy_tag.synthese["notes"] + df = pd.DataFrame(dict_series) + return df + + + + diff --git a/app/pe/pe_trajectoire.py b/app/pe/pe_trajectoire.py index fd98d031b..983b5e28a 100644 --- a/app/pe/pe_trajectoire.py +++ b/app/pe/pe_trajectoire.py @@ -6,43 +6,44 @@ from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date class Trajectoire: - """Modélise, pour un aggrégat visé (par ex: 'S2', '3S', '2A') - et un ensemble d'étudiants donnés, - la combinaison des formsemestres des étudiants amenant à un semestre - terminal visé. - - Si l'aggrégat est un semestre de type Si, elle stocke le (ou les) - formsemestres de numéro i qu'ont suivis l'étudiant pour atteindre le Si - (en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants) - - Pour des aggrégats de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie - les semestres que les étudiants ont suivis pour les amener jusqu'au semestre - terminal de la trajectoire (par ex: ici un S3). - Ces semestres peuvent être : - * des S1+S2+S1+S2+S3 si redoublement de la 1ère année - * des S1+S2+(année de césure)+S3 si césure, ... - """ - def __init__(self, nom_aggregat: str, semestre_final: FormSemestre): """Modélise un ensemble de formsemestres d'étudiants - amenant à un semestre terminal + amenant à un semestre terminal, au sens d'un aggrégat (par ex: 'S2', '3S', '2A'). + + Si l'aggrégat est un semestre de type Si, elle stocke le (ou les) + formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si + (en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants) + + Pour des aggrégats de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie + les semestres que les étudiants ont suivis pour les amener jusqu'au semestre + terminal de la trajectoire (par ex: ici un S3). + + Ces semestres peuvent être : + + * des S1+S2+S1+S2+S3 si redoublement de la 1ère année + * des S1+S2+(année de césure)+S3 si césure, ... Args: nom_aggregat: Un nom d'aggrégat (par ex: '5S') semestre_final: Le semestre final de l'aggrégat """ self.nom = nom_aggregat - self.semestre_final = semestre_final - self.trajectoire_id = (nom_aggregat, semestre_final.formsemestre_id) + """Nom de l'aggrégat""" + + self.formsemestre_final = semestre_final + """FormSemestre terminal de la trajectoire""" + + self.trajectoire_id = (nom_aggregat, semestre_final.formsemestre_id) + """Identifiant de la trajectoire""" - """Les semestres à aggréger""" self.semestres_aggreges = {} + """Semestres aggrégés""" def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]): - """Ajoute des semestres au semestre à aggréger + """Ajout de semestres aux semestres à aggréger Args: - semestres: Dictionnaire ``{fid: FormSemestre(fid)} à ajouter`` + semestres: Dictionnaire ``{fid: FormSemestre(fid)}`` à ajouter """ self.semestres_aggreges = self.semestres_aggreges | semestres @@ -55,27 +56,30 @@ class Trajectoire: semestre = self.semestres_aggreges[fid] noms.append(f"S{semestre.semestre_id}({fid})") noms = sorted(noms) - repr = f"{self.nom} ({self.semestre_final.formsemestre_id}) {self.semestre_final.date_fin.year}" + repr = f"{self.nom} ({self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}" if verbose and noms: repr += " - " + "+".join(noms) return repr class TrajectoiresJuryPE: - """Centralise toutes les trajectoires du jury PE""" - def __init__(self, annee_diplome: int): - """ + """Classe centralisant toutes les trajectoires des étudiants à prendre + en compte dans un jury PE + Args: annee_diplome: L'année de diplomation """ self.annee_diplome = annee_diplome """Toutes les trajectoires possibles""" + self.trajectoires: dict[tuple:Trajectoire] = {} - """Quelle trajectoires pour quel étudiant : - dictionnaire {etudid: {nom_aggregat: Trajectoire}}""" + """Ensemble des trajectoires recensées : {(aggregat, fid_terminal): Trajectoire}""" + self.suivi: dict[int:str] = {} + """Dictionnaire associant, pour chaque étudiant et pour chaque aggrégat, + sa trajectoire : {etudid: {nom_aggregat: Trajectoire}}""" def cree_trajectoires(self, etudiants: EtudiantsJuryPE): """Créé toutes les trajectoires, au regard du cursus des étudiants @@ -122,9 +126,6 @@ class TrajectoiresJuryPE: """Mémoire la trajectoire suivie par l'étudiant""" self.suivi[etudid][nom_aggregat] = trajectoire - """Vérifications""" - # dernier_semestre_aggregat = get_dernier_semestre_en_date(semestres_aggreges) - # assert dernier_semestre_aggregat == formsemestre_terminal def get_trajectoires_etudid(trajectoires, etudid): @@ -142,26 +143,3 @@ def get_trajectoires_etudid(trajectoires, etudid): return liste -def get_semestres_a_aggreger(self, aggregat: str, formsemestre_id_terminal: int): - """Pour un nom d'aggrégat donné (par ex: 'S3') et un semestre terminal cible - identifié par son formsemestre_id (par ex: 'S3 2022-2023'), - renvoie l'ensemble des semestres à prendre en compte dans - l'aggrégat sous la forme d'un dictionnaire {fid: FormSemestre(fid)}. - - Fusionne les cursus individuels des étudiants, dont le cursus correspond - à l'aggrégat visé. - - Args: - aggregat: Un aggrégat (par ex. 1A, 2A, 3S, 6S) - formsemestre_id_terminal: L'identifiant du formsemestre terminal de l'aggrégat, devant correspondre au - dernier semestre de l'aggrégat - """ - noms_semestres_aggreges = pe_comp.PARCOURS[aggregat]["aggregat"] - - formsemestres = {} - for etudid in self.cursus: - cursus_etudiant = self.cursus[etudid][aggregat] - if formsemestre_id_terminal in cursus_etudiant: - formsemestres_etudiant = cursus_etudiant[formsemestre_id_terminal] - formsemestres = formsemestres | formsemestres_etudiant - return formsemestres diff --git a/app/pe/pe_trajectoiretag.py b/app/pe/pe_trajectoiretag.py index 3b42d1586..d29e01e3f 100644 --- a/app/pe/pe_trajectoiretag.py +++ b/app/pe/pe_trajectoiretag.py @@ -43,7 +43,7 @@ import pandas as pd import numpy as np from app.pe.pe_trajectoire import Trajectoire -from app.pe.pe_tabletags import TableTag +from app.pe.pe_tabletags import TableTag, MoyenneTag class TrajectoireTag(TableTag): @@ -58,6 +58,9 @@ class TrajectoireTag(TableTag): Par ex: fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S' + Args: + trajectoire: Une trajectoire (aggrégat+semestre terminal) + semestres_taggues: Les données sur les semestres taggués """ TableTag.__init__(self) @@ -68,8 +71,9 @@ class TrajectoireTag(TableTag): # Le nom de la trajectoire tagguée (identique à la trajectoire) self.nom = self.get_repr() - self.formsemestre_terminal = trajectoire.semestre_final + self.formsemestre_terminal = trajectoire.formsemestre_final """Le formsemestre terminal""" + # Les résultats du formsemestre terminal nt = load_formsemestre_results(self.formsemestre_terminal) @@ -100,11 +104,11 @@ class TrajectoireTag(TableTag): self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted) """Calcul les moyennes par tag sous forme d'un dataframe""" - self.moyennes_tags = {} + self.moyennes_tags: dict[str, MoyenneTag] = {} """Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)""" for tag in self.tags_sorted: moy_gen_tag = self.notes[tag] - self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag) + self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag) def get_repr(self, verbose=False) -> str: """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle