From d6a75b176e6e5a59f2b1d8f9e7df55b769cfe446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Mon, 19 Feb 2024 14:50:38 +0100 Subject: [PATCH] =?UTF-8?q?Am=C3=A9lioration=20structure=20codes=20+=20mis?= =?UTF-8?q?e=20en=20place=20des=20capitalisations=20dans=20les=20SxTag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_comp.py | 29 ++- app/pe/pe_interclasstag.py | 19 +- app/pe/pe_jury.py | 56 ++--- app/pe/pe_moytag.py | 4 +- app/pe/{pe_rcs.py => pe_rcss_jury.py} | 312 +++++--------------------- app/pe/pe_rcstag.py | 44 ++-- app/pe/pe_ressemtag.py | 108 +++++---- app/pe/pe_sxtag.py | 271 +++++++++++++++------- app/pe/pe_tabletags.py | 13 +- app/pe/rcss/__init__.py | 0 app/pe/rcss/pe_rcf.py | 57 +++++ app/pe/rcss/pe_rcrcf.py | 69 ++++++ app/pe/rcss/pe_rcs.py | 62 +++++ app/pe/rcss/rcss_constantes.py | 64 ++++++ 14 files changed, 658 insertions(+), 450 deletions(-) rename app/pe/{pe_rcs.py => pe_rcss_jury.py} (51%) create mode 100644 app/pe/rcss/__init__.py create mode 100644 app/pe/rcss/pe_rcf.py create mode 100644 app/pe/rcss/pe_rcrcf.py create mode 100644 app/pe/rcss/pe_rcs.py create mode 100644 app/pe/rcss/rcss_constantes.py diff --git a/app/pe/pe_comp.py b/app/pe/pe_comp.py index 24edf207a..4f4ba8ed0 100644 --- a/app/pe/pe_comp.py +++ b/app/pe/pe_comp.py @@ -41,13 +41,13 @@ import datetime import re import unicodedata - +import pandas as pd from flask import g import app.scodoc.sco_utils as scu from app.models import FormSemestre -from app.pe.pe_rcs import TYPES_RCS +from app.pe.rcss.rcss_constantes import TYPES_RCS from app.scodoc import sco_formsemestre from app.scodoc.sco_logos import find_logo @@ -291,6 +291,29 @@ def tri_semestres_par_rang(cosemestres: dict[int, FormSemestre]): dictionnaire {rang: [liste des semestres du dit rang]}""" cosemestres_tries = {} for sem in cosemestres.values(): - cosemestres_tries[sem.semestre_id] = cosemestres_tries.get(sem.semestre_id, []) + [sem] + cosemestres_tries[sem.semestre_id] = cosemestres_tries.get( + sem.semestre_id, [] + ) + [sem] return cosemestres_tries + +def find_index_and_columns_communs( + df1: pd.DataFrame, df2: pd.DataFrame +) -> (list, list): + """Partant de 2 DataFrames ``df1`` et ``df2``, renvoie les indices de lignes + et de colonnes, communes aux 2 dataframes + + Args: + df1: Un dataFrame + df2: Un dataFrame + Returns: + Le tuple formé par la liste des indices de lignes communs et la liste des indices + de colonnes communes entre les 2 dataFrames + """ + indices1 = df1.index + indices2 = df2.index + indices_communs = list(df1.index.intersection(df2.index)) + colonnes1 = df1.columns + colonnes2 = df2.columns + colonnes_communes = list(set(colonnes1) & set(colonnes2)) + return indices_communs, colonnes_communes diff --git a/app/pe/pe_interclasstag.py b/app/pe/pe_interclasstag.py index b015136bf..911050665 100644 --- a/app/pe/pe_interclasstag.py +++ b/app/pe/pe_interclasstag.py @@ -38,9 +38,10 @@ import numpy as np 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 +import app.pe.pe_etudiant as pe_etudiant +import app.pe.rcss.pe_rcs as pe_rcs +import app.pe.pe_rcss_jury as pe_rcss_jury +import app.pe.pe_rcstag as pe_rcstag class RCSInterclasseTag(TableTag): @@ -58,9 +59,9 @@ class RCSInterclasseTag(TableTag): def __init__( self, nom_rcs: str, - etudiants: EtudiantsJuryPE, - rcss_jury_pe: RCSsJuryPE, - rcss_tags: dict[tuple, RCSTag], + etudiants: pe_etudiant.EtudiantsJuryPE, + rcss_jury_pe: pe_rcss_jury.RCSsJuryPE, + rcss_tags: dict[tuple, pe_rcstag.RCSTag], ): TableTag.__init__(self) @@ -80,21 +81,21 @@ class RCSInterclasseTag(TableTag): # Les trajectoires (et leur version tagguées), en ne gardant que # celles associées à l'aggrégat - self.rcss: dict[int, RCS] = {} + self.rcss: dict[int, pe_rcs.RCS] = {} """Ensemble des trajectoires associées à l'aggrégat""" for trajectoire_id in rcss_jury_pe.rcss: trajectoire = rcss_jury_pe.rcss[trajectoire_id] if trajectoire_id[0] == nom_rcs: self.rcss[trajectoire_id] = trajectoire - self.trajectoires_taggues: dict[int, RCS] = {} + self.trajectoires_taggues: dict[int, pe_rcs.RCS] = {} """Ensemble des trajectoires tagguées associées à l'aggrégat""" for trajectoire_id in self.rcss: self.trajectoires_taggues[trajectoire_id] = rcss_tags[trajectoire_id] # Les trajectoires suivies par les étudiants du jury, en ne gardant que # celles associées aux diplomés - self.suivi: dict[int, RCS] = {} + self.suivi: dict[int, pe_rcs.RCS] = {} """Association entre chaque étudiant et la trajectoire tagguée à prendre en compte pour l'aggrégat""" for etudid in self.diplomes_ids: diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index c56f70c5c..323080d32 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -50,15 +50,16 @@ from zipfile import ZipFile import numpy as np import pandas as pd +import app.pe.rcss.rcss_constantes as rcss_constants from app.pe import pe_sxtag from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE import app.pe.pe_affichage as pe_affichage import app.pe.pe_etudiant as pe_etudiant -import app.pe.pe_rcs as pe_rcs +import app.pe.rcss.pe_rcs as pe_rcs from app.pe.pe_rcstag import RCSTag from app.pe.pe_ressemtag import ResSemBUTTag from app.pe.pe_interclasstag import RCSInterclasseTag -import app.pe.pe_comp as pe_comp +import app.pe.pe_rcss_jury as pe_rcss_jury class JuryPE(object): @@ -94,7 +95,7 @@ class JuryPE(object): self.etudiants.find_etudiants() self.diplomes_ids = self.etudiants.diplomes_ids - self.rcss_jury = pe_rcs.RCSsJuryPE(self.diplome) + self.rcss_jury = pe_rcss_jury.RCSsJuryPE(self.diplome) """Les informations sur les regroupements de semestres""" self.zipdata = io.BytesIO() @@ -102,16 +103,18 @@ class JuryPE(object): if not self.diplomes_ids: pe_affichage.pe_print("*** Aucun étudiant diplômé") else: - self._gen_xls_diplomes(zipfile) - - self._gen_xls_ressembuttags(zipfile) - self._gen_rcss() - self._gen_xls_sxtags(zipfile) - self._gen_rcrcfs() - self._gen_xls_rcrcss_tags(zipfile) - # self._gen_xls_interclassements_rcss(zipfile) - # self._gen_xls_synthese_jury_par_tag(zipfile) - # self._gen_xls_synthese_par_etudiant(zipfile) + try: + self._gen_xls_diplomes(zipfile) + self._gen_xls_ressembuttags(zipfile) + self._gen_rcss() + self._gen_xls_sxtags(zipfile) + self._gen_rcrcfs() + self._gen_xls_rcrcss_tags(zipfile) + # self._gen_xls_interclassements_rcss(zipfile) + # self._gen_xls_synthese_jury_par_tag(zipfile) + # self._gen_xls_synthese_par_etudiant(zipfile) + except Exception as e: + raise e # et le log self._add_log_to_zip(zipfile) @@ -150,7 +153,7 @@ class JuryPE(object): formsemestres = get_formsemestres_etudiants(self.etudiants) pe_affichage.pe_print( - f" --> {len(formsemestres)} résultats de semestres à considérer" + f"--> {len(formsemestres)} résultats de semestres à considérer" ) self.ressembuttags = {} @@ -186,14 +189,15 @@ class JuryPE(object): ) self.rcss_jury.cree_rcss(self.etudiants) - self.rcss_jury.cree_rcfs(self.etudiants) def _gen_xls_sxtags(self, zipfile: ZipFile): """Génère les semestres taggués en s'appuyant sur les RCS de type Sx (pour identifier les redoublements impactant les semestres taggués). """ # Génère les regroupements de semestres de type Sx - pe_affichage.pe_print("*** Génère les RCF (RCS de type Sx)***") + pe_affichage.pe_print( + "*** Génère les RCSValid (RCS de même Sx donnant lieu à validation du semestre)" + ) self.rcss_jury.cree_rcfs(self.etudiants) # Génère les moyennes des RCS de type Sx @@ -204,11 +208,8 @@ class JuryPE(object): for rcf_id, rcf in self.rcss_jury.rcfs.items(): # SxTag traduisant le RCF sxtag_id = rcf_id - # Les resultats des semestres taggués à prendre en compte dans le RCF - ressemstags = { - fid: self.ressembuttags[fid] for fid in rcf.semestres_aggreges - } - self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, ressemstags) + + self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, rcf, self.ressembuttags) # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() @@ -232,10 +233,11 @@ class JuryPE(object): def _gen_rcrcfs(self): """Génère les regroupements cohérents de RCFs qu'ont suivi chaque étudiant""" - pe_affichage.pe_print("*** Génère les RCRCF (regroupements de RCF de type Sx) amenant du S1 à un semestre final***") + pe_affichage.pe_print( + "*** Génère les RCRCF (regroupements de RCF de type Sx) amenant du S1 à un semestre final***" + ) self.rcss_jury.cree_rcrcfs(self.etudiants) - def _gen_xls_rcrcss_tags(self, zipfile: ZipFile): """Génère les RCS taggués traduisant les moyennes (orientées compétences) de regroupements de semestre de type Sx, xA ou xS. @@ -268,7 +270,7 @@ class JuryPE(object): output, engine="openpyxl" ) as writer: for rcs_tag in self.rcss_tags.values(): - onglet = rcs_tag.get_repr(mode="short") + onglet = rcs_tag.get_repr(verbose=False) df = rcs_tag.df_moyennes_et_classements() # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) @@ -276,7 +278,7 @@ class JuryPE(object): self.add_file_to_zip( zipfile, - f"RCS_taggues_{self.diplome}.xlsx", + f"RCRCFs_{self.diplome}.xlsx", output.read(), path="details", ) @@ -431,7 +433,7 @@ class JuryPE(object): # Ajout des aggrégats for aggregat in pe_rcs.TOUS_LES_RCS: - descr = pe_rcs.TYPES_RCS[aggregat]["descr"] + descr = app.pe.rcss.constantes.TYPES_RCS[aggregat]["descr"] # Les trajectoires (tagguées) suivies par les étudiants pour l'aggrégat et le tag # considéré @@ -632,7 +634,7 @@ def get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict: def compute_interclassements( etudiants: pe_etudiant.EtudiantsJuryPE, - trajectoires_jury_pe: pe_rcs.RCSsJuryPE, + trajectoires_jury_pe: pe_rcss_jury.RCSsJuryPE, trajectoires_tagguees: dict[tuple, pe_rcs.RCS], ): """Interclasse les étudiants, (nom d') aggrégat par aggrégat, diff --git a/app/pe/pe_moytag.py b/app/pe/pe_moytag.py index 05d3882b8..1df487ff9 100644 --- a/app/pe/pe_moytag.py +++ b/app/pe/pe_moytag.py @@ -181,7 +181,7 @@ class MoyennesTag: self.matrice_notes: pd.DataFrame = matrice_notes """Les notes aux UEs ou aux compétences (DataFrame)""" - self.matrice_coeffs: pd.DataFrame = matrice_coeffs + self.matrice_coeffs_moy_gen: pd.DataFrame = matrice_coeffs """Les coeffs à appliquer pour le calcul des moyennes générales (toutes UE ou compétences confondues). NaN si étudiant non inscrit""" @@ -193,7 +193,7 @@ class MoyennesTag: self.moyennes[col] = Moyenne(notes) # Les moyennes générales - notes_gen = self.compute_moy_gen(self.matrice_notes, self.matrice_coeffs) + notes_gen = self.compute_moy_gen(self.matrice_notes, self.matrice_coeffs_moy_gen) self.notes_gen = notes_gen """Les notes générales (moyenne toutes UEs confonudes)""" self.moyenne_gen = Moyenne(notes_gen) diff --git a/app/pe/pe_rcs.py b/app/pe/pe_rcss_jury.py similarity index 51% rename from app/pe/pe_rcs.py rename to app/pe/pe_rcss_jury.py index 0233841fd..e514e5bbb 100644 --- a/app/pe/pe_rcs.py +++ b/app/pe/pe_rcss_jury.py @@ -1,248 +1,11 @@ -############################################################################## -# Module "Avis de poursuite d'étude" -# conçu et développé par Cléo Baras (IUT de Grenoble) -############################################################################## - -""" -Created on 01-2024 - -@author: barasc -""" +import app.pe.rcss.pe_rcf as pe_rcf +import app.pe.rcss.pe_rcrcf as pe_rcrcf +import app.pe.pe_etudiant as pe_etudiant import app.pe.pe_comp as pe_comp - +import app.pe.rcss.rcss_constantes as rcss_constantes from app.models import FormSemestre -from app.pe import pe_sxtag, pe_affichage -from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date - -TYPES_RCS = { - "S1": { - "aggregat": ["S1"], - "descr": "Semestre 1 (S1)", - }, - "S2": { - "aggregat": ["S2"], - "descr": "Semestre 2 (S2)", - }, - "1A": { - "aggregat": ["S1", "S2"], - "descr": "BUT1 (S1+S2)", - }, - "S3": { - "aggregat": ["S3"], - "descr": "Semestre 3 (S3)", - }, - "S4": { - "aggregat": ["S4"], - "descr": "Semestre 4 (S4)", - }, - "2A": { - "aggregat": ["S3", "S4"], - "descr": "BUT2 (S3+S4)", - }, - "3S": { - "aggregat": ["S1", "S2", "S3"], - "descr": "Moyenne du semestre 1 au semestre 3 (S1+S2+S3)", - }, - "4S": { - "aggregat": ["S1", "S2", "S3", "S4"], - "descr": "Moyenne du semestre 1 au semestre 4 (S1+S2+S3+S4)", - }, - "S5": { - "aggregat": ["S5"], - "descr": "Semestre 5 (S5)", - }, - "S6": { - "aggregat": ["S6"], - "descr": "Semestre 6 (S6)", - }, - "3A": { - "aggregat": ["S5", "S6"], - "descr": "3ème année (S5+S6)", - }, - "5S": { - "aggregat": ["S1", "S2", "S3", "S4", "S5"], - "descr": "Moyenne du semestre 1 au semestre 5 (S1+S2+S3+S4+S5)", - }, - "6S": { - "aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"], - "descr": "Moyenne globale (S1+S2+S3+S4+S5+S6)", - }, -} -"""Dictionnaire détaillant les différents regroupements cohérents -de semestres (RCS), en leur attribuant un nom et en détaillant -le nom des semestres qu'ils regroupent et l'affichage qui en sera fait -dans les tableurs de synthèse. -""" - -TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS if not cle.startswith("S")] -TOUS_LES_RCS = list(TYPES_RCS.keys()) -TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")] - - -def get_descr_rcs(nom_rcs: str) -> str: - """Renvoie la description pour les tableurs de synthèse - Excel d'un nom de RCS""" - return TYPES_RCS[nom_rcs]["descr"] - - -class RCS: - """Modélise un regroupement cohérent de semestres (formsemestre ou de Sx)""" - - def __init__(self, nom_rcs: str, semestre_final: FormSemestre): - self.nom: str = nom_rcs - """Nom du RCS""" - - self.formsemestre_final: FormSemestre = semestre_final - """FormSemestre terminal du RCS""" - - self.rang_final = self.formsemestre_final.semestre_id - """Le rang du formsemestre final""" - - self.rcs_id: (str, int) = (nom_rcs, semestre_final.formsemestre_id) - """Identifiant du RCS sous forme (nom_rcs, id du semestre_terminal)""" - - def get_formsemestre_id_final(self) -> int: - """Renvoie l'identifiant du formsemestre final du RCS - - Returns: - L'id du formsemestre final (marquant la fin) du RCS - """ - return self.formsemestre_final.formsemestre_id - - def __repr__(self): - """Représentation textuelle d'un RCS""" - return f"{self.nom} ({self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}" - - def get_repr(self): - return self.__repr__() - - def __eq(self, other): - """Egalité de RCS""" - return ( - self.nom == other.nom - and self.formsemestre_final == other.formsemestre_final - ) - - -class RCF(RCS): - """Modélise un ensemble de (form)semestres d'étudiants - associé à un type de regroupement cohérent de semestres - donné (par ex: 'S2', '3S', '2A'). - - Si le RCF est un semestre de type Si, stocke 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 le RCF de type iS ou iA (par ex, 3A=S1+S2+S3), identifie - les semestres que les étudiants ont suivis pour les amener jusqu'au semestre - terminal du RCS (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_rcs: Un nom du RCS (par ex: '5S') - semestre_final: Le formsemestre final du RCS - """ - - def __init__(self, nom_rcs: str, semestre_final: FormSemestre): - RCS.__init__(self, nom_rcs, semestre_final) - - self.semestres_aggreges: dict[int:FormSemestre] = {} - """Formsemestres regroupés dans le RCS""" - - def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]): - """Ajout de semestres aux semestres à regrouper - - Args: - semestres: Dictionnaire ``{fid: Formsemestre)`` - """ - self.semestres_aggreges = self.semestres_aggreges | semestres - - def get_repr(self, verbose=True) -> str: - """Représentation textuelle d'un RCS - basé sur ses semestres aggrégés""" - - noms = [] - for fid in self.semestres_aggreges: - semestre = self.semestres_aggreges[fid] - noms.append(f"S{semestre.semestre_id}({fid})") - noms = sorted(noms) - title = f"""{str(self)}""" - if verbose and noms: - title += " - " + "+".join(noms) - return title - - - -def get_rcf_from_semestres_aggreges( - rcfs: dict[(str, int):RCF], semestres_a_aggreges: list[FormSemestre] - ) -> (str, int): - """Partant d'un dictionnaire de RCFs (de la forme - ``{ (nom_rcs, fid): RCF }, et connaissant une liste - de (form)semestres à aggréger, renvoie l'identifiant - (nom_rcs, fid) du RCFs qui lui correspond (c'est à dire celui dont - les semestres_aggregés par le RCF sont les même que les - semestres_a_aggreger. - - Returns: - rcf_id: L'identifiant du RCF trouvé - """ - fids_semestres_a_aggreger = set( - [frms.formsemestre_id for frms in semestres_a_aggreges] - ) - for rcf_id, rcf in rcfs.items(): - fids_rcf = set(rcf.semestres_aggreges) - if fids_rcf == fids_semestres_a_aggreger: - return rcf_id - return None - - -class RCRCF: - """Modélise les RCF d'étudiants suivis par un étudiant dans - le cadre d'un RCS donné (par ex: 3S=S1+S2+S3). - - Pour rappel : un RCF (par ex. S1) combine les semestres 1 qu'a suivi - l'étudiant pour valider son S1 (1 si étudiant standard, 2 si redoublant). - - Le RCRCF 3S est donc le regroupement du RCF S1 + RCF S2 + RCF S3. - - Il est identifié par le formsemestre de S3 marquant la fin du regroupement. - - - Args: - nom_rcs: Un nom du RCS (par ex: '5S') - semestre_final: Le semestre final du RCS - """ - - def __init__(self, nom_rcs: str, semestre_final: FormSemestre): - RCS.__init__(self, nom_rcs, semestre_final) - - self.rcfs_aggreges: dict[(str, int) : pe_sxtag.SxTag] = {} - """Les RCFs à aggréger""" - - def add_rcfs_a_aggreger(self, rcfs: dict[(str, int):RCF]): - """Ajout des RCFs aux RCFS à regrouper - - Args: - rcfs: Dictionnaire ``{(str,fid): RCF}`` à ajouter - """ - self.rcfs_aggreges = self.rcfs_aggreges | rcfs - - def get_repr(self, verbose=True) -> str: - """Représentation textuelle d'un RCSF - basé sur ses RCF aggrégés""" - - noms = [] - for rcf_id, rcf in self.rcfs_aggreges.items(): - noms.append(rcf.get_repr()) - title = f"""{str(self)}""" - if verbose and noms: - title += " : " + "+".join(noms) - return title +from app.pe import pe_affichage class RCSsJuryPE: @@ -257,28 +20,28 @@ class RCSsJuryPE: self.annee_diplome = annee_diplome """Année de diplômation""" - self.rcss: dict[tuple(int, str) : RCF] = {} + self.rcss: dict[tuple(int, str): pe_rcf.RCF] = {} """Ensemble des RCS recensés""" self.rcss_suivis: dict[int:dict] = {} """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, son RCS : {etudid: {nom_RCS: RCS}}""" - self.rcfs: dict[tuple(int, str) : RCF] = {} + self.rcfs: dict[tuple(int, str) : pe_rcf.RCF] = {} """Ensemble des RCF recensés : {(nom_RCS, fid_terminal): RCF}""" self.rcfs_suivis: dict[int:dict] = {} """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, son RCS : {etudid: {nom_RCS: RCF}}""" - self.rcrcfs: dict[tuple(int, str) : RCRCF] = {} + self.rcrcfs: dict[tuple(int, str) : pe_rcrcf.RCRCF] = {} """Ensemble des RCS recensés : {(nom_RCS, fid_terminal): RCRCF}""" self.rcrcfs_suivis: dict[int:str] = {} """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, son RCRCF : {etudid: {nom_RCS: RCSx}}""" - def cree_rcss(self, etudiants: EtudiantsJuryPE): + def cree_rcss(self, etudiants: pe_etudiant.EtudiantsJuryPE): """Créé tous les RCS, au regard du cursus des étudiants analysés + les mémorise dans les données de l'étudiant @@ -286,17 +49,17 @@ class RCSsJuryPE: etudiants: Les étudiants à prendre en compte dans le Jury PE """ + tous_les_aggregats = rcss_constantes.TOUS_LES_SEMESTRES + rcss_constantes.TOUS_LES_RCS_AVEC_PLUSIEURS_SEM for etudid in etudiants.trajectoires: self.rcss_suivis[etudid] = { aggregat: None - for aggregat in pe_comp.TOUS_LES_SEMESTRES - + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM + for aggregat in tous_les_aggregats } - for nom_rcs in pe_comp.TOUS_LES_SEMESTRES + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM: + for nom_rcs in tous_les_aggregats: # L'aggrégat considéré (par ex: 3S=S1+S2+S3), son nom de son semestre # terminal (par ex: S3) et son numéro (par ex: 3) - noms_semestre_de_aggregat = TYPES_RCS[nom_rcs]["aggregat"] + noms_semestre_de_aggregat = rcss_constantes.TYPES_RCS[nom_rcs]["aggregat"] nom_semestre_terminal = noms_semestre_de_aggregat[-1] for etudid in etudiants.trajectoires: @@ -305,12 +68,12 @@ class RCSsJuryPE: # (par ex: son dernier S3 en date) trajectoire = etudiants.trajectoires[etudid][nom_semestre_terminal] if trajectoire: - formsemestre_final = get_dernier_semestre_en_date(trajectoire) + formsemestre_final = pe_etudiant.get_dernier_semestre_en_date(trajectoire) # Ajout ou récupération du RCS associé rcs_id = (nom_rcs, formsemestre_final.formsemestre_id) if rcs_id not in self.rcss: - self.rcss[rcs_id] = RCF(nom_rcs, formsemestre_final) + self.rcss[rcs_id] = pe_rcf.RCF(nom_rcs, formsemestre_final) rcs = self.rcss[rcs_id] # La liste des semestres de l'étudiant à prendre en compte @@ -328,23 +91,22 @@ class RCSsJuryPE: # Affichage pour debug jeunes = list(enumerate(self.rcss_suivis)) for no_etud, etudid in jeunes[:20]: - pe_affichage.pe_print(f"-> {etudiants.identites[etudid].nomprenom} :") + pe_affichage.pe_print(f"--> {etudiants.identites[etudid].nomprenom} (#{etudid}) :") for nom_rcs, rcs in self.rcss_suivis[etudid].items(): if rcs: pe_affichage.pe_print(f" > RCS {nom_rcs}: {rcs.get_repr()}") - def cree_rcfs(self, etudiants: EtudiantsJuryPE): + def cree_rcfs(self, etudiants: pe_etudiant.EtudiantsJuryPE): """Créé les RCFs en ne conservant dans les RCS que les regroupements de type Sx""" self.rcfs = {} for rcs_id, rcs in self.rcss.items(): - if rcs and rcs.nom in pe_comp.TOUS_LES_SEMESTRES: + if rcs and rcs.nom in rcss_constantes.TOUS_LES_SEMESTRES: self.rcfs[rcs_id] = rcs - print(self.rcfs) for etudid in self.rcss_suivis: for nom_rcs, rcs in self.rcss_suivis[etudid].items(): - if rcs and nom_rcs in pe_comp.TOUS_LES_SEMESTRES: + if rcs and nom_rcs in rcss_constantes.TOUS_LES_SEMESTRES: if etudid not in self.rcfs_suivis: self.rcfs_suivis[etudid] = {} self.rcfs_suivis[etudid][nom_rcs] = rcs @@ -355,11 +117,11 @@ class RCSsJuryPE: pe_affichage.pe_print(f"-> {etudiants.identites[etudid].nomprenom} :") for nom_rcs, rcs in self.rcfs_suivis[etudid].items(): if rcs: - pe_affichage.pe_print(f" > RCF {nom_rcs}: {rcs.get_repr()}") + pe_affichage.pe_print(f" > RCSValid {nom_rcs}: {rcs.get_repr()}") else: - pe_affichage.pe_print(f" > RCF {nom_rcs}: !!! ") + pe_affichage.pe_print(f" > RCSValid {nom_rcs}: ") - def cree_rcrcfs(self, etudiants: EtudiantsJuryPE): + def cree_rcrcfs(self, etudiants: pe_etudiant.EtudiantsJuryPE): """Créé tous les RCRCF, au regard du cursus des étudiants analysés (trajectoires traduisant son parcours dans les différents semestres) + les mémorise dans les données de l'étudiant @@ -390,7 +152,7 @@ class RCSsJuryPE: # Ajout du RCRCF if rcf_id not in self.rcrcfs: - self.rcrcfs[rcf_id] = RCRCF(rcf_id, rcf.formsemestre_final) + self.rcrcfs[rcf_id] = pe_rcrcf.RCRCF(rcf_id, rcf.formsemestre_final) rcrcf = self.rcrcfs[rcf_id] # Ajout des RCFs au RCRCF @@ -410,6 +172,8 @@ class RCSsJuryPE: else: pe_affichage.pe_print(f" > RCRCF {nom_rcs}: !!! ") + + def get_rcs_etudiant( semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str ) -> dict[int, FormSemestre]: @@ -439,7 +203,7 @@ def get_rcs_etudiant( numero_semestres_possibles = [numero_semestre_terminal] elif nom_rcs.endswith("A"): # les années numero_semestres_possibles = [ - int(sem[-1]) for sem in TYPES_RCS[nom_rcs]["aggregat"] + int(sem[-1]) for sem in rcss_constantes.TYPES_RCS[nom_rcs]["aggregat"] ] assert numero_semestre_terminal in numero_semestres_possibles else: # les xS = tous les semestres jusqu'à Sx (eg S1, S2, S3 pour un S3 terminal) @@ -454,3 +218,27 @@ def get_rcs_etudiant( ): semestres_aggreges[fid] = semestre return semestres_aggreges + + +def get_rcf_from_semestres_aggreges( + rcfs: dict[(str, int):pe_rcf.RCF], semestres_a_aggreges: list[FormSemestre] + ) -> (str, int): + """Partant d'un dictionnaire de RCFs (de la forme + ``{ (nom_rcs, fid): RCF }, et connaissant une liste + de (form)semestres à aggréger, renvoie l'identifiant + (nom_rcs, fid) du RCFs qui lui correspond (c'est à dire celui dont + les semestres_aggregés par le RCF sont les même que les + semestres_a_aggreger. + + Returns: + rcf_id: L'identifiant du RCF trouvé + """ + fids_semestres_a_aggreger = set( + [frms.formsemestre_id for frms in semestres_a_aggreges] + ) + for rcf_id, rcf in rcfs.items(): + fids_rcf = set(rcf.semestres_aggreges) + if fids_rcf == fids_semestres_a_aggreger: + return rcf_id + return None + diff --git a/app/pe/pe_rcstag.py b/app/pe/pe_rcstag.py index 68f3b8f66..70d114910 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/pe_rcstag.py @@ -38,18 +38,18 @@ Created on Fri Sep 9 09:15:05 2016 from app.comp.res_sem import load_formsemestre_results from app.pe import pe_affichage -from app.pe.pe_ressemtag import ResSemBUTTag import pandas as pd import numpy as np -from app.pe.pe_rcs import RCS, RCRCF -from app.pe.pe_sxtag import SxTag +import app.pe.rcss.pe_rcs as pe_rcs +import app.pe.rcss.pe_rcrcf as pe_rcrcf +import app.pe.pe_sxtag as pe_sxtag from app.pe.pe_tabletags import TableTag from app.pe.pe_moytag import MoyennesTag class RCSTag(TableTag): - def __init__(self, rcrcf: RCS, sxstags: dict[(str, int): SxTag]): + def __init__(self, rcrcf: pe_rcs.RCS, sxstags: dict[(str, int): pe_sxtag.SxTag]): """Calcule les moyennes par tag (orientées compétences) d'un regroupement de SxTag (RCRCF), pour extraire les classements par tag pour un @@ -65,7 +65,7 @@ class RCSTag(TableTag): self.rcs_id: tuple(str, int) = rcrcf.rcs_id """Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)""" - self.rcrcf: RCRCF = rcrcf + self.rcrcf: pe_rcrcf.RCRCF = rcrcf """RCRCF associé au RCS taggué""" self.nom = self.get_repr() @@ -74,6 +74,8 @@ class RCSTag(TableTag): self.formsemestre_terminal = rcrcf.formsemestre_final """Le formsemestre terminal""" + pe_affichage.pe_print(f"-> {self.get_repr(verbose=True)}") + # Les résultats du formsemestre terminal nt = load_formsemestre_results(self.formsemestre_terminal) @@ -95,10 +97,12 @@ class RCSTag(TableTag): # Les compétences (extraites de tous les Sxtags) self.competences_sorted = self.do_complist() """Compétences extraites de tous les SxTag aggrégés""" + pe_affichage.pe_print(f"* Compétences : {', '.join(self.competences_sorted)}") # Les tags self.tags_sorted = self.do_taglist() """Tags extraits de tous les SxTag aggrégés""" + pe_affichage.pe_print(f"* Tags : {', '.join(self.tags_sorted)}") # Les moyennes self.moyennes_tags: dict[str, MoyennesTag] = {} @@ -121,10 +125,13 @@ class RCSTag(TableTag): """Egalité de 2 RCS taggués sur la base de leur identifiant""" return self.rcs_id == other.sxtag_id - def get_repr(self, verbose=False) -> str: + def get_repr(self, verbose=True) -> str: """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle est basée)""" - return self.rcrcf.get_repr(verbose=verbose) + if verbose: + return self.rcrcf.get_repr(verbose=verbose) + else: + return f"{self.__class__.__name__} ({self.rcs_id})" def compute_notes_comps_cube(self, tag): """Pour un tag donné, construit : @@ -154,10 +161,11 @@ class RCSTag(TableTag): # Charge les notes et les coeffs du semestre tag notes = moys_tag.matrice_notes.copy() # avec une copie - coeffs = moys_tag.matrice_coeffs.copy() # les coeffs + coeffs = moys_tag.matrice_coeffs_moy_gen.copy() # les coeffs # Traduction des UE en compétences - association_ues_comp = moys_tag.competences + association_ues_comp = self.mapping_ue_competences() + print(association_ues_comp) ues_columns_df = notes.columns comp_associes_aux_ues = [association_ues_comp[ue] for ue in ues_columns_df] notes.columns = comp_associes_aux_ues @@ -204,16 +212,20 @@ class RCSTag(TableTag): tags = [] for frmsem_id in self.sxstags: tags.extend(self.sxstags[frmsem_id].tags_sorted) - pe_affichage.pe_print(f"* Tags : {', '.join(tags)}") return sorted(set(tags)) + def mapping_ue_competences(self): + """Dictionnaire {ue: competences} extrait des SxTags""" + dict_competences = {} + for sxtag_id, sxtag in self.sxstags.items(): + comp = sxtag.competences + dict_competences |= comp + return dict_competences + def do_complist(self): """Synthétise les compétences à partir des Sxtags aggrégés""" - competences = [] - for sxtag_id, sxtag in self.sxstags.items(): - comp = list(sxtag.moyennes_tags["but"].matrice_notes.columns) - competences.extend(comp) - return sorted(set(competences)) + dict_comptences = self.mapping_ue_competences() + return sorted(set(dict_comptences.values())) def compute_notes_competences( @@ -248,7 +260,7 @@ def compute_notes_competences( # Enlève les NaN du cube de notes pour les entrées manquantes set_cube_no_nan = np.nan_to_num(set_cube, nan=0.0) - coeffs_cube_no_nan = no.nan_to_num(coeff_cube, nan=0.0) + coeffs_cube_no_nan = np.nan_to_num(coeff_cube, nan=0.0) # Les moyennes par tag with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) diff --git a/app/pe/pe_ressemtag.py b/app/pe/pe_ressemtag.py index 0c4bc748d..8b7348550 100644 --- a/app/pe/pe_ressemtag.py +++ b/app/pe/pe_ressemtag.py @@ -48,7 +48,7 @@ import app.pe.pe_etudiant as pe_etudiant import app.pe.pe_tabletags as pe_tabletags from app.pe.pe_moytag import MoyennesTag from app.scodoc import sco_tag_module -from app.scodoc.codes_cursus import UE_SPORT +from app.scodoc import codes_cursus as sco_codes class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): @@ -66,44 +66,51 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): ResultatsSemestreBUT.__init__(self, formsemestre) pe_tabletags.TableTag.__init__(self) - # Le semestre - # self.formsemestre_id = self.formsemestre.formsemestre_id - # self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id) - # Le nom du res_semestre taggué self.nom = self.get_repr(verbose=True) 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 (etuds, états civils & etudis) ajouté self.add_etuds(self.etuds) + self.etudids_sorted = sorted(self.etudids) # Les UEs (et les dispenses d'UE) - ues_hors_sport = [ue for ue in self.ues if ue.type != UE_SPORT] + # self.ues + ues_standards = [ue for ue in self.ues if ue.type == sco_codes.UE_STANDARD] + + # Les UEs en fonction des parcours self.ues_inscr_parcours_df = self.load_ues_inscr_parcours() - # Les compétences associées aux UEs + # Les compétences associées aux UEs (définies par les acronymes) self.competences = {} + """L'association acronyme d'UEs -> compétence""" for ue in self.ues: - if ue.type != UE_SPORT: - assert ue.niveau_competence, ScoValueError("Des UEs ne sont pas rattachées à des compétences") + if ue.type == sco_codes.UE_STANDARD: + assert ue.niveau_competence, ScoValueError( + "Des UEs ne sont pas rattachées à des compétences" + ) nom = ue.niveau_competence.competence.titre - self.competences[ue.ue_id] = nom + self.competences[ue.acronyme] = nom + + # Les acronymes des UEs + self.ues_to_acronymes = {ue.id: ue.acronyme for ue in ues_standards} + self.acronymes_sorted = sorted(self.ues_to_acronymes.values()) + """Les acronymes de UE triés par ordre alphabétique""" # 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]] - # Les coefficients - matrice_coeffs = self.ues_inscr_parcours_df * [ - ue.ects - for ue in ues_hors_sport # if ue.type != UE_SPORT <= déjà supprimé + # Les coefficients pour le calcul de la moyenne générale + self.matrice_coeffs_moy_gen = self.ues_inscr_parcours_df * [ + ue.ects for ue in ues_standards # if ue.type != UE_SPORT <= déjà supprimé ] + # Les capitalisations (mask etuids x acronyme_ue valant True si capitalisée, False sinon) + self.capitalisations = self._get_capitalisations(ues_standards) + + # Calcul des moyennes & les classements de chaque étudiant à chaque tag self.moyennes_tags = {} @@ -111,46 +118,56 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): # 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, - moy_ues_tag, - matrice_coeffs + tag, moy_ues_tag, self.matrice_coeffs_moy_gen ) - # 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) + # Ajoute les moyennes par UEs + la moyenne générale (but) df_ues = pd.DataFrame( - {ue.id: self.etud_moy_ue[ue.id] for ue in ues_hors_sport}, + {ue.id: self.etud_moy_ue[ue.id] for ue in ues_standards}, index=self.etudids, ) + # Transforme les UEs en acronyme + colonnes = df_ues.columns + acronymes = [self.ues_to_acronymes[col] for col in colonnes] + df_ues.columns = acronymes + self.moyennes_tags["but"] = MoyennesTag( - "but", df_ues, matrice_coeffs # , moy_gen_but + "but", df_ues, self.matrice_coeffs_moy_gen # , 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, verbose=False): """Nom affiché pour le semestre taggué""" if verbose: - return f"{self.formsemestre} ({self.formsemestre.formsemestre_id})" + return f"{self.formsemestre} (#{self.formsemestre.formsemestre_id})" else: return pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True) + def _get_capitalisations(self, ues_hors_sport) -> pd.DataFrame: + """Renvoie un dataFrame résumant les UEs capitalisables par les + étudiants, d'après les décisions de jury + + Args: + ues_hors_sport: Liste des UEs autres que le sport + """ + capitalisations = pd.DataFrame(False, index=self.etudids_sorted, columns=self.acronymes_sorted) + self.get_formsemestre_validations() # charge les validations + res_jury = self.validations + if res_jury: + for etud in self.etuds: + etudid = etud.etudid + decisions = res_jury.decisions_jury_ues.get(etudid, {}) + for ue in ues_hors_sport: + if ue.id in decisions and decisions[ue.id]["code"] == sco_codes.ADM: + capitalisations.loc[etudid, ue.acronyme] = True + # pe_affichage.pe_print( + # f" ⚠ Capitalisation de {ue.acronyme} pour {etud.etat_civil}" + # ) + return capitalisations + 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. @@ -164,7 +181,7 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): # Adaptation du mask de calcul des moyennes au tag visé modimpls_mask = [ - modimpl.module.ue.type != UE_SPORT + modimpl.module.ue.type == sco_codes.UE_STANDARD for modimpl in self.formsemestre.modimpls_sorted ] @@ -216,9 +233,6 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): self.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 @@ -229,8 +243,11 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): dict_tags["auto"] = {"but": {}} noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp + + # Affichage pe_affichage.pe_print( - f"* Tags automatiquement ajoutés : {', '.join(noms_tags_auto)}" + f"* Tags du programme de formation : {', '.join(noms_tags_perso)} " + + f"Tags automatiques : {', '.join(noms_tags_auto)}" ) return dict_tags @@ -312,4 +329,3 @@ def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre): } return synthese_tags - diff --git a/app/pe/pe_sxtag.py b/app/pe/pe_sxtag.py index 00b41c07b..132824df8 100644 --- a/app/pe/pe_sxtag.py +++ b/app/pe/pe_sxtag.py @@ -36,19 +36,23 @@ Created on Fri Sep 9 09:15:05 2016 @author: barasc """ -from app.comp.res_sem import load_formsemestre_results -from app.models import UniteEns, FormSemestre -from app.pe import pe_affichage -from app.pe.pe_ressemtag import ResSemBUTTag +from app.pe import pe_affichage, pe_comp +import app.pe.pe_ressemtag as pe_ressemtag import pandas as pd import numpy as np from app.pe.pe_tabletags import TableTag from app.pe.pe_moytag import MoyennesTag +import app.pe.rcss.pe_rcf as pe_rcf class SxTag(TableTag): - def __init__(self, sxtag_id: (int, int), ressembuttags: dict[int, ResSemBUTTag]): + def __init__( + self, + sxtag_id: (str, int), + rcf: pe_rcf.RCF, + ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag], + ): """Calcule les moyennes/classements par tag d'un semestre de type 'Sx' (par ex. 'S1', 'S2', ...) avec une orientation par UE : @@ -76,10 +80,14 @@ class SxTag(TableTag): assert sxtag_id and len(sxtag_id) == 2 and sxtag_id[1] in ressembuttags - self.sxtag_id: (int, int) = sxtag_id - """Identifiant du SxTag de la forme (semestre_id, fid_semestre_final)""" + self.sxtag_id: (str, int) = sxtag_id + """Identifiant du SxTag de la forme (nom_Sx, fid_semestre_final)""" - self.ressembuttags = ressembuttags + self.rcf = rcf + """Le RCF sur lequel il s'appuie""" + + # Les resultats des semestres taggués à prendre en compte dans le RCF + self.ressembuttags = {fid: ressembuttags[fid] for fid in rcf.semestres_aggreges} """Les ResSemBUTTags à regrouper dans le SxTag""" # Les données du semestre final @@ -87,9 +95,13 @@ class SxTag(TableTag): self.ressembuttag_final = ressembuttags[self.fid_final] """Le ResSemBUTTag final""" - # Les étudiants (etuds, états civils & etudis) self.etuds = ressembuttags[self.fid_final].etuds + """Les étudiants du ReSemBUTTag final""" + + # Ajout les etudids et les états civils self.add_etuds(self.etuds) + self.etudids_sorted = sorted(self.etudids) + """Les etudids triés""" # Affichage pe_affichage.pe_print(f"--> {self.get_repr()}") @@ -103,50 +115,87 @@ class SxTag(TableTag): moy_sem_final = self.ressembuttag_final.moyennes_tags["but"] self.ues = list(moy_sem_final.matrice_notes.columns) + # L'association UE-compétences extraites du dernier semestre + self.competences = self.ressembuttag_final.competences + # Les acronymes des UE self.acronymes_ues_sorted = sorted(self.ues) # Les inscriptions des étudiants aux UEs # => ne conserve que les UEs du semestre final (pour les redoublants) - self.matrice_coeffs = self.ressembuttag_final.moyennes_tags[ - "but" - ].matrice_coeffs - self.ues_inscr_parcours = ~np.isnan(self.matrice_coeffs.to_numpy()) + self.ues_inscr_parcours_df = self.ressembuttag_final.ues_inscr_parcours_df + self.ues_inscr_parcours_df.sort_index() + # Les coeffs pour la moyenne générale + self.matrice_coeffs_moy_gen = self.ressembuttag_final.moyennes_tags[ + "but" + ].matrice_coeffs_moy_gen + self.matrice_coeffs_moy_gen.sort_index() # Trie les coeff par etudids # Les moyennes par tag self.moyennes_tags: dict[str, pd.DataFrame] = {} """Les notes aux UEs dans différents tags""" - # Masque des inscriptions - inscr_mask = self.ues_inscr_parcours + # Masque des inscriptions et des capitalisations + self.masque_df, masque_cube = compute_masques_ues_cube( + self.etudids_sorted, + self.acronymes_ues_sorted, + self.ressembuttags, + self.fid_final, + ) + # Affichage pour debug + for etud in self.etuds: + cap = [] + for frmsem_id in self.ressembuttags: + if frmsem_id != self.fid_final: + for accr in self.acronymes_ues_sorted: + if self.masque_df[frmsem_id].loc[etud.etudid, accr] > 0.0: + cap += [accr] + if cap: + pe_affichage.pe_print( + f" ⚠ Capitalisation de {etud.etat_civil} : {', '.join(cap)}" + ) + for tag in self.tags_sorted: # Cube de note etudids x UEs - notes_cube = self.compute_notes_ues_cube(tag, self.acronymes_ues_sorted) + notes_df, notes_cube = compute_notes_ues_cube( + tag, self.etudids_sorted, self.acronymes_ues_sorted, self.ressembuttags + ) - # Calcule des moyennes sous forme d'un dataframe""" - matrice_moys_ues = compute_notes_ues( + + + + + # self.ues_inscr_parcours = ~np.isnan(self.matrice_coeffs.to_numpy()) + # inscr_mask = self.ues_inscr_parcours + + # Calcule des moyennes sous forme d'un dataframe + inscr_mask = ~np.isnan(self.ues_inscr_parcours_df.to_numpy()) + matrice_moys_ues: pd.DataFrame = compute_notes_ues( notes_cube, - self.etudids, + masque_cube, + self.etudids_sorted, self.acronymes_ues_sorted, inscr_mask, ) # Les profils d'ects (pour debug) profils_ects = [] - for i in self.matrice_coeffs.index: - val = tuple(self.matrice_coeffs.loc[i].fillna("x")) + for i in self.matrice_coeffs_moy_gen.index: + val = tuple(self.matrice_coeffs_moy_gen.loc[i].fillna("x")) if tuple(val) not in profils_ects: profils_ects.append(tuple(val)) # Les moyennes - self.moyennes_tags[tag] = MoyennesTag(tag, - matrice_moys_ues, - self.matrice_coeffs) + self.moyennes_tags[tag] = MoyennesTag( + tag, matrice_moys_ues, self.matrice_coeffs_moy_gen + ) - pe_affichage.pe_print(f"> MoyTag pour {tag} avec") - pe_affichage.pe_print(f" - ues={self.acronymes_ues_sorted}") - pe_affichage.pe_print(f" - ects={profils_ects}") + pe_affichage.pe_print( + f"> MoyTag 🏷{tag} avec " + + f"ues={self.acronymes_ues_sorted} " + + f"ects={profils_ects}" + ) def __eq__(self, other): """Egalité de 2 SxTag sur la base de leur identifiant""" @@ -155,60 +204,123 @@ class SxTag(TableTag): def get_repr(self, verbose=False) -> str: """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle est basée)""" - affichage = [str(fid) for fid in self.ressembuttags] - return f"{self.sxtag_id[0]}Tag ({'+'.join(affichage)})" + if verbose: + return f"{self.sxtag_id[0]}Tag basé sur {self.rcf.get_repr()}" + else: + # affichage = [str(fid) for fid in self.ressembuttags] + return f"{self.sxtag_id[0]}Tag (#{self.fid_final})" - 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.ressembuttags.keys()) - dfs = {} +def compute_notes_ues_cube( + tag, etudids_sorted, acronymes_ues_sorted, ressembuttags +) -> (pd.DataFrame, np.array): + """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. + (Renvoie également le dataframe associé pour debug). - for frmsem_id in semestres_id: - # Partant d'un dataframe vierge - df = pd.DataFrame(np.nan, index=etudids, columns=acronymes_ues_sorted) + Args: + etudids_sorted: La liste des etudids triés par ordre croissant (dim 0) + acronymes_ues_sorted: La liste des acronymes de UEs triés par acronyme croissant (dim 1) + ressembuttags: Le dictionnaire des résultats de semestres BUT (tous tags confondus) + """ + # Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2) + # etudids_sorted = etudids_sorted + # acronymes_ues = sorted([ue.acronyme for ue in selMf.ues.values()]) + semestres_id = list(ressembuttags.keys()) - # Charge les notes du semestre tag - sem_tag = self.ressembuttags[frmsem_id] - moys_tag = sem_tag.moyennes_tags[tag] - notes = moys_tag.matrice_notes # dataframe etudids x ues - acronymes_ues_sem = list( - notes.columns - ) # les acronymes des UEs du semestre tag + dfs = {} - # UEs communes à celles du SemTag (celles du dernier semestre du RCS) - ues_communes = list(set(acronymes_ues_sorted) & set(acronymes_ues_sem)) + for frmsem_id in semestres_id: + # Partant d'un dataframe vierge + df = pd.DataFrame(np.nan, index=etudids_sorted, columns=acronymes_ues_sorted) - # Etudiants communs - etudids_communs = df.index.intersection(notes.index) + # Charge les notes du semestre tag + sem_tag = ressembuttags[frmsem_id] + moys_tag = sem_tag.moyennes_tags[tag] + notes = moys_tag.matrice_notes # dataframe etudids x ues - # Recopie - df.loc[etudids_communs, ues_communes] = notes.loc[ - etudids_communs, ues_communes + # les étudiants et les acronymes communs + etudids_communs, acronymes_communs = pe_comp.find_index_and_columns_communs( + df, notes + ) + + # Recopie + df.loc[etudids_communs, acronymes_communs] = notes.loc[ + etudids_communs, acronymes_communs + ] + + # Supprime tout ce qui n'est pas numérique + for col in df.columns: + df[col] = pd.to_numeric(df[col], errors="coerce") + + # Stocke le df + dfs[frmsem_id] = df + + """Réunit les notes sous forme d'un cube etudids x ues x semestres""" + 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 dfs, etudids_x_ues_x_semestres + + +def compute_masques_ues_cube( + etudids_sorted: list[int], + acronymes_ues_sorted: list[str], + ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag], + formsemestre_id_final: int, +) -> (pd.DataFrame, np.array): + """Construit le cube traduisant le masque des UEs à prendre en compte dans le calcul + des moyennes, en utilisant le df capitalisations de chaque ResSemBUTTag + + Ce masque contient : 1 si la note doit être prise en compte ; 0 sinon + + Args: + etudids_sorted: La liste des etudids triés par ordre croissant (dim 0) + acronymes_ues_sorted: La liste des acronymes de UEs triés par acronyme croissant (dim 1) + # ues_inscr_parcours_df: Le dataFrame des inscriptions au UE en fonction du parcours + ressembuttags: Le dictionnaire des résultats de semestres BUT (tous tags confondus) + formsemestre_id_final: L'identifiant du formsemestre_id_final (dont il faut forcément prendre en compte les coeffs) + """ + # Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2) + # etudids_sorted = etudids_sorted + # acronymes_ues = sorted([ue.acronyme for ue in selMf.ues.values()]) + semestres_id = list(ressembuttags.keys()) + + dfs = {} + + for frmsem_id in semestres_id: + # Partant d'un dataframe contenant des 1.0 + if frmsem_id == formsemestre_id_final: + df = pd.DataFrame(1.0, index=etudids_sorted, columns=acronymes_ues_sorted) + else: # semestres redoublés + df = pd.DataFrame(0.0, index=etudids_sorted, columns=acronymes_ues_sorted) + + # Traitement des capitalisations + capitalisations = ressembuttags[frmsem_id].capitalisations + capitalisations = capitalisations.replace(True, 1.0).replace(False, 0.0) + + # Met à 0 les coeffs des UEs non capitalisées : 1.0*False => 0.0 + etudids_communs, acronymes_communs = pe_comp.find_index_and_columns_communs( + df, capitalisations + ) + + df.loc[etudids_communs, acronymes_communs] = capitalisations.loc[ + etudids_communs, acronymes_communs ] - # Supprime tout ce qui n'est pas numérique - for col in df.columns: - df[col] = pd.to_numeric(df[col], errors="coerce") + # Stocke le df + dfs[frmsem_id] = df - # Stocke le df - dfs[frmsem_id] = df - - """Réunit les notes sous forme d'un cube etudids x ues x semestres""" - 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 + """Réunit les notes sous forme d'un cube etudids x ues x semestres""" + 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 dfs, etudids_x_ues_x_semestres def compute_notes_ues( set_cube: np.array, - etudids: list, - acronymes_ues: list, + masque_cube: np.array, + etudids_sorted: list, + acronymes_ues_sorted: list, inscr_mask: np.array, ): """Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE @@ -217,8 +329,10 @@ def compute_notes_ues( Args: 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) + masque_cube: masque indiquant si la note doit être prise en compte ndarray + (semestre_ids x etudids x UEs), des 1.0 ou des 0.0 + etudids_sorted: liste des étudiants (dim. 0 du cube) trié par etudid + acronymes_ues_sorted: liste des acronymes des ues (dim. 1 du cube) trié par acronyme inscr_mask: masque etudids x UE traduisant les inscriptions des étudiants aux UE (du semestre terminal) Returns: @@ -227,18 +341,21 @@ def compute_notes_ues( """ nb_etuds, nb_ues, nb_semestres = set_cube.shape nb_etuds_mask, nb_ues_mask = inscr_mask.shape - assert nb_etuds == len(etudids) - assert nb_ues == len(acronymes_ues) + assert nb_etuds == len(etudids_sorted) + assert nb_ues == len(acronymes_ues_sorted) assert nb_etuds == nb_etuds_mask assert nb_ues == nb_ues_mask - # Quelles entrées du cube contiennent des notes ? - mask = ~np.isnan(set_cube) - - # Entrées à garder dans le cube en fonction du mask d'inscription + # Entrées à garder dans le cube en fonction du masque d'inscription aux UEs du parcours inscr_mask_3D = np.stack([inscr_mask] * nb_semestres, axis=-1) set_cube = set_cube * inscr_mask_3D + # Entrées à garder en fonction des UEs capitalisées ou non + set_cube = set_cube * masque_cube + + # Quelles entrées du cube contiennent des notes ? + mask = ~np.isnan(set_cube) + # Enlève les NaN du cube pour les entrées manquantes : NaN -> -1.0 set_cube_no_nan = np.nan_to_num(set_cube, nan=-1.0) @@ -252,8 +369,8 @@ def compute_notes_ues( # Le dataFrame etud_moy_tag_df = pd.DataFrame( etud_moy, - index=etudids, # les etudids - columns=acronymes_ues, # les tags + index=etudids_sorted, # les etudids + columns=acronymes_ues_sorted, # les tags ) etud_moy_tag_df.fillna(np.nan) diff --git a/app/pe/pe_tabletags.py b/app/pe/pe_tabletags.py index ea789ef57..a14f593b4 100644 --- a/app/pe/pe_tabletags.py +++ b/app/pe/pe_tabletags.py @@ -52,9 +52,7 @@ class TableTag(object): # Les étudiants # self.etuds: list[Identite] = None # A venir """Les étudiants""" - # self.etats_civils: dict[int, Identite] = None - """Les états civils""" - # self.etudids: list[int] = None + # self.etudids: list[int] = {} """Les etudids""" def add_etuds(self, etuds: list[Identite]): @@ -64,8 +62,7 @@ class TableTag(object): etuds: la liste des identités de l'étudiant """ # self.etuds = etuds - self.etats_civils = {etud.etudid: etud.etat_civil for etud in self.etuds} - self.etudids = list(self.etats_civils.keys()) + self.etudids = list({etud.etudid for etud in etuds}) def get_all_tags(self): """Liste des tags de la table, triée par ordre alphabétique, @@ -88,8 +85,8 @@ class TableTag(object): Le dataframe des notes et des classements """ - etudiants = self.etats_civils - df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"]) + etudiants = {etud.etudid: [etud.nom, etud.prenom] for etud in self.etuds} + df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom", "prenom"]) tags_tries = self.get_all_tags() for tag in tags_tries: @@ -104,7 +101,7 @@ class TableTag(object): df = df.join(moy_gen.synthese["notes"].rename(f"Moy {tag} (gen)")) df = df.join(moy_gen.synthese["classements"].rename(f"Class {tag} (gen)")) - df.sort_values(by=['nom']) + df.sort_values(by=["nom", "prenom"]) return df diff --git a/app/pe/rcss/__init__.py b/app/pe/rcss/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/pe/rcss/pe_rcf.py b/app/pe/rcss/pe_rcf.py new file mode 100644 index 000000000..494a35059 --- /dev/null +++ b/app/pe/rcss/pe_rcf.py @@ -0,0 +1,57 @@ + +from app.models import FormSemestre +import app.pe.rcss.pe_rcs as pe_rcs + + +class RCF(pe_rcs.RCS): + """Modélise un ensemble de (form)semestres d'étudiants + associé à un type de regroupement cohérent de semestres + donné (par ex: 'S2', '3S', '2A'). + + Si le RCF est un semestre de type Si, stocke 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 le RCF de type iS ou iA (par ex, 3A=S1+S2+S3), identifie + les semestres que les étudiants ont suivis pour les amener jusqu'au semestre + terminal du RCS (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_rcs: Un nom du RCS (par ex: '5S') + semestre_final: Le formsemestre final du RCS + """ + + def __init__(self, nom_rcs: str, semestre_final: FormSemestre): + pe_rcs.RCS.__init__(self, nom_rcs, semestre_final) + + self.semestres_aggreges: dict[int:FormSemestre] = {} + """Formsemestres regroupés dans le RCS""" + + def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]): + """Ajout de semestres aux semestres à regrouper + + Args: + semestres: Dictionnaire ``{fid: Formsemestre)`` + """ + self.semestres_aggreges = self.semestres_aggreges | semestres + + def get_repr(self, verbose=True) -> str: + """Représentation textuelle d'un RCS + basé sur ses semestres aggrégés""" + title = f"""{self.__class__.__name__} {pe_rcs.RCS.__str__(self)}""" + if verbose: + noms = [] + for fid in self.semestres_aggreges: + semestre = self.semestres_aggreges[fid] + noms.append(f"S{semestre.semestre_id}(#{fid})") + noms = sorted(noms) + if noms: + title += " <" + "+".join(noms) + ">" + else: + title += " " + return title diff --git a/app/pe/rcss/pe_rcrcf.py b/app/pe/rcss/pe_rcrcf.py new file mode 100644 index 000000000..f44d9fb31 --- /dev/null +++ b/app/pe/rcss/pe_rcrcf.py @@ -0,0 +1,69 @@ +############################################################################## +# Module "Avis de poursuite d'étude" +# conçu et développé par Cléo Baras (IUT de Grenoble) +############################################################################## + +""" +Created on 01-2024 + +@author: barasc +""" + +import app.pe.pe_comp as pe_comp +import app.pe.rcss.pe_rcf +import app.pe.rcss.rcss_constantes + +from app.models import FormSemestre +from app.pe import pe_sxtag, pe_affichage +from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date +import app.pe.rcss.pe_rcs as pe_rcs +import app.pe.rcss.pe_rcf as pe_rcf + + + +class RCRCF: + """Modélise les RCF d'étudiants suivis par un étudiant dans + le cadre d'un RCS donné (par ex: 3S=S1+S2+S3). + + Pour rappel : un RCF (par ex. S1) combine les semestres 1 qu'a suivi + l'étudiant pour valider son S1 (1 si étudiant standard, 2 si redoublant). + + Le RCRCF 3S est donc le regroupement du RCF S1 + RCF S2 + RCF S3. + + Il est identifié par le formsemestre de S3 marquant la fin du regroupement. + + + Args: + nom_rcs: Un nom du RCS (par ex: '5S') + semestre_final: Le semestre final du RCS + """ + + def __init__(self, nom_rcs: str, semestre_final: FormSemestre): + pe_rcs.RCS.__init__(self, nom_rcs, semestre_final) + + self.rcfs_aggreges: dict[(str, int) : pe_sxtag.SxTag] = {} + """Les RCFs à aggréger""" + + def add_rcfs_a_aggreger(self, rcfs: dict[(str, int): app.pe.rcss.pe_rcf.RCF]): + """Ajout des RCFs aux RCFS à regrouper + + Args: + rcfs: Dictionnaire ``{(str,fid): RCF}`` à ajouter + """ + self.rcfs_aggreges = self.rcfs_aggreges | rcfs + + def get_repr(self, verbose=True) -> str: + """Représentation textuelle d'un RCSF + basé sur ses RCF aggrégés""" + title = f"""{self.__class__.__name__}{pe_rcs.RCS.__str__(self)}""" + if verbose: + noms = [] + for rcf_id, rcf in self.rcfs_aggreges.items(): + noms.append(rcf.get_repr(verbose=False)) + if noms: + title += " <<" + "+".join(noms) + ">>" + else: + title += " <>" + return title + + diff --git a/app/pe/rcss/pe_rcs.py b/app/pe/rcss/pe_rcs.py new file mode 100644 index 000000000..72a3669be --- /dev/null +++ b/app/pe/rcss/pe_rcs.py @@ -0,0 +1,62 @@ +############################################################################## +# Module "Avis de poursuite d'étude" +# conçu et développé par Cléo Baras (IUT de Grenoble) +############################################################################## + +""" +Created on 01-2024 + +@author: barasc +""" + +from app.models import FormSemestre +import app.pe.rcss.rcss_constantes as rcss_constantes + + + +def get_descr_rcs(nom_rcs: str) -> str: + """Renvoie la description pour les tableurs de synthèse + Excel d'un nom de RCS""" + return rcss_constantes.TYPES_RCS[nom_rcs]["descr"] + + +class RCS: + """Modélise un regroupement cohérent de semestres (formsemestre ou de Sx)""" + + def __init__(self, nom_rcs: str, semestre_final: FormSemestre): + self.nom: str = nom_rcs + """Nom du RCS""" + + self.formsemestre_final: FormSemestre = semestre_final + """FormSemestre terminal du RCS""" + + self.rang_final = self.formsemestre_final.semestre_id + """Le rang du formsemestre final""" + + self.rcs_id: (str, int) = (nom_rcs, semestre_final.formsemestre_id) + """Identifiant du RCS sous forme (nom_rcs, id du semestre_terminal)""" + + def get_formsemestre_id_final(self) -> int: + """Renvoie l'identifiant du formsemestre final du RCS + + Returns: + L'id du formsemestre final (marquant la fin) du RCS + """ + return self.formsemestre_final.formsemestre_id + + def __str__(self): + """Représentation textuelle d'un RCS""" + return f"{self.nom}[#{self.formsemestre_final.formsemestre_id}✟{self.formsemestre_final.date_fin.year}]" + + def get_repr(self, verbose=True): + """Représentation textuelle d'un RCS""" + return self.__str__() + + def __eq__(self, other): + """Egalité de RCS""" + return ( + self.nom == other.nom + and self.formsemestre_final == other.formsemestre_final + ) + + diff --git a/app/pe/rcss/rcss_constantes.py b/app/pe/rcss/rcss_constantes.py new file mode 100644 index 000000000..ee79de275 --- /dev/null +++ b/app/pe/rcss/rcss_constantes.py @@ -0,0 +1,64 @@ + +TYPES_RCS = { + "S1": { + "aggregat": ["S1"], + "descr": "Semestre 1 (S1)", + }, + "S2": { + "aggregat": ["S2"], + "descr": "Semestre 2 (S2)", + }, + "1A": { + "aggregat": ["S1", "S2"], + "descr": "BUT1 (S1+S2)", + }, + "S3": { + "aggregat": ["S3"], + "descr": "Semestre 3 (S3)", + }, + "S4": { + "aggregat": ["S4"], + "descr": "Semestre 4 (S4)", + }, + "2A": { + "aggregat": ["S3", "S4"], + "descr": "BUT2 (S3+S4)", + }, + "3S": { + "aggregat": ["S1", "S2", "S3"], + "descr": "Moyenne du semestre 1 au semestre 3 (S1+S2+S3)", + }, + "4S": { + "aggregat": ["S1", "S2", "S3", "S4"], + "descr": "Moyenne du semestre 1 au semestre 4 (S1+S2+S3+S4)", + }, + "S5": { + "aggregat": ["S5"], + "descr": "Semestre 5 (S5)", + }, + "S6": { + "aggregat": ["S6"], + "descr": "Semestre 6 (S6)", + }, + "3A": { + "aggregat": ["S5", "S6"], + "descr": "3ème année (S5+S6)", + }, + "5S": { + "aggregat": ["S1", "S2", "S3", "S4", "S5"], + "descr": "Moyenne du semestre 1 au semestre 5 (S1+S2+S3+S4+S5)", + }, + "6S": { + "aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"], + "descr": "Moyenne globale (S1+S2+S3+S4+S5+S6)", + }, +} +"""Dictionnaire détaillant les différents regroupements cohérents +de semestres (RCS), en leur attribuant un nom et en détaillant +le nom des semestres qu'ils regroupent et l'affichage qui en sera fait +dans les tableurs de synthèse. +""" + +TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS if not cle.startswith("S")] +TOUS_LES_RCS = list(TYPES_RCS.keys()) +TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")] \ No newline at end of file