From 5ea79c03a33148b0dcaccc980589f052d4a45a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 27 Feb 2024 14:39:14 +0100 Subject: [PATCH] Ajoute les moyennes de ressources/saes --- app/pe/moys/pe_interclasstag.py | 87 ++-- app/pe/moys/pe_moytag.py | 133 ++++-- app/pe/moys/pe_rcstag.py | 326 +++++++-------- app/pe/moys/pe_ressemtag.py | 159 +++---- app/pe/moys/pe_sxtag.py | 389 ++++++++---------- app/pe/moys/pe_tabletags.py | 21 +- app/pe/pe_affichage.py | 110 ++++- app/pe/pe_etudiant.py | 136 +++--- app/pe/pe_jury.py | 177 +++++--- app/pe/pe_rcss_jury.py | 100 ++--- .../pe/pe_view_resultats_etudiant.j2 | 29 +- 11 files changed, 940 insertions(+), 727 deletions(-) diff --git a/app/pe/moys/pe_interclasstag.py b/app/pe/moys/pe_interclasstag.py index 3e026b5b0..870435f5c 100644 --- a/app/pe/moys/pe_interclasstag.py +++ b/app/pe/moys/pe_interclasstag.py @@ -41,6 +41,7 @@ from app.pe import pe_affichage from app.pe.moys import pe_tabletags, pe_moy, pe_moytag, pe_sxtag from app.pe.rcss import pe_rcs import app.pe.pe_comp as pe_comp +from app.scodoc.sco_utils import ModuleType class InterClassTag(pe_tabletags.TableTag): @@ -119,7 +120,7 @@ class InterClassTag(pe_tabletags.TableTag): # Les données sur les tags self.tags_sorted = self._do_taglist() """Liste des tags (triés par ordre alphabétique)""" - aff = pe_affichage.aff_tag(self.tags_sorted) + aff = pe_affichage.repr_tags(self.tags_sorted) pe_affichage.pe_print(f"--> Tags : {aff}") # Les données sur les UEs (si SxTag) ou compétences (si RCSTag) @@ -134,8 +135,8 @@ class InterClassTag(pe_tabletags.TableTag): f"--> Compétences : {pe_affichage.aff_competences(self.champs_sorted)}" ) - # Construit la matrice de notes - etudids_sorted = sorted(list(self.diplomes_ids)) + # Etudids triés + self.etudids_sorted = sorted(list(self.diplomes_ids)) self.nom = self.get_repr() """Représentation textuelle de l'interclassement""" @@ -143,19 +144,24 @@ class InterClassTag(pe_tabletags.TableTag): # Synthétise les moyennes/classements par tag self.moyennes_tags: dict[str, pe_moytag.MoyennesTag] = {} for tag in self.tags_sorted: - notes = self.compute_notes_matrice(tag, etudids_sorted, self.champs_sorted) + # Les moyennes tous modules confondus + notes_gen = self.compute_notes_matrice(tag, pole=None) + # Les ressources + notes_res = self.compute_notes_matrice(tag, pole=ModuleType.RESSOURCE) + # Les SAEs + notes_saes = self.compute_notes_matrice(tag, pole=ModuleType.SAE) - coeffs = self.compute_coeffs_matrice( - tag, etudids_sorted, self.champs_sorted - ) - - aff = pe_affichage.aff_profil_coeffs(coeffs, with_index=True) + # Les coefficients de la moyenne générale + coeffs = self.compute_coeffs_matrice(tag) + aff = pe_affichage.repr_profil_coeffs(coeffs, with_index=True) pe_affichage.pe_print(f"--> Moyenne 👜{tag} avec coeffs: {aff} ") self.moyennes_tags[tag] = pe_moytag.MoyennesTag( tag, self.type, - notes, + notes_gen, + notes_res, + notes_saes, coeffs, # limite les moyennes aux étudiants de la promo ) @@ -163,29 +169,6 @@ class InterClassTag(pe_tabletags.TableTag): """Une représentation textuelle""" return f"{self.nom_rcs} par {self.type}" - def __aff_profil_coeffs(self, matrice_coeffs_moy_gen): - """Extrait de la matrice des coeffs, les différents types d'inscription - et de coefficients (appelés profil) des étudiants et les affiche - (pour debug) - """ - - # Les profils des coeffs d'UE (pour debug) - profils = [] - for i in matrice_coeffs_moy_gen.index: - val = matrice_coeffs_moy_gen.loc[i].fillna("-") - val = " | ".join([str(v) for v in val]) - if val not in profils: - profils += [val] - - # L'affichage - if len(profils) > 1: - profils_aff = "\n" + "\n".join([" " * 10 + prof for prof in profils]) - else: - profils_aff = "\n".join(profils) - pe_affichage.pe_print( - f" > Moyenne calculée avec pour coeffs (de compétences) : {profils_aff}" - ) - def _do_taglist(self): """Synthétise les tags à partir des TableTags (SXTag ou RCSTag) @@ -197,9 +180,7 @@ class InterClassTag(pe_tabletags.TableTag): tags.extend(rcstag.tags_sorted) return sorted(set(tags)) - def compute_notes_matrice( - self, tag, etudids_sorted: list[int], champs_sorted: list[str] - ) -> pd.DataFrame: + def compute_notes_matrice(self, tag, pole=None) -> pd.DataFrame: """Construit la matrice de notes (etudids x champs) en reportant les moyennes obtenues par les étudiants aux semestres de l'aggrégat pour le tag visé. @@ -207,49 +188,56 @@ class InterClassTag(pe_tabletags.TableTag): Les champs peuvent être des acronymes d'UEs ou des compétences. Args: - etudids_sorted: Les etudids des étudiants (diplômés) triés - champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice + tag: Le tag visé Return: Le dataFrame (etudids x champs) reportant les moyennes des étudiants aux champs """ + # etudids_sorted: Les etudids des étudiants (diplômés) triés + # champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice # Partant d'un dataframe vierge - df = pd.DataFrame(np.nan, index=etudids_sorted, columns=champs_sorted) + df = pd.DataFrame(np.nan, index=self.etudids_sorted, columns=self.champs_sorted) for rcstag in self.rcstags.values(): # Charge les moyennes au tag d'un RCStag if tag in rcstag.moyennes_tags: - moytag: pd.DataFrame = rcstag.moyennes_tags[tag].matrice_notes + moytag = rcstag.moyennes_tags[tag] + notes: pd.DataFrame = None + if pole == ModuleType.RESSOURCE: + notes = moytag.matrice_notes_res + elif pole == ModuleType.SAE: + notes = moytag.matrice_notes_saes + else: + notes = moytag.matrice_notes_gen # dataframe etudids x ues # Etudiants/Champs communs entre le RCSTag et les données interclassées ( etudids_communs, champs_communs, - ) = pe_comp.find_index_and_columns_communs(df, moytag) + ) = pe_comp.find_index_and_columns_communs(df, notes) # Injecte les notes par tag - df.loc[etudids_communs, champs_communs] = moytag.loc[ + df.loc[etudids_communs, champs_communs] = notes.loc[ etudids_communs, champs_communs ] return df - def compute_coeffs_matrice( - self, tag, etudids_sorted: list[int], champs_sorted: list[str] - ) -> pd.DataFrame: + def compute_coeffs_matrice(self, tag) -> pd.DataFrame: """Idem que compute_notes_matrices mais pour les coeffs Args: - etudids_sorted: Les etudids des étudiants (diplômés) triés - champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice + tag: Le tag visé Return: Le dataFrame (etudids x champs) reportant les moyennes des étudiants aux champs """ + # etudids_sorted: Les etudids des étudiants (diplômés) triés + # champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice # Partant d'un dataframe vierge - df = pd.DataFrame(np.nan, index=etudids_sorted, columns=champs_sorted) + df = pd.DataFrame(np.nan, index=self.etudids_sorted, columns=self.champs_sorted) for rcstag in self.rcstags.values(): if tag in rcstag.moyennes_tags: @@ -303,7 +291,7 @@ class InterClassTag(pe_tabletags.TableTag): return None def compute_df_synthese_moyennes_tag( - self, tag, aggregat=None, type_colonnes=False + self, tag, pole, aggregat=None, type_colonnes=False ) -> pd.DataFrame: """Construit le dataframe retraçant pour les données des moyennes pour affichage dans la synthèse du jury PE. (cf. to_df()) @@ -338,6 +326,7 @@ class InterClassTag(pe_tabletags.TableTag): if tag in rcstag.moyennes_tags: moytag: pd.DataFrame = rcstag.moyennes_tags[tag] df_moytag = moytag.to_df( + pole, aggregat=aggregat, cohorte="Groupe", ) diff --git a/app/pe/moys/pe_moytag.py b/app/pe/moys/pe_moytag.py index 3b7374f2f..ca6aa50f0 100644 --- a/app/pe/moys/pe_moytag.py +++ b/app/pe/moys/pe_moytag.py @@ -4,7 +4,7 @@ import pandas as pd from app import comp from app.comp.moy_sem import comp_ranks_series from app.pe.moys import pe_moy - +from app.scodoc.sco_utils import ModuleType CODE_MOY_UE = "UEs" CODE_MOY_COMPETENCES = "Compétences" @@ -16,7 +16,9 @@ class MoyennesTag: self, tag: str, type_moyenne: str, - matrice_notes: pd.DataFrame, # etudids x colonnes + matrice_notes_gen: pd.DataFrame, # etudids x colonnes + matrice_notes_res: pd.DataFrame, + matrice_notes_saes: pd.DataFrame, matrice_coeffs: pd.DataFrame, # etudids x colonnes ): """Classe centralisant la synthèse des moyennes/classements d'une série @@ -26,7 +28,11 @@ class MoyennesTag: Args: tag: Un tag - matrice_notes: Les moyennes (etudid x acronymes_ues ou etudid x compétences) aux différentes UEs ou compétences + matrice_notes_gen: Les moyennes (etudid x acronymes_ues ou etudid x compétences) + aux différentes UEs ou compétences (indépendamment des ressources + ou SAEs) + matrice_notes_res: Les moyennes limitées aux ressources + matrice_notes_saes: Les moyennes limitées aux saes matrice_coeffs: Les coeff à appliquer pour le calcul de la moyenne générale # notes_gen: Une série de notes (moyenne) sous forme d'un ``pd.Series`` (toutes UEs confondues) """ @@ -36,45 +42,94 @@ class MoyennesTag: self.type = type_moyenne """Le type de moyennes (par UEs ou par compétences)""" - # Les moyennes par UE - self.matrice_notes: pd.DataFrame = matrice_notes - """Les notes aux UEs ou aux compétences (DataFrame)""" + # Les moyennes par UE/compétences (ressources/SAEs confondues) + self.matrice_notes_gen: pd.DataFrame = matrice_notes_gen + """Les notes par UEs ou Compétences (DataFrame)""" + + # Les moyennes par UE/compétences (limitées aux ressources) + self.matrice_notes_res: pd.DataFrame = matrice_notes_res + """Les notes aux ressources par UEs ou Compétences""" + + # Les moyennes par UE/compétences (limitées aux SAEs) + self.matrice_notes_saes: pd.DataFrame = matrice_notes_saes + """Les notes aux SAEs par UEs ou Compétences""" 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""" - self.moyennes: dict[int, pd.DataFrame] = {} - """Les dataframes retraçant les moyennes/classements/statistiques des étudiants aux UEs""" + self.moyennes_gen: dict[int, pd.DataFrame] = {} + """Dataframes retraçant les moyennes/classements/statistiques des étudiants aux UEs""" - self.etudids = self.matrice_notes.index + self.moyennes_res: dict[int, pd.DataFrame] = {} + """Dataframes retraçant les moyennes/classements/statistiques des étudiants, limitées aux ressources""" + + self.moyennes_saes: dict[int, pd.DataFrame] = {} + """Dataframes retraçant les moyennes/classements/statistiques des étudiants, limitées aux SAEs""" + + self.etudids = self.matrice_notes_gen.index """Les étudids renseignés dans les moyennes""" - self.champs = self.matrice_notes.columns + self.champs = self.matrice_notes_gen.columns """Les champs (acronymes d'UE ou compétences) renseignés dans les moyennes""" for col in self.champs: # if ue.type != UE_SPORT: - notes = matrice_notes[col] - self.moyennes[col] = pe_moy.Moyenne(notes) + # Les moyennes tous modules confondus + notes = matrice_notes_gen[col] + self.moyennes_gen[col] = pe_moy.Moyenne(notes) + # par ressources + notes = matrice_notes_res[col] + self.moyennes_res[col] = pe_moy.Moyenne(notes) + # par SAEs + notes = matrice_notes_saes[col] + self.moyennes_saes[col] = pe_moy.Moyenne(notes) - # Les moyennes générales - notes_gen = pd.Series(np.nan, index=self.matrice_notes.index) - """Les notes générales (moyenne toutes UEs confonudes)""" - if self.has_notes(): - notes_gen = self.compute_moy_gen( - self.matrice_notes, self.matrice_coeffs_moy_gen + # Les moyennes générales (toutes UEs confondues) + self.notes_gen = pd.Series(np.nan, index=self.matrice_notes_gen.index) + if self.has_notes(pole=None): + self.notes_gen = self.compute_moy_gen( + self.matrice_notes_gen, self.matrice_coeffs_moy_gen ) - self.notes_gen = notes_gen - self.moyenne_gen = pe_moy.Moyenne(notes_gen) - """Le dataframe retraçant les moyennes/classements/statistiques général""" + self.moyenne_gen = pe_moy.Moyenne(self.notes_gen) + """Dataframe retraçant les moyennes/classements/statistiques général (toutes UESs confondues et modules confondus)""" - def has_notes(self): + self.notes_res = pd.Series(np.nan, index=self.matrice_notes_res.index) + if self.has_notes(pole=ModuleType.RESSOURCE): + self.notes_res = self.compute_moy_gen( + self.matrice_notes_res, self.matrice_coeffs_moy_gen + ) + self.moyenne_res = pe_moy.Moyenne(self.notes_res) + """Dataframe retraçant les moyennes/classements/statistiques général (toutes UESs confondues et uniquement sur les ressources)""" + + self.notes_saes = pd.Series(np.nan, index=self.matrice_notes_saes.index) + if self.has_notes(pole=ModuleType.SAE): + self.notes_saes = self.compute_moy_gen( + self.matrice_notes_saes, self.matrice_coeffs_moy_gen + ) + self.moyenne_saes = pe_moy.Moyenne(self.notes_saes) + """Dataframe retraçant les moyennes/classements/statistiques général (toutes UESs confondues et uniquement sur les SAEs)""" + + def has_notes(self, pole): """Détermine si les moyennes (aux UEs ou aux compétences) ont des notes + `pole` détermine les modules pris en compte : + + * si `pole` vaut `ModuleType.RESSOURCE`, seules les ressources sont prises + en compte (moyenne de ressources par UEs) + * si `pole` vaut `ModuleType.SAE`, seules les SAEs sont prises en compte + * si `pole` vaut `None` (ou toute autre valeur), + tous les modules sont pris en compte (moyenne d'UEs) + + Returns: True si la moytag a des notes, False sinon """ - notes = self.matrice_notes + if pole == ModuleType.RESSOURCE: + notes = self.matrice_notes_res + elif pole == ModuleType.SAE: + notes = self.matrice_notes_saes + else: + notes = self.matrice_notes_gen nbre_nan = notes.isna().sum().sum() nbre_notes_potentielles = len(notes.index) * len(notes.columns) if nbre_nan == nbre_notes_potentielles: @@ -107,11 +162,14 @@ class MoyennesTag: return moy_gen_tag - def to_df(self, aggregat=None, cohorte=None) -> pd.DataFrame: + def to_df(self, pole, aggregat=None, cohorte=None) -> pd.DataFrame: """Renvoie le df synthétisant l'ensemble des données connues Adapte les intitulés des colonnes aux données fournies (nom d'aggrégat, type de cohorte). + + `pole` détermine les modules à prendre en compte dans la moyenne (None=tous, + RESSOURCES ou SAES) """ etudids_sorted = sorted(self.etudids) @@ -121,19 +179,30 @@ class MoyennesTag: # Ajout des notes pour tous les champs champs = list(self.champs) for champ in champs: - df_champ = self.moyennes[champ].get_df_synthese() # le dataframe + if pole == ModuleType.RESSOURCE: + df_champ = self.moyennes_res[champ].get_df_synthese() + elif pole == ModuleType.SAE: + df_champ = self.moyennes_saes[champ].get_df_synthese() + else: + df_champ = self.moyennes_gen[champ].get_df_synthese() # le dataframe # Renomme les colonnes cols = [ - get_colonne_df(aggregat, self.tag, champ, cohorte, critere) + get_colonne_df(aggregat, pole, self.tag, champ, cohorte, critere) for critere in pe_moy.Moyenne.COLONNES_SYNTHESE ] df_champ.columns = cols df = df.join(df_champ) # Ajoute la moy générale - df_moy_gen = self.moyenne_gen.get_df_synthese() + df_moy_gen: pd.DataFrame = None + if pole == ModuleType.RESSOURCE: + df_moy_gen = self.moyenne_res.get_df_synthese() + elif pole == ModuleType.SAE: + df_moy_gen = self.moyenne_saes.get_df_synthese() + else: + df_moy_gen = self.moyenne_gen.get_df_synthese() cols = [ - get_colonne_df(aggregat, self.tag, CHAMP_GENERAL, cohorte, critere) + get_colonne_df(aggregat, pole, self.tag, CHAMP_GENERAL, cohorte, critere) for critere in pe_moy.Moyenne.COLONNES_SYNTHESE ] df_moy_gen.columns = cols @@ -142,12 +211,18 @@ class MoyennesTag: return df -def get_colonne_df(aggregat, tag, champ, cohorte, critere): +def get_colonne_df(aggregat, pole, tag, champ, cohorte, critere): """Renvoie le tuple (aggregat, tag, champ, cohorte, critere) utilisé pour désigner les colonnes du df""" liste_champs = [] if aggregat != None: liste_champs += [aggregat] + if pole == ModuleType.RESSOURCE: + liste_champs += ["ressources"] + elif pole == ModuleType.SAE: + liste_champs += ["saes"] + else: + liste_champs += ["global"] liste_champs += [tag, champ] if cohorte != None: liste_champs += [cohorte] diff --git a/app/pe/moys/pe_rcstag.py b/app/pe/moys/pe_rcstag.py index 75286aeb4..10bcd60bf 100644 --- a/app/pe/moys/pe_rcstag.py +++ b/app/pe/moys/pe_rcstag.py @@ -44,6 +44,7 @@ from app.pe.rcss import pe_rcs, pe_rcsemx import app.pe.moys.pe_sxtag as pe_sxtag import app.pe.pe_comp as pe_comp from app.pe.moys import pe_tabletags, pe_moytag +from app.scodoc.sco_utils import ModuleType class RCSemXTag(pe_tabletags.TableTag): @@ -117,8 +118,8 @@ class RCSemXTag(pe_tabletags.TableTag): set(self.acronymes_ues_to_competences.values()) ) """Compétences (triées par nom, extraites des SxTag aggrégés)""" - self._aff_comp_et_ues_debug() - # pe_affichage.pe_print(f"--> Compétences : {', '.join(self.competences_sorted)}") + aff = pe_affichage.repr_comp_et_ues(self.acronymes_ues_to_competences) + pe_affichage.pe_print(f"--> Compétences : {', '.join(self.competences_sorted)}") # Les tags self.tags_sorted = self._do_taglist() @@ -137,43 +138,45 @@ class RCSemXTag(pe_tabletags.TableTag): # ****************************************** # Cube d'inscription (etudids_sorted x compétences_sorted x sxstags) # indiquant quel sxtag est valide pour chaque étudiant - inscriptions_df, inscriptions_cube = self.compute_inscriptions_comps_cube( - tag, self.etudids_sorted, self.competences_sorted, self.sxstags_aggreges - ) + inscr_df, inscr_cube = self.compute_inscriptions_comps_cube(tag) # Traitement des notes # ******************** - # Cube de notes (etudids_sorted x compétences_sorted x sxstags) - notes_df, notes_cube = self.compute_notes_comps_cube( - tag, self.etudids_sorted, self.competences_sorted, self.sxstags_aggreges - ) + ### Moyennes tous modules confondus + # Cube de notes (etudids_sorted x compétences_sorted x sxstags) + notes_df, notes_cube = self.compute_notes_comps_cube(tag, mode=None) # Calcule les moyennes sous forme d'un dataframe en les "aggrégant" # compétence par compétence - moys_competences = compute_notes_competences( - notes_cube, - inscriptions_cube, - self.etudids_sorted, - self.competences_sorted, + moys_competences = self.compute_notes_competences(notes_cube, inscr_cube) + + ## Moyennes des ressources + notes_df_res, notes_cube_res = self.compute_notes_comps_cube( + tag, mode=ModuleType.RESSOURCE + ) + moys_competences_res = self.compute_notes_competences( + notes_cube_res, inscr_cube + ) + + ## Moyennes des SAEs + notes_df_sae, notes_cube_sae = self.compute_notes_comps_cube( + tag, mode=ModuleType.SAE + ) + moys_competences_saes = self.compute_notes_competences( + notes_cube_sae, inscr_cube ) # Traitement des coeffs pour la moyenne générale # *********************************************** # Df des coeffs sur tous les SxTags aggrégés - coeffs_df, coeffs_cube = self.compute_coeffs_comps_cube( - tag, - self.etudids_sorted, - self.competences_sorted, - self.sxstags_aggreges, - ) + coeffs_df, coeffs_cube = self.compute_coeffs_comps_cube(tag) + # Synthèse des coefficients à prendre en compte pour la moyenne générale - matrice_coeffs_moy_gen = compute_coeffs_competences( - coeffs_cube, - inscriptions_cube, - notes_cube, - self.etudids_sorted, - self.competences_sorted, + matrice_coeffs_moy_gen = self.compute_coeffs_competences( + coeffs_cube, inscr_cube, notes_cube ) - aff = pe_affichage.aff_profil_coeffs( + + # Affichage des coeffs + aff = pe_affichage.repr_profil_coeffs( matrice_coeffs_moy_gen, with_index=True ) pe_affichage.pe_print(f" > Moyenne calculée avec pour coeffs : {aff}") @@ -183,6 +186,8 @@ class RCSemXTag(pe_tabletags.TableTag): tag, pe_moytag.CODE_MOY_COMPETENCES, moys_competences, + moys_competences_res, + moys_competences_saes, matrice_coeffs_moy_gen, ) @@ -200,35 +205,42 @@ class RCSemXTag(pe_tabletags.TableTag): else: return f"{self.__class__.__name__} {self.rcs_id}" - def compute_notes_comps_cube( - self, - tag, - etudids_sorted: list[int], - competences_sorted: list[str], - sxstags: dict[(str, int) : pe_sxtag.SxTag], - ): + def compute_notes_comps_cube(self, tag, mode=None): """Pour un tag donné, construit le cube de notes (etudid x competences x SxTag) nécessaire au calcul des moyennes, en remplaçant les données d'UE (obtenus du SxTag) par les compétences + `mode` détermine les modules pris en compte : + + * si `mode` vaut `ModuleType.RESSOURCE`, seules les ressources sont prises + en compte (moyenne de ressources par UEs) + * si `mode` vaut `ModuleType.SAE`, seules les SAEs sont prises en compte + * si `mode` vaut `None` (ou toute autre valeur), + tous les modules sont pris en compte (moyenne d'UEs) + Args: tag: Le tag visé - etudids_sorted: Les etudis triés (dim 0) - competences_sorted: Les compétences triées (dim 1) - sxstags: Les SxTag à réunir """ + # etudids_sorted: list[int], + # competences_sorted: list[str], + # sxstags: dict[(str, int) : pe_sxtag.SxTag], notes_dfs = {} - for sxtag_id, sxtag in sxstags.items(): + for sxtag_id, sxtag in self.sxstags_aggreges.items(): # Partant d'un dataframe vierge notes_df = pd.DataFrame( - np.nan, index=etudids_sorted, columns=competences_sorted + np.nan, index=self.etudids_sorted, columns=self.competences_sorted ) # Charge les notes du semestre tag (copie car changement de nom de colonnes à venir) if tag in sxtag.moyennes_tags: # si le tag est présent dans le semestre moys_tag = sxtag.moyennes_tags[tag] - notes = moys_tag.matrice_notes.copy() # avec une copie + if mode == ModuleType.RESSOURCE: + notes = moys_tag.matrice_notes_res.copy() # avec une copie + elif mode == ModuleType.SAE: + notes = moys_tag.matrice_notes_saes.copy() + else: + notes = moys_tag.matrice_notes_gen.copy() # dataframe etudids x ues # Traduction des acronymes d'UE en compétences acronymes_ues_columns = notes.columns @@ -257,18 +269,14 @@ class RCSemXTag(pe_tabletags.TableTag): notes_dfs[sxtag_id] = notes_df """Réunit les notes sous forme d'un cube etudids x competences x semestres""" - sxtag_x_etudids_x_comps = [notes_dfs[sxtag_id] for sxtag_id in sxstags] + sxtag_x_etudids_x_comps = [ + notes_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges + ] notes_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1) return notes_dfs, notes_etudids_x_comps_x_sxtag - def compute_coeffs_comps_cube( - self, - tag, - etudids_sorted: list[int], - competences_sorted: list[str], - sxstags: dict[(str, int) : pe_sxtag.SxTag], - ): + def compute_coeffs_comps_cube(self, tag): """Pour un tag donné, construit le cube de coeffs (etudid x competences x SxTag) (traduisant les inscriptions des étudiants aux UEs en fonction de leur parcours) @@ -277,16 +285,17 @@ class RCSemXTag(pe_tabletags.TableTag): Args: tag: Le tag visé - etudids_sorted: Les etudis triés - competences_sorted: Les compétences triées - sxstags: Les SxTag à réunir """ + # etudids_sorted: list[int], + # competences_sorted: list[str], + # sxstags: dict[(str, int) : pe_sxtag.SxTag], + coeffs_dfs = {} - for sxtag_id, sxtag in sxstags.items(): + for sxtag_id, sxtag in self.sxstags_aggreges.items(): # Partant d'un dataframe vierge coeffs_df = pd.DataFrame( - np.nan, index=etudids_sorted, columns=competences_sorted + np.nan, index=self.etudids_sorted, columns=self.competences_sorted ) if tag in sxtag.moyennes_tags: moys_tag = sxtag.moyennes_tags[tag] @@ -316,7 +325,9 @@ class RCSemXTag(pe_tabletags.TableTag): coeffs_dfs[sxtag_id] = coeffs_df """Réunit les coeffs sous forme d'un cube etudids x competences x semestres""" - sxtag_x_etudids_x_comps = [coeffs_dfs[sxtag_id] for sxtag_id in sxstags] + sxtag_x_etudids_x_comps = [ + coeffs_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges + ] coeffs_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1) return coeffs_dfs, coeffs_etudids_x_comps_x_sxtag @@ -324,9 +335,6 @@ class RCSemXTag(pe_tabletags.TableTag): def compute_inscriptions_comps_cube( self, tag, - etudids_sorted: list[int], - competences_sorted: list[str], - sxstags: dict[(str, int) : pe_sxtag.SxTag], ): """Pour un tag donné, construit le cube etudid x competences x SxTag traduisant quels sxtags est à prendre @@ -335,24 +343,24 @@ class RCSemXTag(pe_tabletags.TableTag): Args: tag: Le tag visé - etudids_sorted: Les etudis triés - competences_sorted: Les compétences triées - sxstags: Les SxTag à réunir """ + # etudids_sorted: list[int], + # competences_sorted: list[str], + # sxstags: dict[(str, int) : pe_sxtag.SxTag], # Initialisation inscriptions_dfs = {} - for sxtag_id, sxtag in sxstags.items(): + for sxtag_id, sxtag in self.sxstags_aggreges.items(): # Partant d'un dataframe vierge inscription_df = pd.DataFrame( - 0, index=etudids_sorted, columns=competences_sorted + 0, index=self.etudids_sorted, columns=self.competences_sorted ) # Les étudiants dont les résultats au sxtag ont été calculés etudids_sxtag = sxtag.etudids_sorted # Les étudiants communs - etudids_communs = sorted(set(etudids_sorted) & set(etudids_sxtag)) + etudids_communs = sorted(set(self.etudids_sorted) & set(etudids_sxtag)) # Acte l'inscription inscription_df.loc[etudids_communs, :] = 1 @@ -361,7 +369,9 @@ class RCSemXTag(pe_tabletags.TableTag): inscriptions_dfs[sxtag_id] = inscription_df """Réunit les inscriptions sous forme d'un cube etudids x competences x semestres""" - sxtag_x_etudids_x_comps = [inscriptions_dfs[sxtag_id] for sxtag_id in sxstags] + sxtag_x_etudids_x_comps = [ + inscriptions_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges + ] inscriptions_etudids_x_comps_x_sxtag = np.stack( sxtag_x_etudids_x_comps, axis=-1 ) @@ -392,117 +402,97 @@ class RCSemXTag(pe_tabletags.TableTag): dict_competences |= sxtag.acronymes_ues_to_competences return dict_competences - def _aff_comp_et_ues_debug(self): - """Affichage pour debug""" - aff_comp = [] + def compute_notes_competences(self, set_cube: np.array, inscriptions: np.array): + """Calcule la moyenne par compétences (à un tag donné) sur plusieurs semestres (partant du set_cube). - for comp in self.competences_sorted: - liste = [] - for acro in self.acronymes_ues_to_competences: - if self.acronymes_ues_to_competences[acro] == comp: - liste += ["📍" + acro] - aff_comp += [f" 💡{comp} (⇔ {', '.join(liste)})"] - pe_affichage.pe_print(f"--> Compétences :") - pe_affichage.pe_print("\n".join(aff_comp)) + La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles + + *Remarque* : Adaptation de moy_ue.compute_ue_moys_apc au cas des moyennes de tag + par aggrégat de plusieurs semestres. + + Args: + set_cube: notes moyennes aux compétences ndarray + (etuds x UEs|compétences x sxtags), des floats avec des NaN + inscriptions: inscrptions aux compétences ndarray + (etuds x UEs|compétences x sxtags), des 0 et des 1 + Returns: + Un DataFrame avec pour columns les moyennes par tags, + et pour rows les etudid + """ + # etudids_sorted: liste des étudiants (dim. 0 du cube) + # competences_sorted: list (dim. 1 du cube) + nb_etuds, nb_comps, nb_semestres = set_cube.shape + # assert nb_etuds == len(etudids_sorted) + # assert nb_comps == len(competences_sorted) + + # Applique le masque d'inscriptions + set_cube_significatif = set_cube * inscriptions + + # Quelles entrées du cube contiennent des notes ? + mask = ~np.isnan(set_cube_significatif) + + # Enlève les NaN du cube de notes pour les entrées manquantes + set_cube_no_nan = np.nan_to_num(set_cube_significatif, nan=0.0) + + # Les moyennes par tag + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + etud_moy_tag = np.sum(set_cube_no_nan, axis=2) / np.sum(mask, axis=2) + + # Le dataFrame des notes moyennes + etud_moy_tag_df = pd.DataFrame( + etud_moy_tag, + index=self.etudids_sorted, # les etudids + columns=self.competences_sorted, # les competences + ) + etud_moy_tag_df.fillna(np.nan) + + return etud_moy_tag_df + + def compute_coeffs_competences( + self, + coeff_cube: np.array, + inscriptions: np.array, + set_cube: np.array, + ): + """Calcule les coeffs à utiliser pour la moyenne générale (toutes compétences + confondues), en fonction des inscriptions. + + Args: + coeffs_cube: coeffs impliqués dans la moyenne générale (semestres par semestres) + inscriptions: inscriptions aux UES|Compétences ndarray + (etuds x UEs|compétences x sxtags), des 0 ou des 1 + set_cube: les notes -def compute_coeffs_competences( - coeff_cube: np.array, - inscriptions: np.array, - set_cube: np.array, - etudids_sorted: list, - competences_sorted: list, -): - """Calcule les coeffs à utiliser pour la moyenne générale (toutes compétences - confondues), en fonction des inscriptions. + Returns: + Un DataFrame de coefficients (etudids_sorted x compétences_sorted) + """ + # etudids_sorted: liste des étudiants (dim. 0 du cube) + # competences_sorted: list (dim. 1 du cube) + nb_etuds, nb_comps, nb_semestres = inscriptions.shape + # assert nb_etuds == len(etudids_sorted) + # assert nb_comps == len(competences_sorted) - Args: - coeffs_cube: coeffs impliqués dans la moyenne générale (semestres par semestres) - inscriptions: inscriptions aux UES|Compétences ndarray - (etuds x UEs|compétences x sxtags), des 0 ou des 1 - set_cube: les notes - etudids_sorted: liste des étudiants (dim. 0 du cube) - competences_sorted: list (dim. 1 du cube) + # Applique le masque des inscriptions aux coeffs et aux notes + coeffs_significatifs = coeff_cube * inscriptions - Returns: - Un DataFrame de coefficients (etudids_sorted x compétences_sorted) - """ - nb_etuds, nb_comps, nb_semestres = inscriptions.shape - assert nb_etuds == len(etudids_sorted) - assert nb_comps == len(competences_sorted) + # Enlève les NaN du cube de notes pour les entrées manquantes + coeffs_cube_no_nan = np.nan_to_num(coeffs_significatifs, nan=0.0) - # Applique le masque des inscriptions aux coeffs et aux notes - coeffs_significatifs = coeff_cube * inscriptions + # Quelles entrées du cube contiennent des notes ? + mask = ~np.isnan(set_cube) - # Enlève les NaN du cube de notes pour les entrées manquantes - coeffs_cube_no_nan = np.nan_to_num(coeffs_significatifs, nan=0.0) + # Retire les coefficients associés à des données sans notes + coeffs_cube_no_nan = coeffs_cube_no_nan * mask - # Quelles entrées du cube contiennent des notes ? - mask = ~np.isnan(set_cube) + # Somme les coefficients (correspondant à des notes) + coeff_tag = np.sum(coeffs_cube_no_nan, axis=2) - # Retire les coefficients associés à des données sans notes - coeffs_cube_no_nan = coeffs_cube_no_nan * mask + # Le dataFrame des coeffs + coeffs_df = pd.DataFrame( + coeff_tag, index=self.etudids_sorted, columns=self.competences_sorted + ) + # Remet à Nan les coeffs à 0 + coeffs_df = coeffs_df.fillna(np.nan) - # Somme les coefficients (correspondant à des notes) - coeff_tag = np.sum(coeffs_cube_no_nan, axis=2) - - # Le dataFrame des coeffs - coeffs_df = pd.DataFrame( - coeff_tag, index=etudids_sorted, columns=competences_sorted - ) - # Remet à Nan les coeffs à 0 - coeffs_df = coeffs_df.fillna(np.nan) - - return coeffs_df - - -def compute_notes_competences( - set_cube: np.array, - inscriptions: np.array, - etudids_sorted: list, - competences_sorted: list, -): - """Calcule la moyenne par compétences (à un tag donné) sur plusieurs semestres (partant du set_cube). - - La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles - - *Remarque* : Adaptation de moy_ue.compute_ue_moys_apc au cas des moyennes de tag - par aggrégat de plusieurs semestres. - - Args: - set_cube: notes moyennes aux compétences ndarray - (etuds x UEs|compétences x sxtags), des floats avec des NaN - inscriptions: inscrptions aux compétences ndarray - (etuds x UEs|compétences x sxtags), des 0 et des 1 - etudids_sorted: liste des étudiants (dim. 0 du cube) - competences_sorted: list (dim. 1 du cube) - Returns: - Un DataFrame avec pour columns les moyennes par tags, - et pour rows les etudid - """ - nb_etuds, nb_comps, nb_semestres = set_cube.shape - assert nb_etuds == len(etudids_sorted) - assert nb_comps == len(competences_sorted) - - # Applique le masque d'inscriptions - set_cube_significatif = set_cube * inscriptions - - # Quelles entrées du cube contiennent des notes ? - mask = ~np.isnan(set_cube_significatif) - - # Enlève les NaN du cube de notes pour les entrées manquantes - set_cube_no_nan = np.nan_to_num(set_cube_significatif, nan=0.0) - - # Les moyennes par tag - with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) - etud_moy_tag = np.sum(set_cube_no_nan, axis=2) / np.sum(mask, axis=2) - - # Le dataFrame des notes moyennes - etud_moy_tag_df = pd.DataFrame( - etud_moy_tag, - index=etudids_sorted, # les etudids - columns=competences_sorted, # les competences - ) - etud_moy_tag_df.fillna(np.nan) - - return etud_moy_tag_df + return coeffs_df diff --git a/app/pe/moys/pe_ressemtag.py b/app/pe/moys/pe_ressemtag.py index 2b2b7581d..d974f47c9 100644 --- a/app/pe/moys/pe_ressemtag.py +++ b/app/pe/moys/pe_ressemtag.py @@ -1,4 +1,4 @@ -# -*- mode: python -*- +# -*- pole: python -*- # -*- coding: utf-8 -*- ############################################################################## @@ -46,6 +46,7 @@ import app.pe.pe_etudiant as pe_etudiant from app.pe.moys import pe_tabletags, pe_moytag from app.scodoc import sco_tag_module from app.scodoc import codes_cursus as sco_codes +from app.scodoc.sco_utils import * class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): @@ -79,9 +80,20 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): ] """Liste des UEs standards du ResultatsSemestreBUT""" + # Les parcours des étudiants à ce semestre + self.parcours = [] + """Parcours auxquels sont inscrits les étudiants""" + for etudid in self.etudids_sorted: + parcour = self.formsemestre.etuds_inscriptions[etudid].parcour + if parcour: + self.parcours += [parcour.libelle] + else: + self.parcours += [None] + # Les UEs en fonction des parcours self.ues_inscr_parcours_df = self.load_ues_inscr_parcours() - """Les inscriptions des étudiants aux UEs du parcours""" + """Inscription des étudiants aux UEs des parcours""" + # Les acronymes des UEs self.ues_to_acronymes = {ue.id: ue.acronyme for ue in self.ues_standards} self.acronymes_sorted = sorted(self.ues_to_acronymes.values()) @@ -89,7 +101,7 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): # Les compétences associées aux UEs (définies par les acronymes) self.acronymes_ues_to_competences = {} - """L'association acronyme d'UEs -> compétence""" + """Association acronyme d'UEs -> compétence""" for ue in self.ues_standards: assert ue.niveau_competence, ScoValueError( "Des UEs ne sont pas rattachées à des compétences" @@ -99,12 +111,15 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): self.competences_sorted = sorted( list(set(self.acronymes_ues_to_competences.values())) ) - """Les compétences triées par nom""" - self._aff_ue_et_comp_debug() + """Compétences triées par nom""" + aff = pe_affichage.repr_asso_ue_comp(self.acronymes_ues_to_competences) + pe_affichage.pe_print(f"--> UEs/Compétences : {aff}") # Les tags personnalisés et auto: tags_dict = self._get_tags_dict() - self._aff_tags_debug(tags_dict) + pe_affichage.pe_print( + f"""--> {pe_affichage.aff_tags_par_categories(tags_dict)}""" + ) self._check_tags(tags_dict) # Les coefficients pour le calcul de la moyenne générale, donnés par @@ -113,7 +128,10 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): self.ues_inscr_parcours_df, self.ues_standards ) """DataFrame indiquant les coeffs des UEs par ordre alphabétique d'acronyme""" - self.__aff_profil_coeffs() + profils_aff = pe_affichage.repr_profil_coeffs(self.matrice_coeffs_moy_gen) + pe_affichage.pe_print( + f"--> Moyenne générale calculée avec pour coeffs d'UEs : {profils_aff}" + ) # Les capitalisations (mask etuids x acronyme_ue valant True si capitalisée, False sinon) self.capitalisations = self._get_capitalisations(self.ues_standards) @@ -121,24 +139,44 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): # Calcul des moyennes & les classements de chaque étudiant à chaque tag self.moyennes_tags = {} - """Les moyennes par tags (personnalisés ou 'but')""" + """Moyennes par tags (personnalisés ou 'but')""" 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) + info_tag = tags_dict["personnalises"][tag] + # Les moyennes générales par UEs + moy_ues_tag = self.compute_moy_ues_tag(info_tag=info_tag, pole=None) + # Les moyennes par ressources de chaque UE + moy_res_tag = self.compute_moy_ues_tag( + info_tag=info_tag, pole=ModuleType.RESSOURCE + ) + # Les moyennes par SAEs de chaque UE + moy_saes_tag = self.compute_moy_ues_tag( + info_tag=info_tag, pole=ModuleType.SAE + ) self.moyennes_tags[tag] = pe_moytag.MoyennesTag( - tag, pe_moytag.CODE_MOY_UE, moy_ues_tag, self.matrice_coeffs_moy_gen + tag, + pe_moytag.CODE_MOY_UE, + moy_ues_tag, + moy_res_tag, + moy_saes_tag, + self.matrice_coeffs_moy_gen, ) # Ajoute les moyennes par UEs + la moyenne générale (but) moy_gen = self.compute_moy_gen() + moy_res_gen = self.compute_moy_ues_tag(info_tag=None, pole=ModuleType.RESSOURCE) + moy_saes_gen = self.compute_moy_ues_tag(info_tag=None, pole=ModuleType.SAE) + self.moyennes_tags["but"] = pe_moytag.MoyennesTag( "but", pe_moytag.CODE_MOY_UE, moy_gen, - self.matrice_coeffs_moy_gen, # , moy_gen_but + moy_res_gen, + moy_saes_gen, + self.matrice_coeffs_moy_gen, ) + # Tous les tags self.tags_sorted = self.get_all_significant_tags() """Tags (personnalisés+compétences) par ordre alphabétique""" @@ -206,33 +244,58 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): capitalisations = capitalisations.sort_index(axis=1) 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, + def compute_moy_ues_tag( + self, info_tag: dict[int, dict] = None, pole=None + ) -> pd.DataFrame: + """Calcule la moyenne par UE des étudiants pour un tag donné, en ayant connaissance des informations sur le tag. + info_tag détermine les modules pris en compte : + * si non `None`, seuls les modules rattachés au tag sont pris en compte + * si `None`, tous les modules (quelque soit leur rattachement au tag) sont pris + en compte (sert au calcul de la moyenne générale par ressource ou SAE) + + `pole` détermine les modules pris en compte : + + * si `pole` vaut `ModuleType.RESSOURCE`, seules les ressources sont prises + en compte (moyenne de ressources par UEs) + * si `pole` vaut `ModuleType.SAE`, seules les SAEs sont prises en compte + * si `pole` vaut `None` (ou toute autre valeur), + tous les modules sont pris en compte (moyenne d'UEs) + + 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 """ + modimpls_sorted = self.formsemestre.modimpls_sorted # Adaptation du mask de calcul des moyennes au tag visé - modimpls_mask = [ - modimpl.module.ue.type == sco_codes.UE_STANDARD - for modimpl in self.formsemestre.modimpls_sorted - ] + modimpls_mask = [] + for modimpl in modimpls_sorted: + module = modimpl.module # Le module + mask = module.ue.type == sco_codes.UE_STANDARD # Est-ce une UE stantard ? + if pole == ModuleType.RESSOURCE: + mask &= module.module_type == ModuleType.RESSOURCE + elif pole == ModuleType.SAE: + mask &= module.module_type == ModuleType.SAE + modimpls_mask += [mask] - # 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 + # Prise en compte du tag + if info_tag: + # Désactive tous les modules qui ne sont pas pris en compte pour ce tag + for i, modimpl in enumerate(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 + if info_tag: + 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( @@ -304,24 +367,6 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): dict_tags["auto"] = {"but": {}} return dict_tags - def _aff_ue_et_comp_debug(self): - """Affichage pour debug""" - aff_comp = [] - for acro in self.acronymes_sorted: - aff_comp += [f"📍{acro} (∈ 💡{self.acronymes_ues_to_competences[acro]})"] - pe_affichage.pe_print(f"--> UEs/Compétences : {', '.join(aff_comp)}") - - def _aff_tags_debug(self, dict_tags): - """Affichage pour debug""" - noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys()))) - noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp - aff_tags_auto = ", ".join([f"👜{nom}" for nom in noms_tags_auto]) - aff_tags_perso = ", ".join([f"👜{nom}" for nom in noms_tags_perso]) - # Affichage - pe_affichage.pe_print( - f"""--> Tags du programme de formation : {aff_tags_perso} + Automatiques : {aff_tags_auto}""" - ) - def _check_tags(self, dict_tags): """Vérifie l'unicité des tags""" noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys()))) @@ -347,29 +392,6 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): """ raise ScoValueError(message) - def __aff_profil_coeffs(self): - """Extrait de la matrice des coeffs, les différents types d'inscription - et de coefficients (appelés profil) des étudiants et les affiche - (pour debug) - """ - - # Les profils des coeffs d'UE (pour debug) - profils = [] - for i in self.matrice_coeffs_moy_gen.index: - val = self.matrice_coeffs_moy_gen.loc[i].fillna("-") - val = " | ".join([str(v) for v in val]) - if val not in profils: - profils += [val] - - # L'affichage - if len(profils) > 1: - profils_aff = "\n" + "\n".join([" " * 10 + prof for prof in profils]) - else: - profils_aff = "\n".join(profils) - pe_affichage.pe_print( - f"--> Moyenne générale calculée avec pour coeffs d'UEs : {profils_aff}" - ) - def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre): """Etant données les implémentations des modules du semestre (modimpls), @@ -414,12 +436,7 @@ def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre): # 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 diff --git a/app/pe/moys/pe_sxtag.py b/app/pe/moys/pe_sxtag.py index 36ef71a42..b1594593c 100644 --- a/app/pe/moys/pe_sxtag.py +++ b/app/pe/moys/pe_sxtag.py @@ -43,6 +43,7 @@ import numpy as np from app.pe.moys import pe_moytag, pe_tabletags import app.pe.rcss.pe_trajectoires as pe_trajectoires +from app.scodoc.sco_utils import ModuleType class SxTag(pe_tabletags.TableTag): @@ -91,7 +92,8 @@ class SxTag(pe_tabletags.TableTag): and isinstance(self.sxtag_id[1], int) ), "Format de l'identifiant du SxTag non respecté" - self.nom_rcs = sxtag_id[0] + self.agregat = sxtag_id[0] + """Nom de l'aggrégat du RCS""" self.semx = semx """Le SemX sur lequel il s'appuie""" @@ -121,7 +123,7 @@ class SxTag(pe_tabletags.TableTag): # Les tags self.tags_sorted = self.ressembuttag_final.tags_sorted """Tags (extraits du ReSemBUTTag final)""" - aff_tag = ["👜" + tag for tag in self.tags_sorted] + aff_tag = pe_affichage.repr_tags(self.tags_sorted) pe_affichage.pe_print(f"--> Tags : {', '.join(aff_tag)}") # Les UE données par leur acronyme @@ -135,13 +137,18 @@ class SxTag(pe_tabletags.TableTag): """L'association acronyme d'UEs -> compétence""" self.competences_sorted = sorted(self.acronymes_ues_to_competences.values()) """Les compétences triées par nom""" - self._aff_ue_et_comp_debug() + + aff = pe_affichage.repr_asso_ue_comp(self.acronymes_ues_to_competences) + pe_affichage.pe_print(f"--> UEs/Compétences : {aff}") # Les coeffs pour la moyenne générale (traduisant également l'inscription # des étudiants aux UEs) (etudids_sorted x acronymes_ues_sorted) self.matrice_coeffs_moy_gen = self.ressembuttag_final.matrice_coeffs_moy_gen """La matrice des coeffs pour la moyenne générale""" - self.__aff_profil_coeffs() + aff = pe_affichage.repr_profil_coeffs(self.matrice_coeffs_moy_gen) + pe_affichage.pe_print( + f"--> Moyenne générale calculée avec pour coeffs d'UEs : {aff}" + ) # Masque des inscriptions et des capitalisations self.masque_df = None @@ -152,83 +159,85 @@ class SxTag(pe_tabletags.TableTag): self.ressembuttags, self.fid_final, ) - self._aff_capitalisations() + pe_affichage.aff_capitalisations( + self.etuds, + self.ressembuttags, + self.fid_final, + self.acronymes_sorted, + self.masque_df, + ) # Les moyennes par tag self.moyennes_tags: dict[str, pd.DataFrame] = {} """Moyennes aux UEs (identifiées par leur acronyme) des différents tags""" + if self.tags_sorted: pe_affichage.pe_print("--> Calcul des moyennes par tags :") for tag in self.tags_sorted: - # Y-a-t-il des notes ? - if not self.has_notes(tag): - pe_affichage.pe_print(f" > MoyTag 👜{tag} actuellement sans notes") + pe_affichage.pe_print(f" > MoyTag 👜{tag}") + + # Masque des inscriptions aux UEs (extraits de la matrice de coefficients) + inscr_mask: np.array = ~np.isnan(self.matrice_coeffs_moy_gen.to_numpy()) + + # Moyennes (tous modules confondus) + if not self.has_notes_tag(tag): + pe_affichage.pe_print( + f" --> Semestre (final) actuellement sans notes" + ) matrice_moys_ues = pd.DataFrame( np.nan, index=self.etudids_sorted, columns=self.acronymes_sorted ) + matrice_moys_res = pd.DataFrame( + np.nan, index=self.etudids_sorted, columns=self.acronymes_sorted + ) + matrice_moys_saes = pd.DataFrame( + np.nan, index=self.etudids_sorted, columns=self.acronymes_sorted + ) else: - # Cube de note etudids x UEs - notes_df, notes_cube = compute_notes_ues_cube( - tag, - self.etudids_sorted, - self.acronymes_sorted, - self.ressembuttags, + # Moyennes tous modules confondus + ### Cube de note etudids x UEs tous modules confondus + notes_df_gen, notes_cube_gen = self.compute_notes_ues_cube( + tag, mode=None + ) + # DataFrame des moyennes (tous modules confondus) + matrice_moys_ues = self.compute_notes_ues( + notes_cube_gen, masque_cube, inscr_mask ) - # Masque des inscriptions aux UEs (extraits de la matrice de coefficients) - inscr_mask: np.array = ~np.isnan(self.matrice_coeffs_moy_gen.to_numpy()) - - # Matrice des moyennes - matrice_moys_ues: pd.DataFrame = compute_notes_ues( - notes_cube, - masque_cube, - self.etudids_sorted, - self.acronymes_sorted, - inscr_mask, + ### Moyennes par ressources + notes_df_res, notes_cube_res = self.compute_notes_ues_cube( + tag, mode=ModuleType.RESSOURCE + ) + matrice_moys_res = self.compute_notes_ues( + notes_cube_res, masque_cube, inscr_mask ) - # Affichage de debug - aff = pe_affichage.aff_profil_coeffs( - self.matrice_coeffs_moy_gen, with_index=True + ### Moyennes par SAEs + notes_df_saes, notes_cube_saes = self.compute_notes_ues_cube( + tag, mode=ModuleType.SAE + ) + matrice_moys_saes = self.compute_notes_ues( + notes_cube_saes, masque_cube, inscr_mask ) - pe_affichage.pe_print(f" > MoyTag 👜{tag} : {aff}") # Mémorise les infos pour la moyennes au tag self.moyennes_tags[tag] = pe_moytag.MoyennesTag( tag, pe_moytag.CODE_MOY_UE, matrice_moys_ues, + matrice_moys_res, + matrice_moys_saes, self.matrice_coeffs_moy_gen, ) - def __aff_profil_coeff_ects(self, tag): - """Extrait de la matrice des coeffs, les différents types d'inscription - et de coefficients (appelés profil) des étudiants et les affiche - (pour debug) - """ + # Affichage de debug + aff = pe_affichage.repr_profil_coeffs( + self.matrice_coeffs_moy_gen, with_index=True + ) + pe_affichage.pe_print(f" > Moyenne générale calculée avec : {aff}") - # Les profils des coeffs d'UE (pour debug) - profils = [] - for i in self.matrice_coeffs_moy_gen.index: - val = self.matrice_coeffs_moy_gen.loc[i].fillna("-") - val = " | ".join([str(v) for v in val]) - if val not in profils: - profils += [val] - - # L'affichage - if len(profils) > 1: - profils_aff = "\n" + "\n".join([" " * 10 + prof for prof in profils]) - else: - profils_aff = "\n".join(profils) - - # L'affichage - ues = ", ".join(self.acronymes_sorted) - pe_affichage.pe_print( - f" > MoyTag 👜{tag} pour UEs : {ues} avec pour coeffs : {profils_aff}" - ) - - def has_notes(self, tag): + def has_notes_tag(self, tag): """Détermine si le SxTag, pour un tag donné, est en cours d'évaluation. Si oui, n'a pas (encore) de notes dans le resformsemestre final. @@ -239,13 +248,7 @@ class SxTag(pe_tabletags.TableTag): True si a des notes, False sinon """ moy_tag_dernier_sem = self.ressembuttag_final.moyennes_tags[tag] - notes = moy_tag_dernier_sem.matrice_notes - nbre_nan = notes.isna().sum().sum() - nbre_notes_potentielles = len(notes.index) * len(notes.columns) - if nbre_nan == nbre_notes_potentielles: - return False - else: - return True + return moy_tag_dernier_sem.has_notes(None) def __eq__(self, other): """Egalité de 2 SxTag sur la base de leur identifiant""" @@ -258,104 +261,128 @@ class SxTag(pe_tabletags.TableTag): return f"SXTag basé sur {self.semx.get_repr()}" else: # affichage = [str(fid) for fid in self.ressembuttags] - return f"SXTag {self.nom_rcs}#{self.fid_final}" + return f"SXTag {self.agregat}#{self.fid_final}" - def _aff_ue_et_comp_debug(self): - """Affichage pour debug""" - aff_comp = [] - for acro in self.acronymes_sorted: - aff_comp += [f"📍{acro} (∈ 💡{self.acronymes_ues_to_competences[acro]})"] - pe_affichage.pe_print(f"--> UEs/Compétences : {', '.join(aff_comp)}") + def compute_notes_ues_cube(self, tag, mode=None) -> (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). - def _aff_capitalisations(self): - """Affichage des capitalisations du sxtag pour debug""" - aff_cap = [] - for etud in self.etuds: - cap = [] - for frmsem_id in self.ressembuttags: - if frmsem_id != self.fid_final: - for accr in self.acronymes_sorted: - if self.masque_df[frmsem_id].loc[etud.etudid, accr] > 0.0: - cap += [accr] - if cap: - aff_cap += [f" > {etud.nomprenom} : {', '.join(cap)}"] - if aff_cap: - pe_affichage.pe_print(f"--> ⚠️ Capitalisations :") - pe_affichage.pe_print("\n".join(aff_cap)) + `mode` détermine les modules pris en compte : - def __aff_profil_coeffs(self): - """Extrait de la matrice des coeffs, les différents types d'inscription - et de coefficients (appelés profil) des étudiants et les affiche - (pour debug) + * si `mode` vaut `ModuleType.RESSOURCE`, seules les ressources sont prises + en compte (moyenne de ressources par UEs) + * si `mode` vaut `ModuleType.SAE`, seules les SAEs sont prises en compte + * si `mode` vaut `None` (ou toute autre valeur), + tous les modules sont pris en compte (moyenne d'UEs) + + Args: + tag: Le tag considéré (personalisé ou "but") """ + # 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(self.ressembuttags.keys()) - # Les profils des coeffs d'UE (pour debug) - profils = [] - for i in self.matrice_coeffs_moy_gen.index: - val = self.matrice_coeffs_moy_gen.loc[i].fillna("-") - val = " | ".join([str(v) for v in val]) - if val not in profils: - profils += [val] + dfs = {} - # L'affichage - if len(profils) > 1: - profils_aff = "\n" + "\n".join([" " * 10 + prof for prof in profils]) - else: - profils_aff = "\n".join(profils) - pe_affichage.pe_print( - f"--> Moyenne générale calculée avec pour coeffs d'UEs : {profils_aff}" + for frmsem_id in semestres_id: + # Partant d'un dataframe vierge + df = pd.DataFrame( + np.nan, index=self.etudids_sorted, columns=self.acronymes_sorted + ) + + # Charge les notes du semestre tag + sem_tag = self.ressembuttags[frmsem_id] + moys_tag = sem_tag.moyennes_tags[tag] + if mode == ModuleType.RESSOURCE: + notes = moys_tag.matrice_notes_res + elif mode == ModuleType.SAE: + notes = moys_tag.matrice_notes_saes + else: + notes = moys_tag.matrice_notes_gen # dataframe etudids x ues + + # 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_notes_ues( + self, + set_cube: np.array, + masque_cube: np.array, + inscr_mask: np.array, + ) -> pd.DataFrame: + """Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE + par UE) obtenue par un étudiant à un semestre. + + Args: + set_cube: notes moyennes aux modules ndarray + (semestre_ids x etudids x UEs), des floats avec des NaN + 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 + inscr_mask: masque etudids x UE traduisant les inscriptions des + étudiants aux UE (du semestre terminal) + Returns: + Un DataFrame avec pour columns les moyennes par ues, + et pour rows les etudid + """ + # etudids_sorted: liste des étudiants (dim. 0 du cube) trié par etudid + # acronymes_sorted: liste des acronymes des ues (dim. 1 du cube) trié par acronyme + nb_etuds, nb_ues, nb_semestres = set_cube.shape + nb_etuds_mask, nb_ues_mask = inscr_mask.shape + # assert nb_etuds == len(self.etudids_sorted) + # assert nb_ues == len(self.acronymes_sorted) + # assert nb_etuds == nb_etuds_mask + # assert nb_ues == nb_ues_mask + + # 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) + + # 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=self.etudids_sorted, # les etudids + columns=self.acronymes_sorted, # les acronymes d'UEs ) + etud_moy_tag_df = etud_moy_tag_df.fillna(np.nan) -def compute_notes_ues_cube( - tag, etudids_sorted, acronymes_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). - - Args: - etudids_sorted: La liste des etudids triés par ordre croissant (dim 0) - acronymes_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()) - - dfs = {} - - for frmsem_id in semestres_id: - # Partant d'un dataframe vierge - df = pd.DataFrame(np.nan, index=etudids_sorted, columns=acronymes_sorted) - - # 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 - - # 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 + return etud_moy_tag_df def compute_masques_capitalisation_cube( @@ -415,65 +442,3 @@ def compute_masques_capitalisation_cube( 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, - masque_cube: np.array, - etudids_sorted: list, - acronymes_sorted: list, - inscr_mask: np.array, -): - """Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE - par UE) obtenue par un étudiant à un semestre. - - Args: - set_cube: notes moyennes aux modules ndarray - (semestre_ids x etudids x UEs), des floats avec des NaN - 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_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: - Un DataFrame avec pour columns les moyennes par ues, - et pour rows les etudid - """ - nb_etuds, nb_ues, nb_semestres = set_cube.shape - nb_etuds_mask, nb_ues_mask = inscr_mask.shape - assert nb_etuds == len(etudids_sorted) - assert nb_ues == len(acronymes_sorted) - assert nb_etuds == nb_etuds_mask - assert nb_ues == nb_ues_mask - - # 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) - - # 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_sorted, # les etudids - columns=acronymes_sorted, # les acronymes d'UEs - ) - - etud_moy_tag_df = etud_moy_tag_df.fillna(np.nan) - - return etud_moy_tag_df diff --git a/app/pe/moys/pe_tabletags.py b/app/pe/moys/pe_tabletags.py index 4f5c1c31e..9f5bf7ded 100644 --- a/app/pe/moys/pe_tabletags.py +++ b/app/pe/moys/pe_tabletags.py @@ -1,4 +1,4 @@ -# -*- mode: python -*- +# -*- pole: python -*- # -*- coding: utf-8 -*- ############################################################################## @@ -79,12 +79,17 @@ class TableTag(object): tag: str = "" moytag: pe_moytag.MoyennesTag = None for tag, moytag in self.moyennes_tags.items(): - if moytag.has_notes(): + if moytag.has_notes(None): tags.append(tag) return sorted(tags) def to_df( - self, administratif=True, aggregat=None, tags_cibles=None, cohorte=None + self, + pole, + administratif=True, + aggregat=None, + tags_cibles=None, + cohorte=None, ) -> pd.DataFrame: """Renvoie un dataframe listant toutes les données des moyennes/classements/nb_inscrits/min/max/moy @@ -98,6 +103,7 @@ class TableTag(object): aggregat: l'aggrégat représenté tags_cibles: la liste des tags ciblés cohorte: la cohorte représentée + pole: Les modules à prendre en compte dans la moyenne (None=tous, Ressources ou SAEs) Returns: Le dataframe complet de synthèse """ @@ -114,20 +120,22 @@ class TableTag(object): # Les étudiants visés if administratif: - df = df_administratif(self.etuds, aggregat, cohorte) + df = df_administratif(self.etuds, aggregat=aggregat, cohorte=cohorte) else: df = pd.DataFrame(index=self.etudids) # Ajout des données par tags for tag in tags_cibles: if tag in self.moyennes_tags: - moy_tag_df = self.moyennes_tags[tag].to_df(aggregat, cohorte) + moy_tag_df = self.moyennes_tags[tag].to_df( + pole, aggregat=aggregat, cohorte=cohorte + ) df = df.join(moy_tag_df) # Tri par nom, prénom if administratif: colonnes_tries = [ - _get_champ_administratif(champ, aggregat, cohorte) + _get_champ_administratif(champ, aggregat=aggregat, cohorte=cohorte) for champ in CHAMPS_ADMINISTRATIFS[1:] ] # Nom + Prénom df = df.sort_values(by=colonnes_tries) @@ -155,6 +163,7 @@ def _get_champ_administratif(champ, aggregat=None, cohorte=None): liste = [] if aggregat != None: liste += [aggregat] + liste += [""] # le pole (None, RESSOURCES, SAEs) liste += ["Administratif", "Identité"] if cohorte != None: liste += [champ] diff --git a/app/pe/pe_affichage.py b/app/pe/pe_affichage.py index 3dc97265c..54d6aff6f 100644 --- a/app/pe/pe_affichage.py +++ b/app/pe/pe_affichage.py @@ -8,8 +8,9 @@ from flask import g from app import log +from app.pe.rcss import pe_rcs -PE_DEBUG = False +PE_DEBUG = True # On stocke les logs PE dans g.scodoc_pe_log @@ -20,7 +21,7 @@ def pe_start_log() -> list[str]: return g.scodoc_pe_log -def pe_print(*a): +def pe_print(*a, **cles): "Log (or print in PE_DEBUG mode) and store in g" if PE_DEBUG: msg = " ".join(a) @@ -31,7 +32,8 @@ def pe_print(*a): lines = pe_start_log() msg = " ".join(a) lines.append(msg) - log(msg) + if "info" in cles: + log(msg) def pe_get_log() -> str: @@ -43,7 +45,7 @@ def pe_get_log() -> str: SANS_NOTE = "-" -def aff_profil_coeffs(matrice_coeffs_moy_gen, with_index=False): +def repr_profil_coeffs(matrice_coeffs_moy_gen, with_index=False): """Affiche les différents types de coefficients (appelés profil) d'une matrice_coeffs_moy_gen (pour debug) """ @@ -79,8 +81,20 @@ def aff_profil_coeffs(matrice_coeffs_moy_gen, with_index=False): return profils_aff +def repr_asso_ue_comp(acronymes_ues_to_competences): + """Représentation textuelle de l'association UE -> Compétences + fournies dans acronymes_ues_to_competences + """ + champs = acronymes_ues_to_competences.keys() + champs = sorted(champs) + aff_comp = [] + for acro in champs: + aff_comp += [f"📍{acro} (∈ 💡{acronymes_ues_to_competences[acro]})"] + return ", ".join(aff_comp) + + def aff_UEs(champs): - """Affiche les UEs""" + """Représentation textuelle des UEs fournies dans `champs`""" champs_tries = sorted(champs) aff_comp = [] @@ -99,8 +113,92 @@ def aff_competences(champs): return ", ".join(aff_comp) -def aff_tag(tags): +def repr_tags(tags): """Affiche les tags""" tags_tries = sorted(tags) aff_tag = ["👜" + tag for tag in tags_tries] return ", ".join(aff_tag) + + +def aff_tags_par_categories(dict_tags): + """Etant donné un dictionnaire de tags, triés + par catégorie (ici "personnalisés" ou "auto") + représentation textuelle 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 + aff_tags_auto = ", ".join([f"👜{nom}" for nom in noms_tags_auto]) + aff_tags_perso = ", ".join([f"👜{nom}" for nom in noms_tags_perso]) + # Affichage + return f"Tags du programme de formation : {aff_tags_perso} + Automatiques : {aff_tags_auto}" + + +def aff_trajectoires_suivies_par_etudiants(etudiants): + """Affiche les trajectoires (regroupement de (form)semestres) + amenant un étudiant du S1 à un semestre final""" + # Affichage pour debug + etudiants_ids = etudiants.etudiants_ids + jeunes = list(enumerate(etudiants_ids)) + for no_etud, etudid in jeunes: + etat = "⛔" if etudid in etudiants.abandons_ids else "✅" + + pe_print(f"--> {etat} {etudiants.identites[etudid].nomprenom} (#{etudid}) :") + trajectoires = etudiants.trajectoires[etudid] + for nom_rcs, rcs in trajectoires.items(): + if rcs: + pe_print(f" > RCS ⏯️{nom_rcs}: {rcs.get_repr()}") + + +def aff_semXs_suivis_par_etudiants(etudiants): + """Affiche les SemX (regroupement de semestres de type Sx) + amenant un étudiant à valider un Sx""" + etudiants_ids = etudiants.etudiants_ids + jeunes = list(enumerate(etudiants_ids)) + + for no_etud, etudid in jeunes: + etat = "⛔" if etudid in etudiants.abandons_ids else "✅" + pe_print(f"--> {etat} {etudiants.identites[etudid].nomprenom} :") + for nom_rcs, rcs in etudiants.semXs[etudid].items(): + if rcs: + pe_print(f" > SemX ⏯️{nom_rcs}: {rcs.get_repr()}") + + vides = [] + for nom_rcs in pe_rcs.TOUS_LES_SEMESTRES: + les_semX_suivis = [] + for no_etud, etudid in jeunes: + if etudiants.semXs[etudid][nom_rcs]: + les_semX_suivis.append(etudiants.semXs[etudid][nom_rcs]) + if not les_semX_suivis: + vides += [nom_rcs] + vides = sorted(list(set(vides))) + pe_print(f"⚠️ SemX sans données : {', '.join(vides)}") + + +def aff_capitalisations(etuds, ressembuttags, fid_final, acronymes_sorted, masque_df): + """Affichage des capitalisations du sxtag pour debug""" + aff_cap = [] + for etud in etuds: + cap = [] + for frmsem_id in ressembuttags: + if frmsem_id != fid_final: + for accr in acronymes_sorted: + if masque_df[frmsem_id].loc[etud.etudid, accr] > 0.0: + cap += [accr] + if cap: + aff_cap += [f" > {etud.nomprenom} : {', '.join(cap)}"] + if aff_cap: + pe_print(f"--> ⚠️ Capitalisations :") + pe_print("\n".join(aff_cap)) + + +def repr_comp_et_ues(acronymes_ues_to_competences): + """Affichage pour debug""" + aff_comp = [] + competences_sorted = sorted(acronymes_ues_to_competences.keys()) + for comp in competences_sorted: + liste = [] + for acro in acronymes_ues_to_competences: + if acronymes_ues_to_competences[acro] == comp: + liste += ["📍" + acro] + aff_comp += [f" 💡{comp} (⇔ {', '.join(liste)})"] + return "\n".join(aff_comp) diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 386807845..9e7fbb803 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -40,10 +40,10 @@ import pandas as pd from app import ScoValueError from app.models import FormSemestre, Identite, Formation from app.pe import pe_comp, pe_affichage +from app.pe.rcss import pe_rcs from app.scodoc import codes_cursus from app.scodoc import sco_utils as scu from app.comp.res_sem import load_formsemestre_results -import warnings class EtudiantsJuryPE: @@ -57,16 +57,17 @@ class EtudiantsJuryPE: self.annee_diplome = annee_diplome """L'année du diplôme""" - self.identites: dict[int, Identite] = {} # ex. ETUDINFO_DICT - "Les identités des étudiants traités pour le jury" + self.identites: dict[int:Identite] = {} # ex. ETUDINFO_DICT + """Les identités des étudiants traités pour le jury""" - self.cursus: dict[int, dict] = {} - "Les cursus (semestres suivis, abandons) des étudiants" + self.cursus: dict[int:dict] = {} + """Les cursus (semestres suivis, abandons) des étudiants""" - self.cursus = {} - """Les trajectoires/chemins de semestres suivis par les étudiants - pour atteindre un aggrégat donné - (par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements)""" + self.trajectoires: dict[int:dict] = {} + """Les trajectoires (regroupement cohérents de semestres) suivis par les étudiants""" + + self.semXs: dict[int:dict] = {} + """Les semXs (RCS de type Sx) suivis par chaque étudiant""" self.etudiants_diplomes = {} """Les identités des étudiants à considérer au jury (ceux qui seront effectivement @@ -101,27 +102,26 @@ class EtudiantsJuryPE: self.cosemestres = cosemestres pe_affichage.pe_print( - f"1) Recherche des cosemestres -> {len(cosemestres)} trouvés" + f"1) Recherche des cosemestres -> {len(cosemestres)} trouvés", info=True ) - pe_affichage.pe_print("2) Liste des étudiants dans les différents cosemestres") - self.etudiants_ids = get_etudiants_dans_semestres(cosemestres) pe_affichage.pe_print( - f" => {len(self.etudiants_ids)} étudiants trouvés dans les cosemestres" + "2) Liste des étudiants dans les différents cosemestres", info=True + ) + etudiants_ids = get_etudiants_dans_semestres(cosemestres) + pe_affichage.pe_print( + f" => {len(etudiants_ids)} étudiants trouvés dans les cosemestres", + info=True, ) # Analyse des parcours étudiants pour déterminer leur année effective de diplome # avec prise en compte des redoublements, des abandons, .... - pe_affichage.pe_print("3) Analyse des parcours individuels des étudiants") + pe_affichage.pe_print( + "3) Analyse des parcours individuels des étudiants", info=True + ) - for etudid in self.etudiants_ids: - self.identites[etudid] = Identite.get_etud(etudid) - - # Analyse son cursus - self.analyse_etat_etudiant(etudid, cosemestres) - - # Analyse son parcours pour atteindre chaque semestre de la formation - self.structure_cursus_etudiant(etudid) + # Ajoute une liste d'étudiants + self.add_etudiants(etudiants_ids) # Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris self.etudiants_diplomes = self.get_etudiants_diplomes() @@ -136,9 +136,10 @@ class EtudiantsJuryPE: # Les identifiants des étudiants ayant redoublés ou ayant abandonnés # Synthèse - pe_affichage.pe_print(f"4) Bilan") + pe_affichage.pe_print(f"4) Bilan", info=True) pe_affichage.pe_print( - f"--> {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}" + f"--> {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}", + info=True, ) nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes) assert nbre_abandons == len(self.abandons_ids) @@ -147,6 +148,24 @@ class EtudiantsJuryPE: f"--> {nbre_abandons} étudiants traités mais non diplômés (redoublement, réorientation, abandon)" ) + def add_etudiants(self, etudiants_ids): + """Ajoute une liste d'étudiants aux données du jury""" + nbre_etudiants_ajoutes = 0 + for etudid in etudiants_ids: + if etudid not in self.identites: + nbre_etudiants_ajoutes += 1 + + # L'identité de l'étudiant + self.identites[etudid] = Identite.get_etud(etudid) + + # Analyse son cursus + self.analyse_etat_etudiant(etudid, self.cosemestres) + + # Analyse son parcours pour atteindre chaque semestre de la formation + self.structure_cursus_etudiant(etudid) + self.etudiants_ids = set(self.identites.keys()) + return nbre_etudiants_ajoutes + def get_etudiants_diplomes(self) -> dict[int, Identite]: """Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}` qui vont être à traiter au jury PE pour @@ -215,11 +234,19 @@ class EtudiantsJuryPE: if formsemestre.formation.is_apc() } + # Le parcours final + parcour = formsemestres[0].etuds_inscriptions[etudid].parcour + if parcour: + libelle = parcour.libelle + else: + libelle = None + self.cursus[etudid] = { "etudid": etudid, # les infos sur l'étudiant "etat_civil": identite.etat_civil, # Ajout à la table jury "nom": identite.nom, "entree": formsemestres[-1].date_debut.year, # La date d'entrée à l'IUT + "parcours": libelle, # Le parcours final "diplome": get_annee_diplome( identite ), # Le date prévisionnelle de son diplôme @@ -234,8 +261,7 @@ class EtudiantsJuryPE: if self.cursus[etudid]["diplome"] == self.annee_diplome: # Est-il démissionnaire : charge son dernier semestre pour connaitre son état ? dernier_semes_etudiant = formsemestres[0] - with warnings.catch_warnings(): - res = load_formsemestre_results(dernier_semes_etudiant) + res = load_formsemestre_results(dernier_semes_etudiant) etud_etat = res.get_etud_etat(etudid) if etud_etat == scu.DEMISSION: self.cursus[etudid]["abandon"] = True @@ -245,29 +271,9 @@ class EtudiantsJuryPE: identite, cosemestres ) - def get_semestres_significatifs(self, etudid: int): - """Ensemble des semestres d'un étudiant, qui : - - * l'amènent à être diplômé à l'année visée - * l'auraient amené à être diplômé à l'année visée s'il n'avait pas redoublé et sera donc - diplômé plus tard - - Supprime les semestres qui conduisent à une diplomation postérieure à celle du jury visé. - - Args: - etudid: L'identifiant d'un étudiant - - Returns: - Un dictionnaire ``{fid: FormSemestre(fid)}`` dans lequel les semestres - amènent à une diplômation antérieur à celle de la diplômation visée par le jury jury - """ - semestres_etudiant = self.cursus[etudid]["formsemestres"] - semestres_significatifs = {} - for fid in semestres_etudiant: - semestre = semestres_etudiant[fid] - if pe_comp.get_annee_diplome_semestre(semestre) <= self.annee_diplome: - semestres_significatifs[fid] = semestre - return semestres_significatifs + # Initialise ses trajectoires/SemX/RCSemX + self.trajectoires[etudid] = {aggregat: None for aggregat in pe_rcs.TOUS_LES_RCS} + self.semXs[etudid] = {aggregat: None for aggregat in pe_rcs.TOUS_LES_SEMESTRES} def structure_cursus_etudiant(self, etudid: int): """Structure les informations sur les semestres suivis par un @@ -278,7 +284,9 @@ class EtudiantsJuryPE: le dernier semestre (en date) de numéro i qu'il a suivi (1 ou 0 si pas encore suivi). Ce semestre influera les interclassements par semestre dans la promo. """ - semestres_significatifs = self.get_semestres_significatifs(etudid) + semestres_significatifs = get_semestres_significatifs( + self.cursus[etudid]["formsemestres"], self.annee_diplome + ) # Tri des semestres par numéro de semestre for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1): @@ -381,13 +389,35 @@ class EtudiantsJuryPE: return df +def get_semestres_significatifs(formsemestres, annee_diplome): + """Partant d'un ensemble de semestre, renvoie les semestres qui amèneraient les étudiants + à être diplômé à l'année visée, y compris s'ils n'avaient pas redoublé et seraient donc + diplômé plus tard. + + De fait, supprime les semestres qui conduisent à une diplomation postérieure + à celle visée. + + Args: + formsemestres: une liste de formsemestres + annee_diplome: l'année du diplôme visée + + Returns: + Un dictionnaire ``{fid: FormSemestre(fid)}`` dans lequel les semestres + amènent à une diplômation antérieur à celle de la diplômation visée par le jury + """ + # semestres_etudiant = self.cursus[etudid]["formsemestres"] + semestres_significatifs = {} + for fid in formsemestres: + semestre = formsemestres[fid] + if pe_comp.get_annee_diplome_semestre(semestre) <= annee_diplome: + semestres_significatifs[fid] = semestre + return semestres_significatifs + + def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set: """Ensemble d'identifiants des étudiants (identifiés via leur ``etudid``) inscrits à l'un des semestres de la liste de ``semestres``. - Remarque : Les ``cosemestres`` sont généralement obtenus avec - ``sco_formsemestre.do_formsemestre_list()`` - Args: semestres: Un dictionnaire ``{fid: Formsemestre(fid)}`` donnant un ensemble d'identifiant de semestres diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index e47442ee6..aff6b6b62 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -65,6 +65,7 @@ from app.pe.moys import ( pe_moytag, ) import app.pe.pe_rcss_jury as pe_rcss_jury +from app.scodoc.sco_utils import * class JuryPE(object): @@ -93,7 +94,7 @@ class JuryPE(object): pe_affichage.pe_print( f"""***********************************************************\n""" f"""*** Recherche des étudiants diplômés 🎓 en {self.diplome}\n""" - f"""***********************************************************\n""" + f"""***********************************************************""" ) # Les infos sur les étudiants @@ -102,7 +103,7 @@ class JuryPE(object): self.etudiants.find_etudiants() self.diplomes_ids = self.etudiants.diplomes_ids - self.rcss_jury = pe_rcss_jury.RCSsJuryPE(self.diplome) + self.rcss_jury = pe_rcss_jury.RCSsJuryPE(self.diplome, self.etudiants) """Les informations sur les regroupements de semestres""" self.zipdata = io.BytesIO() @@ -168,7 +169,16 @@ class JuryPE(object): self.ressembuttags = {} for frmsem_id, formsemestre in formsemestres.items(): # Crée le semestre_tag et exécute les calculs de moyennes - self.ressembuttags[frmsem_id] = pe_ressemtag.ResSemBUTTag(formsemestre) + ressembuttag = pe_ressemtag.ResSemBUTTag(formsemestre) + self.ressembuttags[frmsem_id] = ressembuttag + # Ajoute les étudiants découverts dans les ressembuttags aux données des étudiants + # nbre_etudiants_ajoutes = self.etudiants.add_etudiants( + # ressembuttag.etudids_sorted + # ) + # if nbre_etudiants_ajoutes: + # pe_affichage.pe_print( + # f"--> Ajout de {nbre_etudiants_ajoutes} étudiants aux données du jury" + # ) # Intègre le bilan des semestres taggués au zip final pe_affichage.pe_print(f"2) Bilan") @@ -178,14 +188,19 @@ class JuryPE(object): ) as writer: onglets = [] for res_sem_tag in self.ressembuttags.values(): - onglet = res_sem_tag.get_repr(verbose=True) - onglet = onglet.replace("Semestre ", "S") - onglets += ["📊" + onglet] - df = res_sem_tag.to_df() - # Conversion colonnes en multiindex - df = convert_colonnes_to_multiindex(df) - # écriture dans l'onglet - df.to_excel(writer, onglet, index=True, header=True) + for pole in [None, ModuleType.RESSOURCE, ModuleType.SAE]: + onglet = res_sem_tag.get_repr(verbose=True) + onglet = onglet.replace("Semestre ", "S") + if pole: + onglet += ( + " (res.)" if pole == ModuleType.RESSOURCE else " (saes)" + ) + onglets += ["📊" + onglet] + df = res_sem_tag.to_df(pole) + # Conversion colonnes en multiindex + df = convert_colonnes_to_multiindex(df) + # écriture dans l'onglet + df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"--> Export excel de {', '.join(onglets)}") output.seek(0) @@ -207,8 +222,8 @@ class JuryPE(object): "***************************************************************************" ) - self.rcss_jury.cree_trajectoires(self.etudiants) - self.rcss_jury._aff_trajectoires(self.etudiants) + self.rcss_jury.cree_trajectoires() + pe_affichage.aff_trajectoires_suivies_par_etudiants(self.etudiants) def _gen_semXs(self): """Génère les SemXs (trajectoires/combinaisons de semestre de même rang x) @@ -218,8 +233,8 @@ class JuryPE(object): pe_affichage.pe_print( "*** Génère les SemXs (RCS de même Sx donnant lieu à validation du semestre)" ) - self.rcss_jury.cree_semxs(self.etudiants) - self.rcss_jury._aff_semxs_suivis(self.etudiants) + self.rcss_jury.cree_semxs() + pe_affichage.aff_semXs_suivis_par_etudiants(self.etudiants) def _gen_xls_sxtags(self, zipfile: ZipFile): """Génère les semestres taggués en s'appuyant sur les RCF de type Sx (pour @@ -248,14 +263,20 @@ class JuryPE(object): ) as writer: onglets = [] for sxtag in self.sxtags.values(): - onglet = sxtag.get_repr(verbose=False) if sxtag.is_significatif(): - df = sxtag.to_df() - # Conversion colonnes en multiindex - df = convert_colonnes_to_multiindex(df) - onglets += ["📊" + onglet] - # écriture dans l'onglet - df.to_excel(writer, onglet, index=True, header=True) + for pole in [None, ModuleType.RESSOURCE, ModuleType.SAE]: + onglet = sxtag.get_repr(verbose=False) + if pole: + onglet += ( + " (res.)" if pole == ModuleType.RESSOURCE else " (saes)" + ) + onglets += ["📊" + onglet] + df = sxtag.to_df(pole) + # Conversion colonnes en multiindex + df = convert_colonnes_to_multiindex(df) + + # écriture dans l'onglet + df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"--> Export excel de {', '.join(onglets)}") output.seek(0) @@ -320,14 +341,21 @@ class JuryPE(object): ) as writer: onglets = [] for rcs_tag in self.rcsstags.values(): - onglet = rcs_tag.get_repr(verbose=False) if rcs_tag.is_significatif(): - df = rcs_tag.to_df() - # Conversion colonnes en multiindex - df = convert_colonnes_to_multiindex(df) - onglets += ["📊" + onglet] - # écriture dans l'onglet - df.to_excel(writer, onglet, index=True, header=True) + for pole in [None, ModuleType.RESSOURCE, ModuleType.SAE]: + onglet = rcs_tag.get_repr(verbose=False) + if pole: + onglet += ( + " (res.)" if pole == ModuleType.RESSOURCE else " (saes)" + ) + onglets += ["📊" + onglet] + + df = rcs_tag.to_df(pole) + # Conversion colonnes en multiindex + df = convert_colonnes_to_multiindex(df) + onglets += ["📊" + onglet] + # écriture dans l'onglet + df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"--> Export excel de {', '.join(onglets)}") output.seek(0) @@ -393,14 +421,22 @@ class JuryPE(object): ]: interclasstag = self.interclasstags[type_interclass] for nom_rcs, interclass in interclasstag.items(): - onglet = interclass.get_repr() if interclass.is_significatif(): - df = interclass.to_df(cohorte="Promo") - # Conversion colonnes en multiindex - df = convert_colonnes_to_multiindex(df) - onglets += [onglet] - # écriture dans l'onglet - df.to_excel(writer, onglet, index=True, header=True) + for pole in [None, ModuleType.RESSOURCE, ModuleType.SAE]: + onglet = interclass.get_repr() + if pole: + onglet += ( + " (res.)" + if pole == ModuleType.RESSOURCE + else " (saes)" + ) + onglets += ["📊" + onglet] + df = interclass.to_df(pole, cohorte="Promo") + # Conversion colonnes en multiindex + df = convert_colonnes_to_multiindex(df) + onglets += [onglet] + # écriture dans l'onglet + df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) @@ -428,7 +464,10 @@ class JuryPE(object): tags = self._do_tags_list(self.interclasstags) for tag in tags: for type_moy in [pe_moytag.CODE_MOY_UE, pe_moytag.CODE_MOY_COMPETENCES]: - self.synthese[(tag, type_moy)] = self.df_tag_type(tag, type_moy) + for pole in [None, ModuleType.RESSOURCE, ModuleType.SAE]: + self.synthese[(tag, type_moy, pole)] = self.df_tag_type( + tag, type_moy, pole + ) # Export des données => mode 1 seule feuille -> supprimé pe_affichage.pe_print("*** Export du jury de synthese par tags") @@ -443,10 +482,18 @@ class JuryPE(object): df_final = convert_colonnes_to_multiindex(df_final) # Nom de l'onglet if isinstance(onglet, tuple): - if onglet[1] == pe_moytag.CODE_MOY_COMPETENCES: - nom_onglet = onglet[0][: 31 - 7] + " (Comp)" + (repr, type_moy, pole) = onglet + nom_onglet = onglet[0][: 31 - 11] + if type_moy == pe_moytag.CODE_MOY_COMPETENCES: + nom_onglet = nom_onglet + "(Comp" else: - nom_onglet = onglet[0][: 31 - 5] + " (UE)" + nom_onglet = nom_onglet + "(UE" + if pole and pole == ModuleType.RESSOURCE: + nom_onglet = nom_onglet + ",res)" + elif pole and pole == ModuleType.SAE: + nom_onglet = nom_onglet + ",saes)" + else: + nom_onglet = nom_onglet + ")" else: nom_onglet = onglet onglets += [nom_onglet] @@ -515,7 +562,7 @@ class JuryPE(object): # Méthodes pour la synthèse du juryPE # ***************************************************************************************************************** - def df_tag_type(self, tag, type_moy): + def df_tag_type(self, tag, type_moy, pole): """Génère le DataFrame synthétisant les moyennes/classements (groupe + interclassement promo) pour tous les aggrégats prévus, en fonction du type (UEs ou Compétences) de données souhaitées, @@ -551,7 +598,7 @@ class JuryPE(object): if interclass.is_significatif(): # Le dataframe du classement sur le groupe df_groupe = interclass.compute_df_synthese_moyennes_tag( - tag, aggregat=aggregat, type_colonnes=False + tag, pole, aggregat=aggregat, type_colonnes=False ) if not df_groupe.empty: aff_aggregat += [aggregat] @@ -559,6 +606,7 @@ class JuryPE(object): # Le dataframe du classement sur la promo df_promo = interclass.to_df( + pole, administratif=False, aggregat=aggregat, tags_cibles=[tag], @@ -571,8 +619,14 @@ class JuryPE(object): if aff_aggregat: aff_aggregat = sorted(set(aff_aggregat)) + if pole and pole == ModuleType.RESSOURCE: + aff_pole = "et par ressources" + elif pole and pole == ModuleType.SAE: + aff_pole = "et par saes" + else: + aff_pole = "tous modules confondus" pe_affichage.pe_print( - f" -> Synthèse de 👜{tag} par {type_moy} avec {', '.join(aff_aggregat)}" + f" -> Synthèse de 👜{tag} par {type_moy} {aff_pole} avec {', '.join(aff_aggregat)}" ) else: pe_affichage.pe_print(f" -> Synthèse du tag {tag} par {type_moy} : ") @@ -587,6 +641,8 @@ class JuryPE(object): Returns: Un tuple nom, prenom, html """ + pole = None + etudiant = self.etudiants.identites[etudid] nom = etudiant.nom prenom = etudiant.prenom # initial du prénom @@ -608,57 +664,58 @@ class JuryPE(object): tags = self._do_tags_list(self.interclasstags) - # Descripti - # Les données par UE moyennes = {} for tag in tags: moyennes[tag] = {} # Les données de synthèse - df = self.synthese[(tag, pe_moytag.CODE_MOY_COMPETENCES)] + df = self.synthese[(tag, pe_moytag.CODE_MOY_COMPETENCES, pole)] for aggregat in pe_rcs.TOUS_LES_RCS: - moyennes[tag][aggregat] = {} + # moyennes[tag][aggregat] = {} + descr = pe_rcs.get_descr_rcs(aggregat) + + moy = {} + est_significatif = False for comp in competences + ["Général"]: - moyennes[tag][aggregat][comp] = { + moy[comp] = { "note": "", "rang_groupe": "", "rang_promo": "", } colonne = pe_moytag.get_colonne_df( - aggregat, tag, comp, "Groupe", "note" + aggregat, pole, tag, comp, "Groupe", "note" ) if colonne in df.columns: valeur = df.loc[etudid, colonne] if not np.isnan(valeur): - moyennes[tag][aggregat][comp]["note"] = round(valeur, 2) + moy[comp]["note"] = round(valeur, 2) + est_significatif = True colonne = pe_moytag.get_colonne_df( - aggregat, tag, comp, "Groupe", "rang" + aggregat, pole, tag, comp, "Groupe", "rang" ) if colonne in df.columns: valeur = df.loc[etudid, colonne] if valeur and str(valeur) != "nan": - moyennes[tag][aggregat][comp]["rang_groupe"] = valeur + moy[comp]["rang_groupe"] = valeur colonne = pe_moytag.get_colonne_df( - aggregat, tag, comp, "Promo", "rang" + aggregat, pole, tag, comp, "Promo", "rang" ) if colonne in df.columns: valeur = df.loc[etudid, colonne] if valeur and str(valeur) != "nan": - moyennes[tag][aggregat][comp]["rang_promo"] = valeur + moy[comp]["rang_promo"] = valeur + + if est_significatif: + moyennes[tag][descr] = moy html = template.render( nom=nom, prenom=prenom, + pole="Moyennes calculées tous modules (ressources/SAEs) confondus", colonnes_html=colonnes_html, tags=tags, moyennes=moyennes, ) - # for onglet, df_synthese in self.synthese.items(): - # if isinstance(onglet, tuple): # Les onglets autres que "administratif" - # tag = onglet[0] - # type_moy = onglet[1] - - # colonnes = list(df_synthese.columns) return (nom, prenom, html) diff --git a/app/pe/pe_rcss_jury.py b/app/pe/pe_rcss_jury.py index 9cf8ab298..99003c2fd 100644 --- a/app/pe/pe_rcss_jury.py +++ b/app/pe/pe_rcss_jury.py @@ -14,10 +14,13 @@ class RCSsJuryPE: annee_diplome: L'année de diplomation """ - def __init__(self, annee_diplome: int): + def __init__(self, annee_diplome: int, etudiants: pe_etudiant.EtudiantsJuryPE): self.annee_diplome = annee_diplome """Année de diplômation""" + self.etudiants = etudiants + """Les étudiants recensés""" + self.trajectoires: dict[tuple(int, str) : pe_trajectoires.Trajectoire] = {} """Ensemble des trajectoires recensées (regroupement de (form)semestres BUT)""" @@ -41,7 +44,7 @@ class RCSsJuryPE: """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, son RCSemX : {etudid: {nom_RCS: RCSemX}}""" - def cree_trajectoires(self, etudiants: pe_etudiant.EtudiantsJuryPE): + def cree_trajectoires(self): """Créé toutes les trajectoires, au regard du cursus des étudiants analysés + les mémorise dans les données de l'étudiant @@ -49,31 +52,28 @@ class RCSsJuryPE: etudiants: Les étudiants à prendre en compte dans le Jury PE """ - tous_les_aggregats = ( - pe_rcs.TOUS_LES_SEMESTRES + pe_rcs.TOUS_LES_RCS_AVEC_PLUSIEURS_SEM - ) - for etudid in etudiants.cursus: - self.trajectoires_suivies[etudid] = { - aggregat: None for aggregat in tous_les_aggregats - } + tous_les_aggregats = pe_rcs.TOUS_LES_RCS + + for etudid in self.etudiants.cursus: + self.trajectoires_suivies[etudid] = self.etudiants.trajectoires[etudid] 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 = pe_rcs.TYPES_RCS[nom_rcs]["aggregat"] - nom_semestre_terminal = noms_semestre_de_aggregat[-1] + noms_semestres = pe_rcs.TYPES_RCS[nom_rcs]["aggregat"] + nom_semestre_final = noms_semestres[-1] - for etudid in etudiants.cursus: - # Le formsemestre terminal (dernier en date) associé au - # semestre marquant la fin de l'aggrégat - # (par ex: son dernier S3 en date) - trajectoire = etudiants.cursus[etudid][nom_semestre_terminal] - if trajectoire: + for etudid in self.etudiants.cursus: + # Le (ou les) semestre(s) marquant la fin du cursus de l'étudiant + sems_final = self.etudiants.cursus[etudid][nom_semestre_final] + if sems_final: + # Le formsemestre final (dernier en date) de l'étudiant, + # marquant la fin de son aggrégat (par ex: son dernier S3 en date) formsemestre_final = app.pe.pe_comp.get_dernier_semestre_en_date( - trajectoire + sems_final ) - # Ajout ou récupération du RCS associé + # Ajout (si nécessaire) et récupération du RCS associé rcs_id = (nom_rcs, formsemestre_final.formsemestre_id) if rcs_id not in self.trajectoires: self.trajectoires[rcs_id] = pe_trajectoires.Trajectoire( @@ -84,7 +84,7 @@ class RCSsJuryPE: # La liste des semestres de l'étudiant à prendre en compte # pour cette trajectoire semestres_a_aggreger = get_rcs_etudiant( - etudiants.cursus[etudid], formsemestre_final, nom_rcs + self.etudiants.cursus[etudid], formsemestre_final, nom_rcs ) # Ajout des semestres au RCS @@ -92,64 +92,30 @@ class RCSsJuryPE: # Mémorise le RCS suivi par l'étudiant self.trajectoires_suivies[etudid][nom_rcs] = rcs + self.etudiants.trajectoires[etudid][nom_rcs] = rcs - def _aff_trajectoires(self, etudiants: pe_etudiant.EtudiantsJuryPE): - """Affiche les chemins trouvés pour debug""" - # Affichage pour debug - jeunes = list(enumerate(self.trajectoires_suivies)) - for no_etud, etudid in jeunes: - etat = "⛔" if etudid in etudiants.abandons_ids else "✅" - - pe_affichage.pe_print( - f"--> {etat} {etudiants.identites[etudid].nomprenom} (#{etudid}) :" - ) - for nom_rcs, rcs in self.trajectoires_suivies[etudid].items(): - if rcs: - pe_affichage.pe_print(f" > RCS ⏯️{nom_rcs}: {rcs.get_repr()}") - - def cree_semxs(self, etudiants: pe_etudiant.EtudiantsJuryPE): - """Créé les les SemXs (trajectoires/combinaisons de semestre de même rang x), + def cree_semxs(self): + """Créé les SemXs (trajectoires/combinaisons de semestre de même rang x), en ne conservant dans les trajectoires que les regroupements de type Sx""" self.semXs = {} for rcs_id, trajectoire in self.trajectoires.items(): - if trajectoire and trajectoire.nom in pe_rcs.TOUS_LES_SEMESTRES: + if trajectoire.nom in pe_rcs.TOUS_LES_SEMESTRES: self.semXs[rcs_id] = pe_trajectoires.SemX(trajectoire) + # L'association (pour chaque étudiant entre chaque Sx et le SemX associé) self.semXs_suivis = {} - for etudid in self.trajectoires_suivies: + for etudid in self.etudiants.trajectoires: self.semXs_suivis[etudid] = { - nom_rcs: None for nom_rcs in pe_rcs.TOUS_LES_SEMESTRES + agregat: None for agregat in pe_rcs.TOUS_LES_SEMESTRES } - - for nom_rcs, trajectoire in self.trajectoires_suivies[etudid].items(): - if trajectoire and nom_rcs in pe_rcs.TOUS_LES_SEMESTRES: + for agregat in pe_rcs.TOUS_LES_SEMESTRES: + trajectoire = self.etudiants.trajectoires[etudid][agregat] + if trajectoire: rcs_id = trajectoire.rcs_id - self.semXs_suivis[etudid][nom_rcs] = self.semXs[rcs_id] - - def _aff_semxs_suivis(self, etudiants: pe_etudiant.EtudiantsJuryPE): - """Affichage des SemX pour debug""" - jeunes = list(enumerate(self.semXs_suivis)) - - for no_etud, etudid in jeunes: - etat = "⛔" if etudid in etudiants.abandons_ids else "✅" - pe_affichage.pe_print( - f"--> {etat} {etudiants.identites[etudid].nomprenom} :" - ) - for nom_rcs, rcs in self.semXs_suivis[etudid].items(): - if rcs: - pe_affichage.pe_print(f" > SemX ⏯️{nom_rcs}: {rcs.get_repr()}") - - vides = [] - for nom_rcs in pe_rcs.TOUS_LES_SEMESTRES: - les_semX_suivis = [] - for no_etud, etudid in jeunes: - if self.semXs_suivis[etudid][nom_rcs]: - les_semX_suivis.append(self.semXs_suivis[etudid][nom_rcs]) - if not les_semX_suivis: - vides += [nom_rcs] - vides = sorted(list(set(vides))) - pe_affichage.pe_print(f"⚠️ SemX sans données : {', '.join(vides)}") + semX = self.semXs[rcs_id] + self.semXs_suivis[etudid][agregat] = semX + self.etudiants.semXs[etudid][agregat] = semX def cree_rcsemxs(self, etudiants: pe_etudiant.EtudiantsJuryPE): """Créé tous les RCSemXs, au regard du cursus des étudiants diff --git a/app/templates/pe/pe_view_resultats_etudiant.j2 b/app/templates/pe/pe_view_resultats_etudiant.j2 index d746eca27..f72e2cf10 100644 --- a/app/templates/pe/pe_view_resultats_etudiant.j2 +++ b/app/templates/pe/pe_view_resultats_etudiant.j2 @@ -4,19 +4,36 @@ PE de {{ nom }} - -
-

Résultats PE de {{prenom}} {{nom}}

+ +

Résultats PE de {{prenom}} {{nom}}

+ +

Légende

+
    +
  • ../.. Classement par groupe
  • +
  • ../.. Classement par promo
  • +
+ + {{ pole }} {% for tag in tags %}