# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## ############################################################################## # Module "Avis de poursuite d'étude" # conçu et développé par Cléo Baras (IUT de Grenoble) ############################################################################## """ Created on Fri Sep 9 09:15:05 2016 @author: barasc """ from app.pe import pe_affichage, pe_comp import app.pe.moys.pe_ressemtag as pe_ressemtag import pandas as pd 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): def __init__( self, sxtag_id: (str, int), semx: pe_trajectoires.SemX, ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag], ): """Calcule les moyennes/classements par tag d'un semestre de type 'Sx' (par ex. 'S1', 'S2', ...) représentés par acronyme d'UE. Il représente : * pour les étudiants *non redoublants* : moyennes/classements du semestre suivi * pour les étudiants *redoublants* : une fusion des moyennes/classements dans les (2) 'Sx' qu'il a suivi, en exploitant les informations de capitalisation : meilleure moyenne entre l'UE capitalisée et l'UE refaite (la notion de meilleure s'appliquant à la moyenne d'UE) Un SxTag (regroupant potentiellement plusieurs semestres) est identifié par un tuple ``(Sx, fid)`` où : * ``x`` est le rang (semestre_id) du semestre * ``fid`` le formsemestre_id du semestre final (le plus récent) du regroupement. Les **tags**, les **UE** et les inscriptions aux UEs (pour les étudiants) considérés sont uniquement ceux du semestre final. Args: sxtag_id: L'identifiant de SxTag ressembuttags: Un dictionnaire de la forme `{fid: ResSemBUTTag(fid)}` donnant les semestres à regrouper et les résultats/moyennes par tag des semestres """ pe_tabletags.TableTag.__init__(self) assert sxtag_id and len(sxtag_id) == 2 and sxtag_id[1] in ressembuttags self.sxtag_id: (str, int) = sxtag_id """Identifiant du SxTag de la forme (nom_Sx, fid_semestre_final)""" assert ( len(self.sxtag_id) == 2 and isinstance(self.sxtag_id[0], str) and isinstance(self.sxtag_id[1], int) ), "Format de l'identifiant du SxTag non respecté" self.agregat = sxtag_id[0] """Nom de l'aggrégat du RCS""" self.semx = semx """Le SemX sur lequel il s'appuie""" assert semx.rcs_id == sxtag_id, "Problème de correspondance SxTag/SemX" # Les resultats des semestres taggués à prendre en compte dans le SemX self.ressembuttags = { fid: ressembuttags[fid] for fid in semx.semestres_aggreges } """Les ResSemBUTTags à regrouper dans le SxTag""" # Les données du semestre final self.fid_final = sxtag_id[1] self.ressembuttag_final = ressembuttags[self.fid_final] """Le ResSemBUTTag final""" # Ajoute les etudids et les états civils self.etuds = self.ressembuttag_final.etuds """Les étudiants (extraits du ReSemBUTTag final)""" self.add_etuds(self.etuds) self.etudids_sorted = sorted(self.etudids) """Les etudids triés""" # Affichage pe_affichage.pe_print(f"*** {self.get_repr(verbose=True)}") # Les tags self.tags_sorted = self.ressembuttag_final.tags_sorted """Tags (extraits du ReSemBUTTag final)""" aff_tag = pe_affichage.repr_tags(self.tags_sorted) pe_affichage.pe_print(f"--> Tags : {aff_tag}") # Les UE données par leur acronyme self.acronymes_sorted = self.ressembuttag_final.acronymes_sorted """Les acronymes des UEs (extraits du ResSemBUTTag final)""" # L'association UE-compétences extraites du dernier semestre self.acronymes_ues_to_competences = ( self.ressembuttag_final.acronymes_ues_to_competences ) """L'association acronyme d'UEs -> compétence""" self.competences_sorted = sorted(self.acronymes_ues_to_competences.values()) """Les 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 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""" 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 """Le DataFrame traduisant les capitalisations des différents semestres""" self.masque_df, masque_cube = compute_masques_capitalisation_cube( self.etudids_sorted, self.acronymes_sorted, self.ressembuttags, self.fid_final, ) 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: 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 ) else: # 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) # DataFrame des moyennes (tous modules confondus) matrice_moys_ues = self.compute_notes_ues( notes_cube_gen, masque_cube, inscr_mask ) # Mémorise les infos pour la moyenne au tag self.moyennes_tags[tag] = pe_moytag.MoyennesTag( tag, pe_moytag.CODE_MOY_UE, matrice_moys_ues, self.matrice_coeffs_moy_gen, ) # 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}") 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. Args: tag: Le tag visé Returns: True si a des notes, False sinon """ moy_tag_dernier_sem = self.ressembuttag_final.moyennes_tags[tag] return moy_tag_dernier_sem.has_notes() def __eq__(self, other): """Egalité de 2 SxTag sur la base de leur identifiant""" return self.sxtag_id == other.sxtag_id def get_repr(self, verbose=False) -> str: """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle est basée)""" if verbose: return f"SXTag basé sur {self.semx.get_repr()}" else: # affichage = [str(fid) for fid in self.ressembuttags] return f"SXTag {self.agregat}#{self.fid_final}" def compute_notes_ues_cube(self, tag) -> (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: 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()) dfs = {} 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] 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) return etud_moy_tag_df def compute_masques_capitalisation_cube( etudids_sorted: list[int], acronymes_sorted: list[str], ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag], formsemestre_id_final: int, ) -> (pd.DataFrame, np.array): """Construit le cube traduisant les masques des UEs à prendre en compte dans le calcul des moyennes, en utilisant le dataFrame de capitalisations de chaque ResSemBUTTag Ces masques contiennent : 1 si la note doit être prise en compte, 0 sinon Le masque des UEs à prendre en compte correspondant au semestre final (identifié par son formsemestre_id_final) est systématiquement à 1 (puisque les résultats de ce semestre doivent systématiquement être pris en compte notamment pour les étudiants non redoublant). 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) formsemestre_id_final: L'identifiant du formsemestre_id_final """ # Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2) # etudids_sorted = etudids_sorted # acronymes_ues = sorted([ue.acronyme for ue in selMf.ues.values()]) semestres_id = list(ressembuttags.keys()) dfs = {} for frmsem_id in semestres_id: # Partant d'un dataframe contenant des 1.0 if frmsem_id == formsemestre_id_final: df = pd.DataFrame(1.0, index=etudids_sorted, columns=acronymes_sorted) else: # semestres redoublés df = pd.DataFrame(0.0, index=etudids_sorted, columns=acronymes_sorted) # Traitement des capitalisations : remplace les infos de capitalisations par les coeff 1 ou 0 capitalisations = ressembuttags[frmsem_id].capitalisations capitalisations = capitalisations.replace(True, 1.0).replace(False, 0.0) # Met à 0 les coeffs des UEs non capitalisées pour les étudiants # inscrits dans les 2 semestres: 1.0*False => 0.0 etudids_communs, acronymes_communs = pe_comp.find_index_and_columns_communs( df, capitalisations ) df.loc[etudids_communs, acronymes_communs] = capitalisations.loc[ etudids_communs, acronymes_communs ] # 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