# -*- 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 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.nom_rcs = sxtag_id[0] 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 = ["👜" + tag for tag in self.tags_sorted] pe_affichage.pe_print(f"--> Tags : {', '.join(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""" self._aff_ue_et_comp_debug() # 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() # 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, ) self._aff_capitalisations() # 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") matrice_moys_ues = 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, ) # 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, ) # Affichage de debug self.__aff_profil_coeff_ects(tag) # 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, 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) """ # 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): """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] 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 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.nom_rcs}#{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 _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)) 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 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 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 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.fillna(np.nan) return etud_moy_tag_df