# -*- 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.pe_ressemtag as pe_ressemtag import pandas as pd import numpy as np from app.pe.pe_tabletags import TableTag from app.pe.pe_moytag import MoyennesTag import app.pe.rcss.pe_rcf as pe_rcf class SxTag(TableTag): def __init__( self, sxtag_id: (str, int), rcf: pe_rcf.RCF, ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag], ): """Calcule les moyennes/classements par tag d'un semestre de type 'Sx' (par ex. 'S1', 'S2', ...) avec une orientation par UE : * pour les étudiants non redoublants, ce sont les moyennes/classements du semestre suivi * pour les étudiants redoublants, c'est une fusion des moyennes/classements dans les (2) 'Sx' qu'il a suivi Un SxTag peut donc regrouper plusieurs semestres. Un SxTag est identifié par un tuple (x, fid) où x est le numéro (semestre_id) du semestre et fid le formsemestre_id du semestre final (le plus récent) du regrouprement. Les **tags**, les **UE** et les inscriptions aux UEs (pour les etudiants) 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 """ 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)""" self.rcf = rcf """Le RCF sur lequel il s'appuie""" # Les resultats des semestres taggués à prendre en compte dans le RCF self.ressembuttags = {fid: ressembuttags[fid] for fid in rcf.semestres_aggreges} """Les ResSemBUTTags à regrouper dans le SxTag""" # Les données du semestre final self.fid_final = sxtag_id[1] self.ressembuttag_final = ressembuttags[self.fid_final] """Le ResSemBUTTag final""" self.etuds = ressembuttags[self.fid_final].etuds """Les étudiants du ReSemBUTTag final""" # Ajout les etudids et les états civils self.add_etuds(self.etuds) self.etudids_sorted = sorted(self.etudids) """Les etudids triés""" # Affichage pe_affichage.pe_print(f"--> {self.get_repr()}") # Les tags self.tags_sorted = self.ressembuttag_final.tags_sorted """Tags (extraits uniquement du semestre final)""" pe_affichage.pe_print(f"* Tags : {', '.join(self.tags_sorted)}") # Les UE moy_sem_final = self.ressembuttag_final.moyennes_tags["but"] self.ues = list(moy_sem_final.matrice_notes.columns) # L'association UE-compétences extraites du dernier semestre self.competences = self.ressembuttag_final.competences # Les acronymes des UE self.acronymes_ues_sorted = sorted(self.ues) # Les inscriptions des étudiants aux UEs # => ne conserve que les UEs du semestre final (pour les redoublants) self.ues_inscr_parcours_df = self.ressembuttag_final.ues_inscr_parcours_df self.ues_inscr_parcours_df.sort_index() # Les coeffs pour la moyenne générale self.matrice_coeffs_moy_gen = self.ressembuttag_final.moyennes_tags[ "but" ].matrice_coeffs_moy_gen self.matrice_coeffs_moy_gen.sort_index() # Trie les coeff par etudids # Les moyennes par tag self.moyennes_tags: dict[str, pd.DataFrame] = {} """Les notes aux UEs dans différents tags""" # Masque des inscriptions et des capitalisations self.masque_df, masque_cube = compute_masques_ues_cube( self.etudids_sorted, self.acronymes_ues_sorted, self.ressembuttags, self.fid_final, ) self._aff_capitalisations() 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_ues_sorted) else: # Cube de note etudids x UEs notes_df, notes_cube = compute_notes_ues_cube( tag, self.etudids_sorted, self.acronymes_ues_sorted, self.ressembuttags, ) # self.ues_inscr_parcours = ~np.isnan(self.matrice_coeffs.to_numpy()) # inscr_mask = self.ues_inscr_parcours # Calcule des moyennes sous forme d'un dataframe inscr_mask = ~np.isnan(self.ues_inscr_parcours_df.to_numpy()) matrice_moys_ues: pd.DataFrame = compute_notes_ues( notes_cube, masque_cube, self.etudids_sorted, self.acronymes_ues_sorted, inscr_mask, ) # Les profils d'ects (pour debug) profils_ects = [] for i in self.matrice_coeffs_moy_gen.index: val = tuple(self.matrice_coeffs_moy_gen.loc[i].fillna("x")) if tuple(val) not in profils_ects: profils_ects.append(tuple(val)) pe_affichage.pe_print( f"> MoyTag 🏷{tag} avec " + f"ues={self.acronymes_ues_sorted} " + f"ects={profils_ects}" ) # Les moyennes au tag self.moyennes_tags[tag] = MoyennesTag( tag, matrice_moys_ues, self.matrice_coeffs_moy_gen ) 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"{self.sxtag_id[0]}Tag basé sur {self.rcf.get_repr()}" else: # affichage = [str(fid) for fid in self.ressembuttags] return f"{self.sxtag_id[0]}Tag (#{self.fid_final})" def _aff_capitalisations(self): """Affichage des capitalisations du sxtag pour debug""" for etud in self.etuds: cap = [] for frmsem_id in self.ressembuttags: if frmsem_id != self.fid_final: for accr in self.acronymes_ues_sorted: if self.masque_df[frmsem_id].loc[etud.etudid, accr] > 0.0: cap += [accr] if cap: pe_affichage.pe_print( f" ⚠ Capitalisation de {etud.etat_civil} : {', '.join(cap)}" ) def compute_notes_ues_cube( tag, etudids_sorted, acronymes_ues_sorted, ressembuttags ) -> (pd.DataFrame, np.array): """Construit le cube de notes des UEs (etudid x accronyme_ue x semestre_aggregé) nécessaire au calcul des moyennes du tag pour le RCS Sx. (Renvoie également le dataframe associé pour debug). Args: etudids_sorted: La liste des etudids triés par ordre croissant (dim 0) acronymes_ues_sorted: La liste des acronymes de UEs triés par acronyme croissant (dim 1) ressembuttags: Le dictionnaire des résultats de semestres BUT (tous tags confondus) """ # Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2) # etudids_sorted = etudids_sorted # acronymes_ues = sorted([ue.acronyme for ue in selMf.ues.values()]) semestres_id = list(ressembuttags.keys()) dfs = {} for frmsem_id in semestres_id: # Partant d'un dataframe vierge df = pd.DataFrame(np.nan, index=etudids_sorted, columns=acronymes_ues_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_ues_cube( etudids_sorted: list[int], acronymes_ues_sorted: list[str], ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag], formsemestre_id_final: int, ) -> (pd.DataFrame, np.array): """Construit le cube traduisant le masque des UEs à prendre en compte dans le calcul des moyennes, en utilisant le df capitalisations de chaque ResSemBUTTag Ce masque contient : 1 si la note doit être prise en compte ; 0 sinon Args: etudids_sorted: La liste des etudids triés par ordre croissant (dim 0) acronymes_ues_sorted: La liste des acronymes de UEs triés par acronyme croissant (dim 1) # ues_inscr_parcours_df: Le dataFrame des inscriptions au UE en fonction du parcours ressembuttags: Le dictionnaire des résultats de semestres BUT (tous tags confondus) formsemestre_id_final: L'identifiant du formsemestre_id_final (dont il faut forcément prendre en compte les coeffs) """ # Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2) # etudids_sorted = etudids_sorted # acronymes_ues = sorted([ue.acronyme for ue in selMf.ues.values()]) semestres_id = list(ressembuttags.keys()) dfs = {} for frmsem_id in semestres_id: # Partant d'un dataframe contenant des 1.0 if frmsem_id == formsemestre_id_final: df = pd.DataFrame(1.0, index=etudids_sorted, columns=acronymes_ues_sorted) else: # semestres redoublés df = pd.DataFrame(0.0, index=etudids_sorted, columns=acronymes_ues_sorted) # Traitement des capitalisations capitalisations = ressembuttags[frmsem_id].capitalisations capitalisations = capitalisations.replace(True, 1.0).replace(False, 0.0) # Met à 0 les coeffs des UEs non capitalisées : 1.0*False => 0.0 etudids_communs, acronymes_communs = pe_comp.find_index_and_columns_communs( df, capitalisations ) df.loc[etudids_communs, acronymes_communs] = capitalisations.loc[ etudids_communs, acronymes_communs ] # 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_ues_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_ues_sorted: liste des acronymes des ues (dim. 1 du cube) trié par acronyme inscr_mask: masque etudids x UE traduisant les inscriptions des étudiants aux UE (du semestre terminal) Returns: 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_ues_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_ues_sorted, # les tags ) etud_moy_tag_df.fillna(np.nan) return etud_moy_tag_df