diff --git a/app/api/departements.py b/app/api/departements.py index 7d056e46..b82966e3 100644 --- a/app/api/departements.py +++ b/app/api/departements.py @@ -187,7 +187,7 @@ def dept_etudiants(acronym: str): ] """ dept = Departement.query.filter_by(acronym=acronym).first_or_404() - return [etud.to_dict_short() for etud in dept.etudiants] + return [etud.to_dict_short() for etud in dept.etats_civils] @bp.route("/departement/id//etudiants") @@ -200,7 +200,7 @@ def dept_etudiants_by_id(dept_id: int): Retourne la liste des étudiants d'un département d'id donné. """ dept = Departement.query.get_or_404(dept_id) - return [etud.to_dict_short() for etud in dept.etudiants] + return [etud.to_dict_short() for etud in dept.etats_civils] @bp.route("/departement//formsemestres_ids") diff --git a/app/pe/pe_interclasstag.py b/app/pe/pe_interclasstag.py index 895595ed..000feacf 100644 --- a/app/pe/pe_interclasstag.py +++ b/app/pe/pe_interclasstag.py @@ -36,7 +36,8 @@ Created on Thu Sep 8 09:36:33 2016 import pandas as pd import numpy as np -from app.pe.pe_tabletags import TableTag, MoyenneTag +from app.pe.pe_tabletags import TableTag +from app.pe.pe_moytag import MoyennesTag from app.pe.pe_etudiant import EtudiantsJuryPE from app.pe.pe_rcs import RCS, RCSsJuryPE from app.pe.pe_rcstag import RCSTag @@ -107,10 +108,10 @@ class RCSInterclasseTag(TableTag): """Matrice des notes de l'aggrégat""" # Synthétise les moyennes/classements par tag - self.moyennes_tags: dict[str, MoyenneTag] = {} + self.moyennes_tags: dict[str, MoyennesTag] = {} for tag in self.tags_sorted: moy_gen_tag = self.notes[tag] - self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag) + self.moyennes_tags[tag] = MoyennesTag(tag, moy_gen_tag) # Est significatif ? (aka a-t-il des tags et des notes) self.significatif = len(self.tags_sorted) > 0 diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index d486692f..bfe72bd2 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -53,9 +53,9 @@ import pandas as pd from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE import app.pe.pe_affichage as pe_affichage from app.pe.pe_etudiant import * # TODO A éviter -> pe_etudiant. -from app.pe.pe_rcs import * # TODO A éviter +import app.pe.pe_rcs as pe_rcs from app.pe.pe_rcstag import RCSTag -from app.pe.pe_semtag import SemestreTag +from app.pe.pe_ressemtag import ResSemTag from app.pe.pe_interclasstag import RCSInterclasseTag @@ -96,11 +96,11 @@ class JuryPE(object): pe_affichage.pe_print("*** Aucun étudiant diplômé") else: self._gen_xls_diplomes(zipfile) - self._gen_xls_semestre_taggues(zipfile) - self._gen_xls_rcss_tags(zipfile) - self._gen_xls_interclassements_rcss(zipfile) - self._gen_xls_synthese_jury_par_tag(zipfile) - self._gen_xls_synthese_par_etudiant(zipfile) + self._gen_xls_resultats_semestres_taggues(zipfile) + # self._gen_xls_rcss_tags(zipfile) + # self._gen_xls_interclassements_rcss(zipfile) + # self._gen_xls_synthese_jury_par_tag(zipfile) + # self._gen_xls_synthese_par_etudiant(zipfile) # et le log self._add_log_to_zip(zipfile) @@ -131,19 +131,19 @@ class JuryPE(object): path="details", ) - def _gen_xls_semestre_taggues(self, zipfile: ZipFile): - "Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE" - pe_affichage.pe_print("*** Génère les semestres taggués") - self.sems_tags = compute_semestres_tag(self.etudiants) + def _gen_xls_resultats_semestres_taggues(self, zipfile: ZipFile): + """Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE""" + pe_affichage.pe_print("*** Génère les résultats des semestres taggués") + self.res_sems_tags = compute_resultats_semestres_tag(self.etudiants) # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: - for formsemestretag in self.sems_tags.values(): - onglet = formsemestretag.nom - df = formsemestretag.df_moyennes_et_classements() + for res_sem_tag in self.res_sems_tags.values(): + onglet = res_sem_tag.get_repr() + df = res_sem_tag.df_moyennes_et_classements() # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) output.seek(0) @@ -156,20 +156,46 @@ class JuryPE(object): ) def _gen_xls_rcss_tags(self, zipfile: ZipFile): - """Génère les RCS (combinaisons de semestres suivis - par un étudiant) + """Génère : + + * les RCS (combinaisons de semestres suivis par les étudiants au sens + d'un aggrégat (par ex: '3S')) + * les RCS tagguées des RCS, en calculant les moyennes et les classements par tag + pour chacune. + + Stocke le résultat dans self.rccs_tag, un dictionnaire de + la forme ``{nom_aggregat: {fid_terminal: SetTag(fid_terminal)} }`` + + Pour rappel : Chaque RCS est identifié par un nom d'aggrégat et par un formsemestre terminal. + + Par exemple : + + * combinaisons '3S' : S1+S2+S3 en prenant en compte tous les S3 qu'ont fréquenté les + étudiants du jury PE. Ces S3 marquent les formsemestre terminal de chaque combinaison. + + * combinaisons 'S2' : 1 seul S2 pour des étudiants n'ayant pas redoublé, 2 pour des redoublants (dont les + notes seront moyennées sur leur 2 semestres S2). Ces combinaisons ont pour formsemestre le dernier S2 en + date (le S2 redoublé par les redoublants est forcément antérieur) + + Args: + etudiants: Les données des étudiants + semestres_tag: Les semestres tag (pour lesquels des moyennes par tag ont été calculés) + + """ pe_affichage.pe_print( "*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants" ) - self.rcss = RCSsJuryPE(self.diplome) - self.rcss.cree_rcss(self.etudiants) + self.rcss_jury = pe_rcs.RCSsJuryPE(self.diplome) + self.rcss_jury.cree_rcss(self.etudiants) # Génère les moyennes par tags des trajectoires pe_affichage.pe_print("*** Calcule les moyennes par tag des RCS possibles") - self.rcss_tags = compute_trajectoires_tag( - self.rcss, self.etudiants, self.sems_tags - ) + + self.rcss_tags = {} + for rcs_id, rcs in self.rcss_jury.rcss.items(): + # nom = rcs.get_repr() + self.rcss_tags[rcs_id] = RCSTag(rcs, self.res_sems_tags) # Intègre le bilan des trajectoires tagguées au zip final output = io.BytesIO() @@ -177,7 +203,7 @@ class JuryPE(object): output, engine="openpyxl" ) as writer: for rcs_tag in self.rcss_tags.values(): - onglet = rcs_tag.get_repr() + onglet = rcs_tag.get_repr(mode="short") df = rcs_tag.df_moyennes_et_classements() # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) @@ -195,7 +221,7 @@ class JuryPE(object): # Génère les interclassements (par promo et) par (nom d') aggrégat pe_affichage.pe_print("*** Génère les interclassements par aggrégat") self.interclassements_taggues = compute_interclassements( - self.etudiants, self.rcss, self.rcss_tags + self.etudiants, self.rcss_jury, self.rcss_tags ) # Intègre le bilan des aggrégats (interclassé par promo) au zip final @@ -339,14 +365,14 @@ class JuryPE(object): df_synthese = pd.DataFrame.from_dict(donnees_etudiants, orient="index") # Ajout des aggrégats - for aggregat in TOUS_LES_RCS: - descr = TYPES_RCS[aggregat]["descr"] + for aggregat in pe_rcs.TOUS_LES_RCS: + descr = pe_rcs.TYPES_RCS[aggregat]["descr"] # Les trajectoires (tagguées) suivies par les étudiants pour l'aggrégat et le tag # considéré trajectoires_tagguees = [] for etudid in etudids: - trajectoire = self.rcss.suivi[etudid][aggregat] + trajectoire = self.rcss_jury.suivi[etudid][aggregat] if trajectoire: tid = trajectoire.rcs_id trajectoire_tagguee = self.rcss_tags[tid] @@ -485,14 +511,14 @@ class JuryPE(object): # Une ligne pour le tag donnees[tag] = {("", "", "tag"): tag} - for aggregat in TOUS_LES_RCS: + for aggregat in pe_rcs.TOUS_LES_RCS: # Le dictionnaire par défaut des moyennes donnees[tag] |= get_defaut_dict_synthese_aggregat( aggregat, self.diplome ) # La trajectoire de l'étudiant sur l'aggrégat - trajectoire = self.rcss.suivi[etudid][aggregat] + trajectoire = self.rcss_jury.suivi[etudid][aggregat] if trajectoire: trajectoire_tagguee = self.rcss_tags[trajectoire.rcs_id] if tag in trajectoire_tagguee.moyennes_tags: @@ -541,7 +567,7 @@ def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict: return semestres -def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict: +def compute_resultats_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 des étudiants (cf. attribut etudiants.cursus). @@ -563,68 +589,28 @@ def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict: semestres_tags = {} for frmsem_id, formsemestre in formsemestres.items(): # Crée le semestre_tag et exécute les calculs de moyennes - formsemestretag = SemestreTag(frmsem_id) - pe_affichage.pe_print( - f" --> Semestre taggué {formsemestretag.nom} sur la base de {formsemestre}" - ) + formsemestretag = ResSemTag(frmsem_id) + # Stocke le semestre taggué semestres_tags[frmsem_id] = formsemestretag return semestres_tags -def compute_trajectoires_tag( - trajectoires: RCSsJuryPE, - etudiants: EtudiantsJuryPE, - semestres_taggues: dict[int, SemestreTag], -): - """Créée les trajectoires tagguées (combinaison aggrégeant plusieurs semestres au sens - d'un aggrégat (par ex: '3S')), - en calculant les moyennes et les classements par tag pour chacune. - Pour rappel : Chaque trajectoire est identifiée un nom d'aggrégat et par un formsemestre terminal. - - Par exemple : - - * combinaisons '3S' : S1+S2+S3 en prenant en compte tous les S3 qu'ont fréquenté les - étudiants du jury PE. Ces S3 marquent les formsemestre terminal de chaque combinaison. - - * combinaisons 'S2' : 1 seul S2 pour des étudiants n'ayant pas redoublé, 2 pour des redoublants (dont les - notes seront moyennées sur leur 2 semestres S2). Ces combinaisons ont pour formsemestre le dernier S2 en - date (le S2 redoublé par les redoublants est forcément antérieur) - - - Args: - etudiants: Les données des étudiants - semestres_tag: Les semestres tag (pour lesquels des moyennes par tag ont été calculés) - - Return: - Un dictionnaire de la forme ``{nom_aggregat: {fid_terminal: SetTag(fid_terminal)} }`` - """ - trajectoires_tagguees = {} - - for trajectoire_id, trajectoire in trajectoires.rcss.items(): - nom = trajectoire.get_repr() - pe_affichage.pe_print(f" --> Aggrégat {nom}") - # Trajectoire_tagguee associée - trajectoire_tagguee = RCSTag(trajectoire, semestres_taggues) - # Mémorise le résultat - trajectoires_tagguees[trajectoire_id] = trajectoire_tagguee - - return trajectoires_tagguees def compute_interclassements( etudiants: EtudiantsJuryPE, - trajectoires_jury_pe: RCSsJuryPE, - trajectoires_tagguees: dict[tuple, RCS], + trajectoires_jury_pe: pe_rcs.RCSsJuryPE, + trajectoires_tagguees: dict[tuple, pe_rcs.RCS], ): """Interclasse les étudiants, (nom d') aggrégat par aggrégat, pour fournir un classement sur la promo. Le classement est établi au regard du nombre d'étudiants ayant participé au même aggrégat. """ aggregats_interclasses_taggues = {} - for nom_aggregat in TOUS_LES_RCS: + for nom_aggregat in pe_rcs.TOUS_LES_RCS: pe_affichage.pe_print(f" --> Interclassement {nom_aggregat}") interclass = RCSInterclasseTag( nom_aggregat, etudiants, trajectoires_jury_pe, trajectoires_tagguees @@ -642,7 +628,7 @@ def get_defaut_dict_synthese_aggregat(nom_rcs: str, diplome: int) -> dict: diplôme : l'année du diplôme """ # L'affichage de l'aggrégat dans le tableur excel - descr = get_descr_rcs(nom_rcs) + descr = pe_rcs.get_descr_rcs(nom_rcs) nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}" donnees = { @@ -686,7 +672,7 @@ def get_dict_synthese_aggregat( à l'aggrégat donné et pour un tag donné""" donnees = {} # L'affichage de l'aggrégat dans le tableur excel - descr = get_descr_rcs(aggregat) + descr = pe_rcs.get_descr_rcs(aggregat) # La note de l'étudiant (chargement à venir) note = np.nan diff --git a/app/pe/pe_moytag.py b/app/pe/pe_moytag.py new file mode 100644 index 00000000..159661d1 --- /dev/null +++ b/app/pe/pe_moytag.py @@ -0,0 +1,203 @@ +import numpy as np +import pandas as pd + +from app.comp.moy_sem import comp_ranks_series +from app.models import UniteEns +from app.pe import pe_affichage + + +class Moyenne: + CRITERES = [ + "note", + "classement", + "rang", + "min", + "max", + "moy", + "nb_etuds", + "nb_inscrits", + ] + + def __init__(self, notes: pd.Series): + """Classe centralisant la synthèse des moyennes/classements d'une série + de notes : + + * des "notes": la Serie pandas des notes (float), + * des "classements": la Serie pandas des classements (float), + * des "min": la note minimum, + * des "max": la note maximum, + * des "moy": la moyenne, + * des "nb_inscrits": le nombre d'étudiants ayant une note, + """ + self.notes = notes + """Les notes""" + self.etudids = list(notes.index) # calcul à venir + """Les id des étudiants""" + self.inscrits_ids = notes[notes.notnull()].index.to_list() + """Les id des étudiants dont la note est non nulle""" + self.df: pd.DataFrame = self.comp_moy_et_stat(self.notes) + """Le dataframe retraçant les moyennes/classements/statistiques""" + self.synthese = self.to_dict() + """La synthèse (dictionnaire) des notes/classements/statistiques""" + + 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 (pouvant être une moyenne d'un tag à une UE ou une moyenne générale + d'un 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) + """ + df = pd.DataFrame( + np.nan, + index=self.etudids, + columns=Moyenne.CRITERES, + ) + + # Supprime d'éventuelles chaines de caractères dans les notes + notes = pd.to_numeric(notes, errors="coerce") + df["note"] = notes + + # Les nb d'étudiants & nb d'inscrits + df["nb_etuds"] = len(self.etudids) + # Les étudiants dont la note n'est pas nulle + inscrits_ids = notes[notes.notnull()].index.to_list() + df.loc[inscrits_ids, "nb_inscrits"] = len(inscrits_ids) + + # Le classement des inscrits + notes_non_nulles = notes[inscrits_ids] + (class_str, class_int) = comp_ranks_series(notes_non_nulles) + df.loc[inscrits_ids, "classement"] = class_int + + # Le rang (classement/nb_inscrit) + df["rang"] = df["rang"].astype(str) + df.loc[inscrits_ids, "rang"] = ( + df.loc[inscrits_ids, "classement"].astype(int).astype(str) + + "/" + + df.loc[inscrits_ids, "nb_inscrits"].astype(int).astype(str) + ) + + # Les stat (des inscrits) + df.loc[inscrits_ids, "min"] = notes.min() + df.loc[inscrits_ids, "max"] = notes.max() + df.loc[inscrits_ids, "moy"] = notes.mean() + + return df + + def to_dict(self) -> dict: + """Renvoie un dictionnaire de synthèse des moyennes/classements/statistiques générale (but)""" + synthese = { + "notes": self.df["note"], + "classements": self.df["classement"], + "min": self.df["min"].mean(), + "max": self.df["max"].mean(), + "moy": self.df["moy"].mean(), + "nb_inscrits": self.df["nb_inscrits"].mean(), + } + return synthese + + def get_notes(self): + """Série des notes, arrondies à 2 chiffres après la virgule""" + return self.df_gen["note"].round(2) + + def get_rangs_inscrits(self) -> pd.Series: + """Série des rangs classement/nbre_inscrit""" + return self.df_gen["rang"] + + def get_min(self) -> pd.Series: + """Série des min""" + return self.df_gen["min"].round(2) + + def get_max(self) -> pd.Series: + """Série des max""" + return self.df_gen["max"].round(2) + + def get_moy(self) -> pd.Series: + """Série des moy""" + return self.df_gen["moy"].round(2) + + def get_note_for_df(self, etudid: int): + """Note d'un étudiant donné par son etudid""" + return round(self.df_gen["note"].loc[etudid], 2) + + def get_min_for_df(self) -> float: + """Min renseigné pour affichage dans un df""" + return round(self.synthese["min"], 2) + + def get_max_for_df(self) -> float: + """Max renseigné pour affichage dans un df""" + return round(self.synthese["max"], 2) + + 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 = self.df_gen["rang"].loc[etudid] + if not pd.isna(classement): + return classement + else: + return pe_affichage.SANS_NOTE + + def is_significatif(self) -> bool: + """Indique si la moyenne est significative (c'est-à-dire à des notes)""" + return self.synthese["nb_inscrits"] > 0 + + +class MoyennesTag: + def __init__( + self, + tag: str, + ues: list[UniteEns], + notes_ues: pd.DataFrame, + # notes_gen: pd.Series, + ): + """Classe centralisant la synthèse des moyennes/classements d'une série + d'étudiants à un tag donné, en différenciant les notes + obtenues aux UE et au général (toutes UEs confondues) + + Args: + tag: Un tag + ues: La liste des UEs ayant servie au calcul de la moyenne + notes_ues: Les moyennes (etudid x acronymes_ues) aux différentes UEs et pour le tag + # notes_gen: Une série de notes (moyenne) sous forme d'un pd.Series() (toutes UEs confondues) + """ + self.tag = tag + """Le tag associé aux moyennes""" + + # Les UE + self.ues: dict[int, UniteEns] = {ue.id: ue for ue in ues} + """Les UEs sur lesquelles sont calculées les moyennes""" + colonnes = list(notes_ues.columns) + acronymes = [self.ues[ue_id].acronyme for ue_id in colonnes] + assert len(set(acronymes)) == len(colonnes), \ + "Deux UEs ne peuvent pas avoir le même acronyme" + + # Les moyennes par UE + self.notes_ues = notes_ues + """Les notes aux UEs (dataframe)""" + self.notes_ues.columns = acronymes # remplace les ue.id par leur acronyme + self.moys_ues: dict[int, pd.DataFrame] = {} + """Les dataframes retraçant les moyennes/classements/statistiques des étudiants aux UEs""" + for ue in self.ues.values(): # if ue.type != UE_SPORT: + notes = notes_ues[ue.acronyme] + self.moys_ues[ue.acronyme] = Moyenne(notes) + + # Les moyennes générales + self.notes_gen = notes_gen + """Les notes générales (moyenne toutes UEs confonudes)""" + self.moy_gen = Moyenne(notes_gen) + """Le dataframe retraçant les moyennes/classements/statistiques général""" + + def __eq__(self, other): + """Egalité de deux MoyenneTag lorsque leur tag sont identiques""" + return self.tag == other.tag diff --git a/app/pe/pe_rcs.py b/app/pe/pe_rcs.py index f9ec6688..ecd5d764 100644 --- a/app/pe/pe_rcs.py +++ b/app/pe/pe_rcs.py @@ -116,6 +116,10 @@ class RCS: self.semestres_aggreges = {} """Semestres regroupés dans le RCS""" + def get_formsemestre_id_final(self): + """Renvoie l'identifiant du formsemestre final du RCS""" + return self.formsemestre_final.formsemestre_id + def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]): """Ajout de semestres aux semestres à regrouper diff --git a/app/pe/pe_rcstag.py b/app/pe/pe_rcstag.py index c3d3a05f..f44ba13a 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/pe_rcstag.py @@ -37,17 +37,19 @@ Created on Fri Sep 9 09:15:05 2016 """ from app.comp.res_sem import load_formsemestre_results -from app.pe.pe_semtag import SemestreTag +from app.pe import pe_affichage +from app.pe.pe_ressemtag import ResSemTag import pandas as pd import numpy as np from app.pe.pe_rcs import RCS -from app.pe.pe_tabletags import TableTag, MoyenneTag +from app.pe.pe_tabletags import TableTag +from app.pe.pe_moytag import MoyennesTag class RCSTag(TableTag): def __init__( - self, rcs: RCS, semestres_taggues: dict[int, SemestreTag] + self, rcs: RCS, semestres_taggues: dict[int, ResSemTag] ): """Calcule les moyennes par tag d'une combinaison de semestres (RCS), pour extraire les classements par tag pour un @@ -104,11 +106,11 @@ class RCSTag(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: dict[str, MoyenneTag] = {} + self.moyennes_tags: dict[str, MoyennesTag] = {} """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] = MoyenneTag(tag, moy_gen_tag) + self.moyennes_tags[tag] = MoyennesTag(tag, moy_gen_tag) def __eq__(self, other): """Egalité de 2 RCS taggués sur la base de leur identifiant""" @@ -172,6 +174,7 @@ class RCSTag(TableTag): tags = [] for frmsem_id in self.semestres_tags_aggreges: tags.extend(self.semestres_tags_aggreges[frmsem_id].tags_sorted) + pe_affichage.pe_print(f"* Tags : {', '.join(tags)}") return sorted(set(tags)) diff --git a/app/pe/pe_ressemtag.py b/app/pe/pe_ressemtag.py new file mode 100644 index 00000000..8729580f --- /dev/null +++ b/app/pe/pe_ressemtag.py @@ -0,0 +1,373 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +############################################################################## +# Module "Avis de poursuite d'étude" +# conçu et développé par Cléo Baras (IUT de Grenoble) +############################################################################## + +""" +Created on Fri Sep 9 09:15:05 2016 + +@author: barasc +""" +import pandas as pd + +import app.pe.pe_etudiant +from app import db, ScoValueError +from app import comp +from app.comp.res_sem import load_formsemestre_results +from app.models import FormSemestre +from app.models.moduleimpls import ModuleImpl +import app.pe.pe_affichage as pe_affichage +import app.pe.pe_etudiant as pe_etudiant +from app.pe.pe_tabletags import TableTag +from app.pe.pe_moytag import MoyennesTag +from app.scodoc import sco_tag_module +from app.scodoc.codes_cursus import UE_SPORT + + +class ResSemTag(TableTag): + """ + Un ResSemTag représente les résultats des étudiants à un semestre, en donnant + accès aux moyennes par tag. + Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT. + """ + + def __init__(self, formsemestre_id: int): + """ + Args: + formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base + """ + TableTag.__init__(self) + + # Le semestre + self.formsemestre_id = formsemestre_id + self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + + # Le nom du res_semestre taggué + self.nom = self.get_repr(mode="long") + + pe_affichage.pe_print( + f"--> Résultats de Semestre taggués {self.nom}" + ) + + # Les résultats du semestre + self.nt = load_formsemestre_results(self.formsemestre) + + # Les étudiants + self.etuds = self.nt.etuds + self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds} + self.etudids = list(self.etudiants.keys()) + + # Les notes, les modules implémentés triés, les étudiants, les coeffs, + # récupérés notamment de py:mod:`res_but` + self.sem_cube = self.nt.sem_cube + self.modimpls_sorted = self.nt.formsemestre.modimpls_sorted + self.modimpl_coefs_df = self.nt.modimpl_coefs_df + + # Les inscriptions aux modules + self.modimpl_inscr_df = self.nt.modimpl_inscr_df + + # Les UEs (et les dispenses d'UE) + self.ues = self.nt.ues + ues_hors_sport = [ue for ue in self.ues if ue.type != UE_SPORT] + self.ues_inscr_parcours_df = self.nt.load_ues_inscr_parcours() + self.dispense_ues = self.nt.dispense_ues + + # Les tags personnalisés et auto: + tags_dict = self._get_tags_dict() + self._check_tags(tags_dict) + # self.tags = [tag for cat in dict_tags for tag in dict_tags[cat]] + + # Calcul des moyennes & les classements de chaque étudiant à chaque tag + self.moyennes_tags = {} + + for tag in tags_dict["personnalises"]: + # pe_affichage.pe_print(f" -> Traitement du tag {tag}") + infos_tag = tags_dict["personnalises"][tag] + moy_ues_tag = self.compute_moy_ues_tag(infos_tag) + # moy_gen_tag = self.compute_moy_gen_tag(moy_ues_tag) + + self.moyennes_tags[tag] = MoyennesTag( + tag, ues_hors_sport, moy_ues_tag # moy_gen_tag + ) + + # Ajoute les d'UE moyennes générales de BUT pour le semestre considéré + # moy_gen_but = self.nt.etud_moy_gen + # self.moyennes_tags["but"] = MoyenneTag("but", [], None, moy_gen_but, ) + + # Ajoute les moyennes par UEs (et donc par compétence) + la moyenne générale (but) + df_ues = pd.DataFrame( + {ue.id: self.nt.etud_moy_ue[ue.id] for ue in ues_hors_sport}, + index=self.etudids, + ) + # moy_ues = self.nt.etud_moy_ue[ue_id] + # moy_gen_but = self.nt.etud_moy_gen + self.moyennes_tags["but"] = MoyennesTag( + "but", ues_hors_sport, df_ues #, moy_gen_but + ) + + 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.notes = self.df_notes() + # """Dataframe synthétique des notes par tag""" + + # pe_affichage.pe_print( + # f" => Traitement des tags {', '.join(self.tags_sorted)}" + # ) + + def get_repr(self, mode="long"): + """Nom affiché pour le semestre taggué""" + if mode == "short": + return f"{self.formsemestre} ({self.formsemestre_id})" + else: # mode == "long" + return pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True) + + def compute_moy_ues_tag(self, info_tag: dict[int, dict]) -> pd.DataFrame: + """Calcule la moyenne par UE des étudiants pour un tag, + en ayant connaissance des informations sur le tag. + + Les informations sur le tag sont un dictionnaire listant les modimpl_id rattachés au tag, + et pour chacun leur éventuel coefficient de **repondération**. + + Returns: + Le dataframe des moyennes du tag par UE + """ + + # Adaptation du mask de calcul des moyennes au tag visé + modimpls_mask = [ + modimpl.module.ue.type != UE_SPORT + for modimpl in self.formsemestre.modimpls_sorted + ] + + # 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 info_tag: + modimpls_mask[i] = False + + # Applique la pondération des coefficients + modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy() + for modimpl_id in info_tag: + ponderation = info_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) + moyennes_ues_tag = comp.moy_ue.compute_ue_moys_apc( + self.sem_cube, + self.etuds, + self.formsemestre.modimpls_sorted, + self.modimpl_inscr_df, + modimpl_coefs_ponderes_df, + modimpls_mask, + self.dispense_ues, + block=self.formsemestre.block_moyennes, + ) + return moyennes_ues_tag + + def compute_moy_gen_tag(self, moy_ues_tag: pd.DataFrame) -> pd.Series: + """Calcule la moyenne générale (toutes UE confondus) + pour le tag considéré, en les pondérant par les crédits ECTS. + """ + # Les ects + ects = self.ues_inscr_parcours_df.fillna(0.0) * [ + ue.ects for ue in self.ues if ue.type != UE_SPORT + ] + + # Calcule la moyenne générale dans le semestre (pondérée par le ECTS) + moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects( + moy_ues_tag, + ects, + formation_id=self.formsemestre.formation_id, + skip_empty_ues=True, + ) + + return moy_gen_tag + + def _get_tags_dict(self): + """Renvoie les tags personnalisés (déduits des modules du semestre) + et les tags automatiques ('but'), et toutes leurs informations, + dans un dictionnaire de la forme : + + ``{"personnalises": {tag: info_sur_le_tag}, + "auto": {tag: {}}`` + + Returns: + Le dictionnaire structuré des tags ("personnalises" vs. "auto") + """ + dict_tags = {"personnalises": dict(), "auto": dict()} + # Les tags perso + dict_tags["personnalises"] = get_synthese_tags_personnalises_semestre( + self.nt.formsemestre + ) + noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys()))) + pe_affichage.pe_print( + f"* Tags personnalisés (extraits du programme de formation) : {', '.join(noms_tags_perso)}" + ) + + # Les tags automatiques + # Déduit des compétences + # dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre) + # noms_tags_comp = list(set(dict_ues_competences.values())) + + # BUT + dict_tags["auto"] = {"but": {}} + + noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp + pe_affichage.pe_print( + f"* Tags automatiquement ajoutés : {', '.join(noms_tags_auto)}" + ) + return dict_tags + + def _check_tags(self, dict_tags): + """Vérifie l'unicité des tags""" + noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys()))) + noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp + noms_tags = noms_tags_perso + noms_tags_auto + + intersection = list(set(noms_tags_perso) & set(noms_tags_auto)) + + if intersection: + liste_intersection = "\n".join( + [f"
  • {tag}
  • " for tag in intersection] + ) + s = "s" if len(intersection) > 1 else "" + message = f"""Erreur dans le module PE : Un des tags saisis dans votre + programme de formation fait parti des tags réservés. En particulier, + votre semestre {self.formsemestre.titre_annee()} + contient le{s} tag{s} réservé{s} suivant : + + Modifiez votre programme de formation pour le{s} supprimer. + Il{s} ser{'ont' if s else 'a'} automatiquement à vos documents de poursuites d'études. + """ + raise ScoValueError(message) + + + + +def get_moduleimpl(modimpl_id) -> dict: + """Renvoie l'objet modimpl dont l'id est modimpl_id""" + modimpl = db.session.get(ModuleImpl, modimpl_id) + if modimpl: + return modimpl + return None + + +def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float: + """Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve + le module de modimpl_id + """ + # ré-écrit + modimpl = get_moduleimpl(modimpl_id) # le module + ue_status = nt.get_etud_ue_status(etudid, modimpl.module.ue.id) + if ue_status is None: + return None + return ue_status["moy"] + + +def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre): + """Etant données les implémentations des modules du semestre (modimpls), + 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 + au coeff de repondération fournie avec le tag (par défaut 1 si non indiquée)). + + Le dictionnaire fournit est de la forme : + + ``{ tag : { modimplid: {"modimpl": ModImpl, + "ponderation": coeff_de_reponderation} + } }`` + + Args: + formsemestre: Le formsemestre à la base de la recherche des tags + + Return: + Un dictionnaire décrivant les tags + """ + synthese_tags = {} + + # Instance des modules du semestre + modimpls = formsemestre.modimpls_sorted + + for modimpl in modimpls: + modimpl_id = modimpl.id + + # Liste des tags pour le module concerné + tags = sco_tag_module.module_tag_list(modimpl.module.id) + + # Traitement des tags recensés, chacun pouvant étant de la forme + # "mathématiques", "théorie", "pe:0", "maths:2" + for tag in tags: + # Extraction du nom du tag et du coeff de pondération + (tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag) + + # Ajout d'une clé pour le tag + if tagname not in synthese_tags: + synthese_tags[tagname] = {} + + # Ajout du module (modimpl) au tagname considéré + synthese_tags[tagname][modimpl_id] = { + "modimpl": modimpl, # les données sur le module + # "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre + "ponderation": ponderation, # la pondération demandée pour le tag sur le module + # "module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee + # "ue_id": modimpl.module.ue.id, # les données sur l'ue + # "ue_code": modimpl.module.ue.ue_code, + # "ue_acronyme": modimpl.module.ue.acronyme, + } + + 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. + + Ignore les UEs non associées à un niveau de compétence. + + 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.niveau_competence and ue.type != UE_SPORT: + # ?? inutilisé 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_semtag.py b/app/pe/pe_semtag.py index 8e602531..645c3da7 100644 --- a/app/pe/pe_semtag.py +++ b/app/pe/pe_semtag.py @@ -35,290 +35,216 @@ Created on Fri Sep 9 09:15:05 2016 @author: barasc """ -import pandas as pd -import app.pe.pe_etudiant -from app import db, ScoValueError -from app import comp from app.comp.res_sem import load_formsemestre_results -from app.models import FormSemestre -from app.models.moduleimpls import ModuleImpl -import app.pe.pe_affichage as pe_affichage -from app.pe.pe_tabletags import TableTag, MoyenneTag -from app.scodoc import sco_tag_module -from app.scodoc.codes_cursus import UE_SPORT +from app.models import UniteEns +from app.pe import pe_affichage +from app.pe.pe_ressemtag import ResSemTag +import pandas as pd +import numpy as np +from app.pe.pe_rcs import RCS + +from app.pe.pe_tabletags import TableTag +from app.pe.pe_moytag import MoyennesTag -class SemestreTag(TableTag): - """ - Un SemestreTag représente les résultats des étudiants à un semestre, en donnant - accès aux moyennes par tag. - Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT. - """ +class SemTag(TableTag): + def __init__(self, rcs: RCS, semestres_taggues: dict[int, ResSemTag]): + """Calcule les moyennes/classements par tag à un RCS d'un seul semestre + (ici semestre) de type 'Sx' (par ex. 'S1', 'S2', ...) : + + * pour les étudiants non redoublants, ce sont les moyennes/classements + du semestre suivi + * pour les étudiants redoublants, c'est une fusion des moyennes/classements + suivis les différents 'Sx' (donné par dans le rcs) + + Les **tags considérés** sont uniquement ceux du dernier semestre du RCS - def __init__(self, formsemestre_id: int): - """ Args: - formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base + rcs: Un RCS (identifié par un nom et l'id de son semestre terminal) + semestres_taggues: Les données sur les semestres taggués """ TableTag.__init__(self) - # Le semestre - self.formsemestre_id = formsemestre_id - self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + self.rcs_id = rcs.rcs_id + """Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)""" - # Le nom du semestre taggué + self.rcs = rcs + """RCS associé au RCS taggué""" + + assert self.rcs.nom.startswith( + "S" + ), "Un SemTag ne peut être utilisé que pour un RCS de la forme Sx" self.nom = self.get_repr() + """Représentation textuelle du RCS taggué""" - # Les résultats du semestre - self.nt = load_formsemestre_results(self.formsemestre) + # Les données du formsemestre_terminal + self.formsemestre_terminal = rcs.formsemestre_final + """Le formsemestre terminal""" - # Les étudiants - self.etuds = self.nt.etuds - self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds} - self.etudids = list(self.etudiants.keys()) + # Les résultats du formsemestre terminal + nt = load_formsemestre_results(self.formsemestre_terminal) - # Les notes, les modules implémentés triés, les étudiants, les coeffs, - # récupérés notamment de py:mod:`res_but` - self.sem_cube = self.nt.sem_cube - self.modimpls_sorted = self.nt.formsemestre.modimpls_sorted - self.modimpl_coefs_df = self.nt.modimpl_coefs_df + self.semestres_aggreges = rcs.semestres_aggreges + """Les semestres aggrégés""" - # Les inscriptions au module et les dispenses d'UE - self.modimpl_inscr_df = self.nt.modimpl_inscr_df - self.ues = self.nt.ues - self.ues_inscr_parcours_df = self.nt.load_ues_inscr_parcours() - self.dispense_ues = self.nt.dispense_ues + self.semestres_tags_aggreges = {} + """Les semestres tags associés aux semestres aggrégés""" + try: + for frmsem_id in self.semestres_aggreges: + self.semestres_tags_aggreges[frmsem_id] = semestres_taggues[frmsem_id] + except: + raise ValueError("Semestres taggués manquants") - # Les tags : - ## Saisis par l'utilisateur - tags_personnalises = get_synthese_tags_personnalises_semestre( - self.nt.formsemestre - ) - noms_tags_perso = sorted(list(set(tags_personnalises.keys()))) + # Les données des étudiants + self.etuds = nt.etuds + """Les étudiants""" + self.etudids = [etud.etudid for etud in self.etuds] + """Les etudids""" + self.etats_civils = { + etudid: self.etuds[etudid].etat_civil for etudid in self.etudids + } + """Les états civils""" - ## Déduit des compétences - # dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre) - # noms_tags_comp = list(set(dict_ues_competences.values())) - noms_tags_auto = ["but"] # + noms_tags_comp - self.tags = noms_tags_perso + noms_tags_auto - """Tags du semestre taggué""" + # Les tags + self.tags_sorted = self.comp_tags_list() + """Tags extraits du semestre terminal de l'aggrégat""" - ## Vérifie l'unicité des tags - if len(set(self.tags)) != len(self.tags): - intersection = list(set(noms_tags_perso) & set(noms_tags_auto)) - liste_intersection = "\n".join( - [f"
  • {tag}
  • " for tag in intersection] - ) - s = "s" if len(intersection) > 0 else "" - message = f"""Erreur dans le module PE : Un des tags saisis dans votre - programme de formation fait parti des tags réservés. En particulier, - votre semestre {self.formsemestre.titre_annee()} - contient le{s} tag{s} réservé{s} suivant : - - Modifiez votre programme de formation pour le{s} supprimer. - Il{s} ser{'ont' if s else 'a'} automatiquement à vos documents de poursuites d'études. - """ - raise ScoValueError(message) + # Les UEs + self.ues = self.comp_ues(tag="but") + self.acronymes_ues_sorted = sorted([ue.acronyme for ue in self.ues.values()]) + """UEs extraites du semestre terminal de l'aggrégat (avec + check de concordance sur les UE des semestres_aggrégés)""" - ues_hors_sport = [ue for ue in self.ues if ue.type != UE_SPORT] + self.moyennes_tags: dict[str, MoyennesTag] = {} + """Moyennes/classements par tag (qu'ils soient personnalisés ou automatiques)""" - # Calcul des moyennes & les classements de chaque étudiant à chaque tag - self.moyennes_tags = {} + self.notes: dict[str, pd.DataFrame] = {} + """Les notes aux différents tags""" + for tag in self.tags_sorted: + # Cube de note + notes_cube = self.compute_notes_ues_cube(tag, self.acronymes_ues) - for tag in tags_personnalises: - # pe_affichage.pe_print(f" -> Traitement du tag {tag}") - moy_ues_tag = self.compute_moy_ues_tag(tag, tags_personnalises) - moy_gen_tag = self.compute_moy_gen_tag(moy_ues_tag) + # Calcule des moyennes sous forme d'un dataframe""" + self.notes[tag] = compute_notes_ues(notes_cube, self.etudids, self.acronymes_ues) - self.moyennes_tags[tag] = MoyenneTag( - tag, ues_hors_sport, moy_ues_tag, moy_gen_tag - ) + # Les moyennes + self.moyennes_tags[tag] = MoyennesTag(tag, self.notes[tag]) - # Ajoute les d'UE moyennes générales de BUT pour le semestre considéré - # moy_gen_but = self.nt.etud_moy_gen - # self.moyennes_tags["but"] = MoyenneTag("but", [], None, moy_gen_but, ) + def __eq__(self, other): + """Egalité de 2 RCS taggués sur la base de leur identifiant""" + return self.rcs_id == other.rcs_id - # Ajoute les moyennes par UEs (et donc par compétence) + la moyenne générale (but) - df_ues = pd.DataFrame({ue.id: self.nt.etud_moy_ue[ue.id] for ue in ues_hors_sport}, - index = self.etudids) - # moy_ues = self.nt.etud_moy_ue[ue_id] - moy_gen_but = self.nt.etud_moy_gen - self.moyennes_tags["but"] = MoyenneTag("but", ues_hors_sport, df_ues, moy_gen_but) + def get_repr(self, verbose=False) -> str: + """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle + est basée)""" + return self.rcs.get_repr(verbose=verbose) + def compute_notes_ues_cube(self, tag, acronymes_ues_sorted): + """Construit le cube de notes des UEs (etudid x accronyme_ue x semestre_aggregé) + nécessaire au calcul des moyennes du tag pour le RCS Sx + """ + # Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2) + etudids = [etud.etudid for etud in self.etuds] + # acronymes_ues = sorted([ue.acronyme for ue in self.ues.values()]) + semestres_id = list(self.semestres_tags_aggreges.keys()) - self.tags_sorted = self.get_all_tags() - """Tags (personnalisés+compétences) par ordre alphabétique""" + dfs = {} - # Synthétise l'ensemble des moyennes dans un dataframe + for frmsem_id in semestres_id: + # Partant d'un dataframe vierge + df = pd.DataFrame(np.nan, index=etudids, columns=acronymes_ues_sorted) - self.notes = self.df_notes() - """Dataframe synthétique des notes par tag""" + # Charge les notes du semestre tag + sem_tag = self.semestres_tags_aggreges[frmsem_id] + moys_tag = sem_tag.moyennes_tags[tag] + notes = moys_tag.notes_ues # dataframe etudids x ues + acronymes_ues_sem = list(notes.columns) # les acronymes des UEs du semestre tag - pe_affichage.pe_print( - f" => Traitement des tags {', '.join(self.tags_sorted)}" - ) + # UEs communes à celles du SemTag (celles du dernier semestre du RCS) + ues_communes = list(set(acronymes_ues_sorted) & set(acronymes_ues_sem)) - def get_repr(self): - """Nom affiché pour le semestre taggué""" - return app.pe.pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True) + # Etudiants communs + etudids_communs = df.index.intersection(notes.index) - def compute_moy_ues_tag(self, tag: str, tags_infos: dict) -> pd.Series: - """Calcule la moyenne par UE des étudiants pour le tag indiqué, - pour ce SemestreTag, en ayant connaissance des informations sur - les tags (dictionnaire donnant les coeff de repondération) + # Recopie + df.loc[etudids_communs, ues_communes] = notes.loc[etudids_communs, ues_communes] - Sont pris en compte les modules implémentés associés au tag, - avec leur éventuel coefficient de **repondération**, en utilisant les notes - chargées pour ce SemestreTag. + # Supprime tout ce qui n'est pas numérique + for col in df.columns: + df[col] = pd.to_numeric(df[col], errors="coerce") - Force ou non le calcul de la moyenne lorsque des notes sont manquantes. + # Stocke le df + dfs[frmsem_id] = df + + """Réunit les notes sous forme d'un cube semestres x etdids x ues""" + semestres_x_etudids_x_ues = [dfs[fid].values for fid in dfs] + etudids_x_ues_x_semestres = np.stack(semestres_x_etudids_x_ues, axis=-1) + return etudids_x_ues_x_semestres + + def comp_tags_list(self): + """Récupère les tag du semestre taggué associé au semestre final du RCS Returns: - La série des moyennes + Une liste de tags triés par ordre alphabétique """ + tags = [] + dernier_frmid = self.formsemestre_terminal.formsemestre_id + dernier_semestre_tag = self.semestres_tags_aggreges[dernier_frmid] + tags = dernier_semestre_tag.tags_sorted + pe_affichage.pe_print(f"* Tags : {', '.join(tags)}") + return tags - # Adaptation du mask de calcul des moyennes au tag visé - modimpls_mask = [ - modimpl.module.ue.type != UE_SPORT - for modimpl in self.formsemestre.modimpls_sorted - ] + def comp_ues(self, tag="but") -> dict[int, UniteEns]: + """Récupère les UEs à aggréger, en s'appuyant sur la moyenne générale + (tag but) du semestre final du RCS - # 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 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 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)# - moyennes_ues_tag = comp.moy_ue.compute_ue_moys_apc( - self.sem_cube, - self.etuds, - self.formsemestre.modimpls_sorted, - self.modimpl_inscr_df, - modimpl_coefs_ponderes_df, - modimpls_mask, - self.dispense_ues, - block=self.formsemestre.block_moyennes, - ) - return moyennes_ues_tag - - def compute_moy_gen_tag(self, moy_ues_tag: pd.DataFrame) -> pd.Series: - """Calcule la moyenne générale (toutes UE confondus) - pour le tag considéré, en les pondérant par les crédits ECTS. + Returns: + Un dictionnaire donnant les UEs """ - # Les ects - ects = self.ues_inscr_parcours_df.fillna(0.0) * [ - ue.ects for ue in self.ues if ue.type != UE_SPORT - ] - - # Calcule la moyenne générale dans le semestre (pondérée par le ECTS) - moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects( - moy_ues_tag, - ects, - formation_id=self.formsemestre.formation_id, - skip_empty_ues=True, - ) - - return moy_gen_tag + dernier_frmid = self.formsemestre_terminal.formsemestre_id + dernier_semestre_tag = self.semestres_tags_aggreges[dernier_frmid] + moy_tag = dernier_semestre_tag.moyennes_tags[tag] + return moy_tag.ues # les UEs -def get_moduleimpl(modimpl_id) -> dict: - """Renvoie l'objet modimpl dont l'id est modimpl_id""" - modimpl = db.session.get(ModuleImpl, modimpl_id) - if modimpl: - return modimpl - return None - - -def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float: - """Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve - le module de modimpl_id - """ - # ré-écrit - modimpl = get_moduleimpl(modimpl_id) # le module - ue_status = nt.get_etud_ue_status(etudid, modimpl.module.ue.id) - if ue_status is None: - return None - return ue_status["moy"] - - -def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre): - """Etant données les implémentations des modules du semestre (modimpls), - 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)). - +def compute_notes_ues(set_cube: np.array, etudids: list, acronymes_ues: list): + """Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE + par UE) obtenue par un étudiant à un semestre. Args: - formsemestre: Le formsemestre à la base de la recherche des tags - - Return: - Un dictionnaire de tags - """ - synthese_tags = {} - - # Instance des modules du semestre - modimpls = formsemestre.modimpls_sorted - - for modimpl in modimpls: - modimpl_id = modimpl.id - - # Liste des tags pour le module concerné - tags = sco_tag_module.module_tag_list(modimpl.module.id) - - # Traitement des tags recensés, chacun pouvant étant de la forme - # "mathématiques", "théorie", "pe:0", "maths:2" - for tag in tags: - # Extraction du nom du tag et du coeff de pondération - (tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag) - - # Ajout d'une clé pour le tag - if tagname not in synthese_tags: - synthese_tags[tagname] = {} - - # Ajout du module (modimpl) au tagname considéré - synthese_tags[tagname][modimpl_id] = { - "modimpl": modimpl, # les données sur le module - # "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre - "ponderation": ponderation, # la pondération demandée pour le tag sur le module - # "module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee - # "ue_id": modimpl.module.ue.id, # les données sur l'ue - # "ue_code": modimpl.module.ue.ue_code, - # "ue_acronyme": modimpl.module.ue.acronyme, - } - - 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. - - Ignore les UEs non associées à un niveau de compétence. - - Args: - formsemestre: Un FormSemestre - + set_cube: notes moyennes aux modules ndarray + (semestre_ids x etudids x UEs), des floats avec des NaN + etudids: liste des étudiants (dim. 0 du cube) + acronymes_ues: liste des acronymes des ues (dim. 1 du cube) Returns: - Dictionnaire {ue_id: nom_competence} lisant tous les noms des compétences - en les raccrochant à leur ue + Un DataFrame avec pour columns les moyennes par ues, + et pour rows les etudid """ - # Les résultats du semestre - nt = load_formsemestre_results(formsemestre) + nb_etuds, nb_ues, nb_semestres = set_cube.shape + assert nb_etuds == len(etudids) + assert nb_ues == len(acronymes_ues) - noms_competences = {} - for ue in nt.ues: - if ue.niveau_competence and ue.type != UE_SPORT: - # ?? inutilisé ordre = ue.niveau_competence.ordre - nom = ue.niveau_competence.competence.titre - noms_competences[ue.ue_id] = f"comp. {nom}" - return noms_competences + # Quelles entrées du cube contiennent des notes ? + mask = ~np.isnan(set_cube) + + # Enlève les NaN du cube pour les entrées manquantes + set_cube_no_nan = np.nan_to_num(set_cube, nan=-1.0) + + # Les moyennes par ues + # TODO: Pour l'instant un max sans prise en compte des UE capitalisées + etud_moy = np.max(set_cube_no_nan, axis=2) + + # Fix les max non calculé -1 -> NaN + etud_moy[etud_moy < 0] = np.NaN + + # Le dataFrame + etud_moy_tag_df = pd.DataFrame( + etud_moy, + index=etudids, # les etudids + columns=acronymes_ues, # les tags + ) + + etud_moy_tag_df.fillna(np.nan) + + return etud_moy_tag_df diff --git a/app/pe/pe_tabletags.py b/app/pe/pe_tabletags.py index a761fa0b..a31df300 100644 --- a/app/pe/pe_tabletags.py +++ b/app/pe/pe_tabletags.py @@ -37,195 +37,11 @@ Created on Thu Sep 8 09:36:33 2016 @author: barasc """ -import datetime -import numpy as np - -from app import ScoValueError -from app.comp.moy_sem import comp_ranks_series -from app.models import UniteEns -from app.pe import pe_affichage -from app.pe.pe_affichage import SANS_NOTE -from app.scodoc import sco_utils as scu import pandas as pd -from app.scodoc.codes_cursus import UE_SPORT - TAGS_RESERVES = ["but"] -class MoyenneTag: - def __init__( - self, - tag: str, - ues: list[UniteEns], - notes_ues: pd.DataFrame, - notes_gen: pd.Series, - ): - """Classe centralisant la synthèse des moyennes/classements d'une série - d'étudiants à un tag donné, en stockant : - - `` - { - "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 - ues: La liste des UEs ayant servie au calcul de la moyenne - notes_ues: Les moyennes (etudid x ues) aux différentes UEs et pour le tag - notes_gen: Une série de notes (moyenne) sous forme d'un pd.Series() (toutes UEs confondues) - """ - self.tag = tag - """Le tag associé à la moyenne""" - self.etudids = list(notes_gen.index) # calcul à venir - """Les id des étudiants""" - self.ues: list[UniteEns] = ues - """Les UEs sur lesquelles sont calculées les moyennes""" - self.df_ues: dict[int, pd.DataFrame] = {} - """Les dataframes retraçant les moyennes/classements/statistiques des étudiants aux UEs""" - for ue in self.ues: # if ue.type != UE_SPORT: - notes = notes_ues[ue.id] - self.df_ues[ue.id] = self.comp_moy_et_stat(notes) - - self.inscrits_ids = notes_gen[notes_gen.notnull()].index.to_list() - """Les id des étudiants dont la moyenne générale est non nulle""" - self.df_gen: pd.DataFrame = self.comp_moy_et_stat(notes_gen) - """Le dataframe retraçant les moyennes/classements/statistiques général""" - self.synthese = self.to_dict() - """La synthèse (dictionnaire) des notes/classements/statistiques""" - - 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 - de notes (pouvant être une moyenne d'un tag à une UE ou une moyenne générale - d'un 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) - """ - df = pd.DataFrame( - np.nan, - index=self.etudids, - columns=[ - "note", - "classement", - "rang", - "min", - "max", - "moy", - "nb_etuds", - "nb_inscrits", - ], - ) - - # Supprime d'éventuelles chaines de caractères dans les notes - notes = pd.to_numeric(notes, errors="coerce") - df["note"] = notes - - # Les nb d'étudiants & nb d'inscrits - df["nb_etuds"] = len(self.etudids) - # Les étudiants dont la note n'est pas nulle - inscrits_ids = notes[notes.notnull()].index.to_list() - df.loc[inscrits_ids, "nb_inscrits"] = len(inscrits_ids) - - # Le classement des inscrits - notes_non_nulles = notes[inscrits_ids] - (class_str, class_int) = comp_ranks_series(notes_non_nulles) - df.loc[inscrits_ids, "classement"] = class_int - - # Le rang (classement/nb_inscrit) - df["rang"] = df["rang"].astype(str) - df.loc[inscrits_ids, "rang"] = ( - df.loc[inscrits_ids, "classement"].astype(int).astype(str) - + "/" - + df.loc[inscrits_ids, "nb_inscrits"].astype(int).astype(str) - ) - - # Les stat (des inscrits) - df.loc[inscrits_ids, "min"] = notes.min() - df.loc[inscrits_ids, "max"] = notes.max() - df.loc[inscrits_ids, "moy"] = notes.mean() - - return df - - def to_dict(self) -> dict: - """Renvoie un dictionnaire de synthèse des moyennes/classements/statistiques""" - synthese = { - "notes": self.df_gen["note"], - "classements": self.df_gen["classement"], - "min": self.df_gen["min"].mean(), - "max": self.df_gen["max"].mean(), - "moy": self.df_gen["moy"].mean(), - "nb_inscrits": self.df_gen["nb_inscrits"].mean(), - } - return synthese - - def get_notes(self): - """Série des notes, arrondies à 2 chiffres après la virgule""" - return self.df_gen["note"].round(2) - - def get_rangs_inscrits(self) -> pd.Series: - """Série des rangs classement/nbre_inscrit""" - return self.df_gen["rang"] - - def get_min(self) -> pd.Series: - """Série des min""" - return self.df_gen["min"].round(2) - - def get_max(self) -> pd.Series: - """Série des max""" - return self.df_gen["max"].round(2) - - def get_moy(self) -> pd.Series: - """Série des moy""" - return self.df_gen["moy"].round(2) - - def get_note_for_df(self, etudid: int): - """Note d'un étudiant donné par son etudid""" - return round(self.df_gen["note"].loc[etudid], 2) - - def get_min_for_df(self) -> float: - """Min renseigné pour affichage dans un df""" - return round(self.synthese["min"], 2) - - def get_max_for_df(self) -> float: - """Max renseigné pour affichage dans un df""" - return round(self.synthese["max"], 2) - - 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 = self.df_gen["rang"].loc[etudid] - if not pd.isna(classement): - return classement - else: - return pe_affichage.SANS_NOTE - - 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 @@ -233,7 +49,6 @@ class TableTag(object): """ 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 @@ -261,8 +76,9 @@ class TableTag(object): 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}")) + moy_gen = moy_tag.moy_gen + df = df.join(moy_gen.synthese["notes"].rename(f"Moy {tag}")) + df = df.join(moy_gen.synthese["classements"].rename(f"Class {tag}")) return df diff --git a/config.py b/config.py index af1a173e..eefebdfc 100755 --- a/config.py +++ b/config.py @@ -65,7 +65,7 @@ class DevConfig(Config): ) SECRET_KEY = os.environ.get("DEV_SECRET_KEY") or "bb3faec7d9a34eb68a8e3e710087d87a" # pour le avoir url_for dans le shell: - # SERVER_NAME = "http://localhost:8080" + SERVER_NAME = "http://localhost:8080" class TestConfig(DevConfig):