############################################################################## # ScoDoc # Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """Résultats semestres classiques (non APC) """ import time import numpy as np import pandas as pd from sqlalchemy.sql import text from flask import g, url_for from app import db from app import log from app.comp import moy_mod, moy_ue, inscr_mod from app.comp.res_common import NotesTableCompat from app.comp.bonus_spo import BonusSport from app.models import ScoDocSiteConfig from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre from app.models.ues import UniteEns from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_utils import ModuleType class ResultatsSemestreClassic(NotesTableCompat): """Résultats du semestre (formation classique): organisation des calculs.""" _cached_attrs = NotesTableCompat._cached_attrs + ( "modimpl_coefs", "modimpl_idx", "sem_matrix", ) def __init__(self, formsemestre): super().__init__(formsemestre) if not self.load_cached(): t0 = time.time() self.compute() t1 = time.time() self.store() t2 = time.time() log( f"ResultatsSemestreClassic: cached formsemestre_id={formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)" ) # recalculé (aussi rapide que de les cacher) self.moy_min = self.etud_moy_gen.min() self.moy_max = self.etud_moy_gen.max() self.moy_moy = self.etud_moy_gen.mean() def compute(self): "Charge les notes et inscriptions et calcule les moyennes d'UE et gen." self.sem_matrix, self.modimpls_results = notes_sem_load_matrix( self.formsemestre ) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) self.modimpl_coefs = np.array( [m.module.coefficient for m in self.formsemestre.modimpls_sorted] ) self.modimpl_idx = { m.id: i for i, m in enumerate(self.formsemestre.modimpls_sorted) } "l'idx de la colonne du mod modimpl.id est modimpl_idx[modimpl.id]" modimpl_standards_mask = np.array( [ (m.module.module_type == ModuleType.STANDARD) and (m.module.ue.type != UE_SPORT) for m in self.formsemestre.modimpls_sorted ] ) ( self.etud_moy_gen, self.etud_moy_ue, self.etud_coef_ue_df, ) = moy_ue.compute_ue_moys_classic( self.formsemestre, self.sem_matrix, self.ues, self.modimpl_inscr_df, self.modimpl_coefs, modimpl_standards_mask, ) # --- Modules de MALUS sur les UEs et la moyenne générale self.malus = moy_ue.compute_malus( self.formsemestre, self.sem_matrix, self.ues, self.modimpl_inscr_df ) self.etud_moy_ue -= self.malus # ajuste la moyenne générale (à l'aide des coefs d'UE) self.etud_moy_gen -= (self.etud_coef_ue_df * self.malus).sum( axis=1 ) / self.etud_coef_ue_df.sum(axis=1) # --- Bonus Sport & Culture bonus_class = ScoDocSiteConfig.get_bonus_sport_class() if bonus_class is not None: bonus: BonusSport = bonus_class( self.formsemestre, self.sem_matrix, self.ues, self.modimpl_inscr_df, self.modimpl_coefs, 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 self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True) bonus_mg = bonus.get_bonus_moy_gen() if bonus_mg is not None: self.etud_moy_gen += bonus_mg self.etud_moy_gen.clip(lower=0.0, upper=20.0, inplace=True) # compat nt, utilisé pour l'afficher sur les bulletins: self.bonus = bonus_mg # --- 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 Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM) """ return self.modimpls_results[moduleimpl_id].etuds_moy_module.get(etudid, "NI") def get_mod_stats(self, moduleimpl_id: int) -> dict: """Stats sur les notes obtenues dans un modimpl""" notes_series: pd.Series = self.modimpls_results[moduleimpl_id].etuds_moy_module nb_notes = len(notes_series) if not nb_notes: super().get_mod_stats(moduleimpl_id) return { # Series: Statistical methods from ndarray have been overridden to automatically # exclude missing data (currently represented as NaN) "moy": notes_series.mean(), # donc sans prendre en compte les NaN "max": notes_series.max(), "min": notes_series.min(), "nb_notes": nb_notes, "nb_missing": sum(notes_series.isna()), "nb_valid_evals": sum( self.modimpls_results[moduleimpl_id].evaluations_completes ), } 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. Coef = somme des coefs des modules de l'UE auxquels il est inscrit """ c = comp_etud_sum_coef_modules_ue(self.formsemestre.id, etudid, ue["ue_id"]) if c is not None: # inscrit à au moins un module de cette UE return c # arfff: aucun moyen de déterminer le coefficient de façon sûre log( "* oups: calcul coef UE impossible\nformsemestre_id='%s'\netudid='%s'\nue=%s" % (self.formsemestre.id, etudid, ue) ) etud: Identite = Identite.query.get(etudid) raise ScoValueError( """

Coefficient de l'UE capitalisée %s impossible à déterminer pour l'étudiant %s

Il faut saisir le coefficient de cette UE avant de continuer

""" % ( ue.acronyme, url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), etud.nom_disp(), url_for( "notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, err_ue_id=ue["ue_id"], ), ) ) return 0.0 # ? def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]: """Calcule la matrice des notes du semestre (charge toutes les notes, calcule les moyennes des modules et assemble la matrice) Resultat: sem_matrix : 2d-array (etuds x modimpls) modimpls_results dict { modimpl.id : ModuleImplResultsClassic } """ modimpls_results = {} modimpls_notes = [] for modimpl in formsemestre.modimpls_sorted: mod_results = moy_mod.ModuleImplResultsClassic(modimpl) etuds_moy_module = mod_results.compute_module_moy() modimpls_results[modimpl.id] = mod_results modimpls_notes.append(etuds_moy_module) return ( notes_sem_assemble_matrix(modimpls_notes), modimpls_results, ) def notes_sem_assemble_matrix(modimpls_notes: list[pd.Series]) -> np.ndarray: """Réuni les notes moyennes des modules du semestre en une matrice modimpls_notes : liste des moyennes de module (Series rendus par compute_module_moy, index: etud) Resultat: ndarray (etud x module) """ if not len(modimpls_notes): return np.zeros((0, 0), dtype=float) modimpls_notes_arr = [s.values for s in modimpls_notes] modimpls_notes = np.stack(modimpls_notes_arr) # passe de (mod x etud) à (etud x mod) return modimpls_notes.T def comp_etud_sum_coef_modules_ue(formsemestre_id, etudid, ue_id): """Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit ou None s'il n'y a aucun module. """ # comme l'ancien notes_table.comp_etud_sum_coef_modules_ue # mais en raw sqlalchemy et la somme en SQL sql = text( """ SELECT sum(mod.coefficient) FROM notes_modules mod, notes_moduleimpl mi, notes_moduleimpl_inscription ins WHERE mod.id = mi.module_id and ins.etudid = :etudid and ins.moduleimpl_id = mi.id and mi.formsemestre_id = :formsemestre_id and mod.ue_id = :ue_id """ ) cursor = db.session.execute( sql, {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id} ) r = cursor.fetchone() if r is None: return None return r[0]