diff --git a/app/pe/pe_interclasstag.py b/app/pe/pe_interclasstag.py index ffb2a03a..b4da44cf 100644 --- a/app/pe/pe_interclasstag.py +++ b/app/pe/pe_interclasstag.py @@ -59,8 +59,9 @@ class AggregatInterclasseTag(TableTag): for etudid in self.diplomes_ids: self.suivi[etudid] = trajectoires_jury_pe.suivi[etudid][nom_aggregat] - """Les tags""" + 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() @@ -69,15 +70,7 @@ class AggregatInterclasseTag(TableTag): self.moyennes_tags = {} for tag in self.tags_sorted: moy_gen_tag = self.notes[tag] - class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int - self.moyennes_tags[tag] = { - "notes": moy_gen_tag, - "classements": class_gen_tag, - "min": moy_gen_tag.min(), - "max": moy_gen_tag.max(), - "moy": moy_gen_tag.mean(), - "nb_inscrits": len(moy_gen_tag), - } + self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag) # Est significatif ? (aka a-t-il des tags et des notes) self.significatif = len(self.tags_sorted) > 0 @@ -125,4 +118,7 @@ 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 e16044fb..55752ff4 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -607,7 +607,7 @@ def get_dict_synthese_aggregat( note = TableTag.get_note_for_df(bilan, etudid) # Statistiques sur le groupe - if note != np.nan: + if not pd.isna(note) and note != np.nan: # Les moyennes de cette trajectoire donnees |= { (descr, "", "note"): note, @@ -637,7 +637,7 @@ def get_dict_synthese_aggregat( if tag in interclassement_taggue.moyennes_tags: bilan = interclassement_taggue.moyennes_tags[tag] - if note != np.nan: + if not pd.isna(note) and note != np.nan: nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}" donnees |= { diff --git a/app/pe/pe_semtag.py b/app/pe/pe_semtag.py index 0b8a6238..165a49b0 100644 --- a/app/pe/pe_semtag.py +++ b/app/pe/pe_semtag.py @@ -35,9 +35,12 @@ Created on Fri Sep 9 09:15:05 2016 @author: barasc """ +import numpy as np + import app.pe.pe_etudiant from app import db, log 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 from app.comp.res_sem import load_formsemestre_results from app.models import FormSemestre @@ -49,8 +52,8 @@ import app.pe.pe_affichage as pe_affichage from app.pe.pe_tabletags import TableTag, TAGS_RESERVES import pandas as pd -class SemestreTag(TableTag): +class SemestreTag(TableTag): def __init__(self, formsemestre_id: int): """ Un SemestreTag représente les résultats des étudiants à un semestre, en donnant @@ -89,47 +92,50 @@ class SemestreTag(TableTag): self.ues_inscr_parcours_df = self.nt.load_ues_inscr_parcours() self.dispense_ues = self.nt.dispense_ues - # Les tags (en supprimant les tags réservés) - self.tags = get_synthese_tags_semestre(self.nt.formsemestre) - for tag in TAGS_RESERVES: - if tag in self.tags: - del self.tags[tag] + # Les tags : + ## Saisis par l'utilisateur + self.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) + + # 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}") # Calcul des moyennes & les classements de chaque étudiant à chaque tag self.moyennes_tags = {} - for tag in self.tags: + for tag in self.tags_personnalises: # pe_affichage.pe_print(f" -> Traitement du tag {tag}") moy_gen_tag = self.compute_moyenne_tag(tag) - class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int - self.moyennes_tags[tag] = { - "notes": moy_gen_tag, - "classements": class_gen_tag, - "min": moy_gen_tag.min(), - "max": moy_gen_tag.max(), - "moy": moy_gen_tag.mean(), - "nb_inscrits": len(moy_gen_tag), - } + self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag) # Ajoute les moyennes générales de BUT pour le semestre considéré moy_gen_but = self.nt.etud_moy_gen - class_gen_but = self.nt.etud_moy_gen_ranks_int - self.moyennes_tags["but"] = { - "notes": moy_gen_but, - "classements": class_gen_but, - "min": moy_gen_but.min(), - "max": moy_gen_but.max(), - "moy": moy_gen_but.mean(), - "nb_inscrits": len(moy_gen_but), - } + moy_gen_but = pd.to_numeric(moy_gen_but, errors="coerce") + self.moyennes_tags["but"] = self.comp_moy_et_stat(moy_gen_but) + + # Ajoute les moyennes par compétence + for ue_id, competence in self.tags_competences.items(): + moy_ue = self.nt.etud_moy_ue[ue_id] + self.moyennes_tags[competence] = self.comp_moy_et_stat(moy_ue) # Synthétise l'ensemble des moyennes dans un dataframe - self.tags_sorted = sorted(self.moyennes_tags) # les tags par ordre alphabétique + 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) - pe_affichage.pe_print(f" => Traitement des tags {', '.join(self.tags_sorted)}") + pe_affichage.pe_print( + f" => Traitement des tags {', '.join(self.tags_sorted)}" + ) def get_repr(self): """Nom affiché pour le semestre taggué""" @@ -157,13 +163,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[tag]: + if modimpl.moduleimpl_id not in self.tags_personnalises[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[tag]: - ponderation = self.tags[tag][modimpl_id]["ponderation"] + for modimpl_id in self.tags_personnalises[tag]: + ponderation = self.tags_personnalises[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)""" @@ -193,95 +199,6 @@ class SemestreTag(TableTag): return moy_gen_tag - def get_noteEtCoeff_modimpl(self, modimpl_id, etudid, profondeur=2): - """Renvoie un couple donnant la note et le coeff normalisé d'un étudiant à un module d'id modimpl_id. - La note et le coeff sont extraits : - 1) soit des données du semestre en normalisant le coefficient par rapport à la somme des coefficients des modules du semestre, - 2) soit des données des UE précédemment capitalisées, en recherchant un module de même CODE que le modimpl_id proposé, - le coefficient normalisé l'étant alors par rapport au total des coefficients du semestre auquel appartient l'ue capitalisée - - TODO:: A rependre si nécessaire - """ - - def get_ue_capitalisees(etudid) -> list[dict]: - """Renvoie la liste des capitalisation effectivement capitalisées par un étudiant""" - if etudid in self.nt.validations.ue_capitalisees.index: - return self.nt.validations.ue_capitalisees.loc[[etudid]].to_dict("records") - return [] - - (note, coeff_norm) = (None, None) - - modimpl = get_moduleimpl(modimpl_id) # Le module considéré - if modimpl == None or profondeur < 0: - return (None, None) - - # Y-a-t-il eu capitalisation d'UE ? - ue_capitalisees = get_ue_capitalisees( - etudid - ) # les ue capitalisées des étudiants - ue_capitalisees_id = { - ue_cap["ue_id"] for ue_cap in ue_capitalisees - } # les id des ue capitalisées - - # Si le module ne fait pas partie des UE capitalisées - if modimpl.module.ue.id not in ue_capitalisees_id: - note = self.nt.get_etud_mod_moy(modimpl_id, etudid) # lecture de la note - coeff = modimpl.module.coefficient or 0.0 # le coeff (! non compatible BUT) - coeff_norm = ( - coeff / self.somme_coeffs if self.somme_coeffs != 0 else 0 - ) # le coeff normalisé - - # Si le module fait partie d'une UE capitalisée - elif len(ue_capitalisees) > 0: - moy_ue_actuelle = get_moy_ue_from_nt( - self.nt, etudid, modimpl_id - ) # la moyenne actuelle - # A quel semestre correspond l'ue capitalisée et quelles sont ses notes ? - fids_prec = [ - ue_cap["formsemestre_id"] - for ue_cap in ue_capitalisees - if ue_cap["ue_code"] == modimpl.module.ue.ue_code - ] # and ue['semestre_id'] == semestre_id] - if len(fids_prec) > 0: - # => le formsemestre_id du semestre dont vient la capitalisation - fid_prec = fids_prec[0] - # Lecture des notes de ce semestre - # le tableau de note du semestre considéré: - formsemestre_prec = FormSemestre.get_formsemestre(fid_prec) - nt_prec: NotesTableCompat = res_sem.load_formsemestre_results( - formsemestre_prec - ) - - # Y-a-t-il un module équivalent c'est à dire correspondant au même code (le module_id n'étant pas significatif en cas de changement de PPN) - - modimpl_prec = [ - modi - for modi in nt_prec.formsemestre.modimpls_sorted - if modi.module.code == modimpl.module.code - ] - if len(modimpl_prec) > 0: # si une correspondance est trouvée - modprec_id = modimpl_prec[0].id - moy_ue_capitalisee = get_moy_ue_from_nt(nt_prec, etudid, modprec_id) - if ( - moy_ue_capitalisee is None - ) or moy_ue_actuelle >= moy_ue_capitalisee: # on prend la meilleure ue - note = self.nt.get_etud_mod_moy( - modimpl_id, etudid - ) # lecture de la note - coeff = modimpl.module.coefficient # le coeff - # nota: self.somme_coeffs peut être None - coeff_norm = ( - coeff / self.somme_coeffs if self.somme_coeffs else 0 - ) # le coeff normalisé - else: - semtag_prec = SemestreTag(nt_prec, nt_prec.sem) - (note, coeff_norm) = semtag_prec.get_noteEtCoeff_modimpl( - modprec_id, etudid, profondeur=profondeur - 1 - ) # lecture de la note via le semtag associé au modimpl capitalisé - - # Sinon - pas de notes à prendre en compte - return (note, coeff_norm) - def get_moduleimpl(modimpl_id) -> dict: """Renvoie l'objet modimpl dont l'id est modimpl_id""" @@ -308,21 +225,13 @@ def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float: return ue_status["moy"] -# ----------------------------------------------------------------------------- -def get_synthese_tags_semestre(formsemestre: FormSemestre): +def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre): """Etant données les implémentations des modules du semestre (modimpls), - synthétise les tags les concernant (tags saisis dans le programme pédagogique) + synthétise les tags renseignés dans le programme pédagogique & + associés aux modules du semestre, en les associant aux modimpls qui les concernent (modimpl_id) et aucoeff et pondération fournie avec le tag (par défaut 1 si non indiquée)). - { tagname1: { modimpl_id1: { 'module_id': ..., - 'coeff': ..., - 'coeff_norm': ..., - 'ponderation': ..., - 'module_code': ..., - 'ue_xxx': ...}, - } - } Args: formsemestre: Le formsemestre à la base de la recherche des tags @@ -364,3 +273,26 @@ def get_synthese_tags_semestre(formsemestre: FormSemestre): } return synthese_tags + + +def get_noms_competences_from_ues(formsemestre: FormSemestre) -> dict[int, str]: + """Partant d'un formsemestre, extrait le nom des compétences associés + à (ou aux) parcours des étudiants du formsemestre. + + Args: + formsemestre: Un FormSemestre + + Returns: + Dictionnaire {ue_id: nom_competence} lisant tous les noms des compétences + en les raccrochant à leur ue + """ + # Les résultats du semestre + nt = load_formsemestre_results(formsemestre) + + noms_competences = {} + for ue in nt.ues: + if ue.type != UE_SPORT: + ordre = ue.niveau_competence.ordre + nom = ue.niveau_competence.competence.titre + noms_competences[ue.ue_id] = f"comp. {nom}" + return noms_competences diff --git a/app/pe/pe_tabletags.py b/app/pe/pe_tabletags.py index 9df18c01..610e5693 100644 --- a/app/pe/pe_tabletags.py +++ b/app/pe/pe_tabletags.py @@ -40,6 +40,8 @@ Created on Thu Sep 8 09:36:33 2016 import datetime import numpy as np +from app.comp.moy_sem import comp_ranks_series +from app.pe import pe_affichage from app.scodoc import sco_utils as scu import pandas as pd @@ -104,6 +106,44 @@ class TableTag(object): return df.to_csv(sep=";") + def comp_moy_et_stat(self, notes: pd.Series) -> dict: + """Calcule et structure les données nécessaires au PE pour une série + de notes (souvent une moyenne par tag) dans un dictionnaire spécifique. + + Partant des notes, sont calculés les classements (en ne tenant compte + que des notes non nulles). + + Args: + notes: Une série de notes (avec des éventuels NaN) + + Returns: + Un dictionnaire stockant les notes, les classements, le min, + le max, la moyenne, le nb de notes (donc d'inscrits) + """ + # Supprime d'éventuels chaines de caractères dans les notes + notes = pd.to_numeric(notes, errors="coerce") + + # Les indices des ... et les notes non nulles/pertinentes + indices = notes.notnull() + notes_non_nulles = notes[indices] + + # Les classements sur les notes non nulles + (_, 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[indices] = class_gen_ue_non_nul[indices] + + synthese = { + "notes": notes, + "classements": class_gen_ue, + "min": notes.min(), + "max": notes.max(), + "moy": notes.mean(), + "nb_inscrits": len(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é, @@ -127,12 +167,15 @@ class TableTag(object): """Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné, renvoie le classement ramené au nombre d'inscrits, pour un étudiant donné par son etudid""" - return f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}" - + classement = bilan['classements'].loc[etudid] + if not pd.isna(classement): + return f"{classement}/{bilan['nb_inscrits']}" + 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) \ No newline at end of file + return round(bilan["notes"].loc[etudid], 2) diff --git a/app/pe/pe_trajectoiretag.py b/app/pe/pe_trajectoiretag.py index 7fff55f5..3b42d158 100644 --- a/app/pe/pe_trajectoiretag.py +++ b/app/pe/pe_trajectoiretag.py @@ -47,11 +47,8 @@ from app.pe.pe_tabletags import TableTag class TrajectoireTag(TableTag): - def __init__( - self, - trajectoire: Trajectoire, - semestres_taggues: dict[int, SemestreTag] + self, trajectoire: Trajectoire, semestres_taggues: dict[int, SemestreTag] ): """Calcule les moyennes par tag d'une combinaison de semestres (trajectoires), identifiée par un nom d'aggrégat (par ex: '3S') et @@ -71,11 +68,13 @@ class TrajectoireTag(TableTag): # Le nom de la trajectoire tagguée (identique à la trajectoire) self.nom = self.get_repr() - """Le formsemestre terminal et les semestres aggrégés""" self.formsemestre_terminal = trajectoire.semestre_final + """Le formsemestre terminal""" + # Les résultats du formsemestre terminal nt = load_formsemestre_results(self.formsemestre_terminal) self.semestres_aggreges = trajectoire.semestres_aggreges + """Les semestres aggrégés""" self.semestres_tags_aggreges = {} """Les semestres tags associés aux semestres aggrégés""" @@ -87,32 +86,25 @@ class TrajectoireTag(TableTag): """Les étudiants (état civil + cursus connu)""" self.etuds = nt.etuds + # assert self.etuds == trajectoire.suivi # manque-t-il des étudiants ? self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds} - """Les tags extraits de tous les semestres""" self.tags_sorted = self.do_taglist() + """Tags extraits de tous les semestres""" - """Construit le cube de notes""" self.notes_cube = self.compute_notes_cube() + """Cube de notes""" - """Calcul les moyennes par tag sous forme d'un dataframe""" etudids = list(self.etudiants.keys()) self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted) + """Calcul les moyennes par tag sous forme d'un dataframe""" - """Synthétise les moyennes/classements par tag""" self.moyennes_tags = {} + """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] - class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int - self.moyennes_tags[tag] = { - "notes": moy_gen_tag, - "classements": class_gen_tag, - "min": moy_gen_tag.min(), - "max": moy_gen_tag.max(), - "moy": moy_gen_tag.mean(), - "nb_inscrits": len(moy_gen_tag), - } + self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag) def get_repr(self, verbose=False) -> str: """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle @@ -123,11 +115,11 @@ class TrajectoireTag(TableTag): """Construit le cube de notes (etudid x tags x semestre_aggregé) nécessaire au calcul des moyennes de l'aggrégat """ - nb_tags = len(self.tags_sorted) - nb_etudiants = len(self.etuds) - nb_semestres = len(self.semestres_tags_aggreges) + # nb_tags = len(self.tags_sorted) + # nb_etudiants = len(self.etuds) + # nb_semestres = len(self.semestres_tags_aggreges) - """Index du cube (etudids -> dim 0, tags -> dim 1)""" + # Index du cube (etudids -> dim 0, tags -> dim 1) etudids = [etud.etudid for etud in self.etuds] tags = self.tags_sorted semestres_id = list(self.semestres_tags_aggreges.keys()) @@ -135,17 +127,17 @@ class TrajectoireTag(TableTag): dfs = {} for frmsem_id in semestres_id: - """Partant d'un dataframe vierge""" + # Partant d'un dataframe vierge df = pd.DataFrame(np.nan, index=etudids, columns=tags) - """Charge les notes du semestre tag""" + # Charge les notes du semestre tag notes = self.semestres_tags_aggreges[frmsem_id].notes - """Les étudiants & les tags commun au dataframe final et aux notes du semestre)""" + # Les étudiants & les tags commun au dataframe final et aux notes du semestre) etudids_communs = df.index.intersection(notes.index) tags_communs = df.columns.intersection(notes.columns) - """Injecte les notes par tag""" + # Injecte les notes par tag df.loc[etudids_communs, tags_communs] = notes.loc[ etudids_communs, tags_communs ] @@ -154,7 +146,7 @@ class TrajectoireTag(TableTag): for col in df.columns: df[col] = pd.to_numeric(df[col], errors="coerce") - """Stocke le df""" + # Stocke le df dfs[frmsem_id] = df """Réunit les notes sous forme d'un cube etdids x tags x semestres""" @@ -175,8 +167,6 @@ class TrajectoireTag(TableTag): return sorted(set(tags)) - - def compute_tag_moy(set_cube: np.array, etudids: list, tags: list): """Calcul de la moyenne par tag sur plusieurs semestres. La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles @@ -214,4 +204,6 @@ def compute_tag_moy(set_cube: np.array, etudids: list, tags: list): columns=tags, # les tags ) + etud_moy_tag_df.fillna(np.nan) + return etud_moy_tag_df diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 7b3a4211..8279ff61 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -78,7 +78,7 @@ def pe_view_sem_recap(formsemestre_id: int): sco=ScoData(formsemestre=formsemestre), ) - jury = pe_jury.JuryPE(annee_diplome, formsemestre.formation.formation_id) + jury = pe_jury.JuryPE(annee_diplome) if not jury.diplomes_ids: flash("aucun étudiant à considérer !") return redirect(