############################################################################## # ScoDoc # Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """Résultats semestres BUT """ import time import numpy as np import pandas as pd from app import log from app.comp import moy_ue, moy_sem, inscr_mod from app.comp.res_compat import NotesTableCompat from app.comp.bonus_spo import BonusSport from app.models import ScoDocSiteConfig from app.models.moduleimpls import ModuleImpl from app.models.ues import UniteEns from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc import sco_preferences class ResultatsSemestreBUT(NotesTableCompat): """Résultats BUT: organisation des calculs""" _cached_attrs = NotesTableCompat._cached_attrs + ( "modimpl_coefs_df", "modimpls_evals_poids", "sem_cube", "etuds_parcour_id", # parcours de chaque étudiant "ues_inscr_parcours_df", # inscriptions aux UE / parcours ) def __init__(self, formsemestre): super().__init__(formsemestre) self.sem_cube = None """ndarray (etuds x modimpl x ue)""" self.etuds_parcour_id = None """Parcours de chaque étudiant { etudid : parcour_id }""" if not self.load_cached(): t0 = time.time() self.compute() t1 = time.time() self.store() t2 = time.time() log( f"""ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id } ({(t1-t0):g}s +{(t2-t1):g}s)""" ) def compute(self): "Charge les notes et inscriptions et calcule les moyennes d'UE et gen." ( self.sem_cube, self.modimpls_evals_poids, self.modimpls_results, ) = moy_ue.notes_sem_load_cube(self.formsemestre) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) self.ues_inscr_parcours_df = self.load_ues_inscr_parcours() self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs( self.formsemestre, modimpls=self.formsemestre.modimpls_sorted ) # l'idx de la colonne du mod modimpl.id est # modimpl_coefs_df.columns.get_loc(modimpl.id) # idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id) # Masque de tous les modules _sauf_ les bonus (sport) modimpls_mask = [ modimpl.module.ue.type != UE_SPORT for modimpl in self.formsemestre.modimpls_sorted ] self.dispense_ues = moy_ue.load_dispense_ues( self.formsemestre, self.modimpl_inscr_df.index, self.ues ) self.etud_moy_ue = moy_ue.compute_ue_moys_apc( self.sem_cube, self.etuds, self.formsemestre.modimpls_sorted, self.modimpl_inscr_df, self.modimpl_coefs_df, modimpls_mask, self.dispense_ues, block=self.formsemestre.block_moyennes, ) # Les coefficients d'UE ne sont pas utilisés en APC self.etud_coef_ue_df = pd.DataFrame( 0.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns ) # --- Modules de MALUS sur les UEs self.malus = moy_ue.compute_malus( self.formsemestre, self.sem_cube, self.ues, self.modimpl_inscr_df ) self.etud_moy_ue -= self.malus # --- Bonus Sport & Culture if not all(modimpls_mask): # au moins un module bonus bonus_class = ScoDocSiteConfig.get_bonus_sport_class() if bonus_class is not None: bonus: BonusSport = bonus_class( self.formsemestre, self.sem_cube, self.ues, self.modimpl_inscr_df, self.modimpl_coefs_df.transpose(), self.etud_moy_gen, self.etud_moy_ue, ) self.bonus_ues = bonus.get_bonus_ues() if self.bonus_ues is not None: self.etud_moy_ue += self.bonus_ues # somme les dataframes # Clippe toutes les moyennes d'UE dans [0,20] self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True) # Nanifie les moyennes d'UE hors parcours pour chaque étudiant self.etud_moy_ue *= self.ues_inscr_parcours_df # Les ects (utilisés comme coefs) sont nuls pour les UE hors parcours: ects = self.ues_inscr_parcours_df.fillna(0.0) * [ ue.ects for ue in self.ues if ue.type != UE_SPORT ] # Moyenne générale indicative: # (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte # donc la moyenne indicative) # self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs( # self.etud_moy_ue, self.modimpl_coefs_df # ) if self.formsemestre.block_moyenne_generale or self.formsemestre.block_moyennes: self.etud_moy_gen = pd.Series( index=self.etud_moy_ue.index, dtype=float ) # NaNs else: self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects( self.etud_moy_ue, ects, formation_id=self.formsemestre.formation_id, skip_empty_ues=sco_preferences.get_preference( "but_moy_skip_empty_ues", self.formsemestre.id ), ) # --- UE capitalisées self.apply_capitalisation() # --- Classements: self.compute_rangs() def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: """La moyenne de l'étudiant dans le moduleimpl En APC, il s'agit d'une moyenne indicative sans valeur. Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM) """ mod_idx = self.modimpl_coefs_df.columns.get_loc(moduleimpl_id) etud_idx = self.etud_index[etudid] # moyenne sur les UE: if len(self.sem_cube[etud_idx, mod_idx]): return np.nanmean(self.sem_cube[etud_idx, mod_idx]) return np.nan def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float: """Détermine le coefficient de l'UE pour cet étudiant. N'est utilisé que pour l'injection des UE capitalisées dans la moyenne générale. En BUT, c'est simple: Coef = somme des coefs des modules vers cette UE. (ne dépend pas des modules auxquels est inscrit l'étudiant, ). """ return self.modimpl_coefs_df.loc[ue.id].sum() def modimpls_in_ue(self, ue: UniteEns, etudid, with_bonus=True) -> list[ModuleImpl]: """Liste des modimpl ayant des coefs non nuls vers cette UE et auxquels l'étudiant est inscrit. Inclus modules bonus le cas échéant. """ # sert pour l'affichage ou non de l'UE sur le bulletin et la table recap if ue.type == UE_SPORT: return [ modimpl for modimpl in self.formsemestre.modimpls_sorted if modimpl.module.ue.id == ue.id and self.modimpl_inscr_df[modimpl.id][etudid] ] coefs = self.modimpl_coefs_df # row UE (sans bonus), cols modimpl modimpls = [ modimpl for modimpl in self.formsemestre.modimpls_sorted if modimpl.module.ue.type != UE_SPORT and (coefs[modimpl.id][ue.id] != 0) and self.modimpl_inscr_df[modimpl.id][etudid] ] if not with_bonus: return [ modimpl for modimpl in modimpls if modimpl.module.ue.type != UE_SPORT ] return modimpls def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray: """Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue. Utile pour stats bottom tableau recap. Résultat: 1d array of float """ i = self.modimpl_coefs_df.columns.get_loc(modimpl_id) j = self.modimpl_coefs_df.index.get_loc(ue_id) return self.sem_cube[:, i, j] def load_ues_inscr_parcours(self) -> pd.DataFrame: """Chargement des inscriptions aux parcours et calcul de la matrice d'inscriptions (etuds, ue). S'il n'y pas de référentiel de compétence, donc pas de parcours, on considère l'étudiant inscrit à toutes les ue. La matrice avec ue ne comprend que les UE non bonus. 1.0 si étudiant inscrit à l'UE, NaN sinon. """ etuds_parcour_id = { inscr.etudid: inscr.parcour_id for inscr in self.formsemestre.inscriptions } self.etuds_parcour_id = etuds_parcour_id ue_ids = [ue.id for ue in self.ues if ue.type != UE_SPORT] # matrice de 1, inscrits par défaut à toutes les UE: ues_inscr_parcours_df = pd.DataFrame( 1.0, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float ) if self.formsemestre.formation.referentiel_competence is None: return ues_inscr_parcours_df ue_by_parcours = {} # parcours_id : {ue_id:0|1} for parcour in self.formsemestre.formation.referentiel_competence.parcours: ue_by_parcours[parcour.id] = { ue.id: 1.0 for ue in self.formsemestre.formation.query_ues_parcour( parcour ).filter_by(semestre_idx=self.formsemestre.semestre_id) } for etudid in etuds_parcour_id: parcour_id = etuds_parcour_id[etudid] if parcour_id is not None and parcour_id in ue_by_parcours: ues_inscr_parcours_df.loc[etudid] = ue_by_parcours[parcour_id] return ues_inscr_parcours_df def etud_ues_ids(self, etudid: int) -> list[int]: """Liste des id d'UE auxquelles l'étudiant est inscrit (sans bonus). (surchargée ici pour prendre en compte les parcours) """ s = self.ues_inscr_parcours_df.loc[etudid] return s.index[s.notna()]