forked from ScoDoc/ScoDoc
309 lines
12 KiB
Python
309 lines
12 KiB
Python
##############################################################################
|
|
# ScoDoc
|
|
# Copyright (c) 1999 - 2024 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_mat, moy_mod, moy_sem, moy_ue, inscr_mod
|
|
from app.comp.res_compat 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.codes_cursus import UE_SPORT
|
|
from app.scodoc.sco_exceptions import ScoValueError
|
|
from app.scodoc import sco_preferences
|
|
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",
|
|
"mod_rangs",
|
|
)
|
|
|
|
def __init__(self, formsemestre):
|
|
super().__init__(formsemestre)
|
|
self.sem_matrix: np.ndarray = None
|
|
"sem_matrix : 2d-array (etuds x modimpls)"
|
|
|
|
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 or 0.0 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,
|
|
block=self.formsemestre.block_moyennes,
|
|
)
|
|
# --- 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 None and self.bonus_ues is not None:
|
|
# pas de bonus explicite sur la moyenne générale
|
|
# on l'ajuste pour refléter les modifs d'UE, à l'aide des coefs d'UE.
|
|
bonus_mg = (self.etud_coef_ue_df * self.bonus_ues).sum(
|
|
axis=1
|
|
) / self.etud_coef_ue_df.sum(axis=1)
|
|
self.etud_moy_gen += bonus_mg
|
|
elif bonus_mg is not None:
|
|
# Applique le bonus moyenne générale renvoyé
|
|
self.etud_moy_gen += bonus_mg
|
|
|
|
# compat nt, utilisé pour l'afficher sur les bulletins:
|
|
self.bonus = bonus_mg
|
|
|
|
# --- UE capitalisées
|
|
self.apply_capitalisation()
|
|
|
|
# Clippe toutes les moyennes dans [0,20]
|
|
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
|
|
self.etud_moy_gen.clip(lower=0.0, upper=20.0, inplace=True)
|
|
|
|
# --- Classements:
|
|
self.compute_rangs()
|
|
|
|
# --- En option, moyennes par matières
|
|
if sco_preferences.get_preference("bul_show_matieres", self.formsemestre.id):
|
|
self.compute_moyennes_matieres()
|
|
|
|
def compute_rangs(self):
|
|
"""Calcul des rangs (classements) dans le semestre (moy. gen.), les UE
|
|
et les modules.
|
|
"""
|
|
# rangs moy gen et UEs sont calculées par la méthode commune à toutes les formations:
|
|
super().compute_rangs()
|
|
# les rangs des modules n'existent que dans les formations classiques:
|
|
self.mod_rangs = {}
|
|
for modimpl_result in self.modimpls_results.values():
|
|
# ne prend que les rangs sous forme de chaines:
|
|
rangs = moy_sem.comp_ranks_series(modimpl_result.etuds_moy_module)[0]
|
|
self.mod_rangs[modimpl_result.moduleimpl_id] = (
|
|
rangs,
|
|
modimpl_result.nb_inscrits_module,
|
|
)
|
|
|
|
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)
|
|
"""
|
|
try:
|
|
if self.modimpl_inscr_df[moduleimpl_id][etudid]:
|
|
return self.modimpls_results[moduleimpl_id].etuds_moy_module[etudid]
|
|
except KeyError:
|
|
pass
|
|
return "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 modimpl_notes(
|
|
self,
|
|
modimpl_id: int,
|
|
ue_id: int = None,
|
|
) -> np.ndarray:
|
|
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
|
Utile pour stats bottom tableau recap.
|
|
ue_id n'est pas utilisé ici (formations classiques)
|
|
Résultat: 1d array of float
|
|
"""
|
|
i = self.modimpl_idx[modimpl_id]
|
|
return self.sem_matrix[:, i]
|
|
|
|
def compute_moyennes_matieres(self):
|
|
"""Calcul les moyennes par matière. Doit être appelée au besoin, en fin de compute."""
|
|
self.moyennes_matieres = moy_mat.compute_mat_moys_classic(
|
|
self.formsemestre,
|
|
self.sem_matrix,
|
|
self.ues,
|
|
self.modimpl_inscr_df,
|
|
self.modimpl_coefs,
|
|
)
|
|
|
|
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
|
|
"""
|
|
coef = comp_etud_sum_coef_modules_ue(self.formsemestre.id, etudid, ue["ue_id"])
|
|
if coef is not None: # inscrit à au moins un module de cette UE
|
|
return coef
|
|
# arfff: aucun moyen de déterminer le coefficient de façon sûre
|
|
log(
|
|
f"""* oups: calcul coef UE impossible\nformsemestre_id='{self.formsemestre.id
|
|
}'\netudid='{etudid}'\nue={ue}"""
|
|
)
|
|
etud = Identite.get_etud(etudid)
|
|
raise ScoValueError(
|
|
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
|
|
impossible à déterminer pour l'étudiant <a href="{
|
|
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
|
}" class="discretelink">{etud.nom_disp()}</a></p>
|
|
<p>Il faut <a href="{
|
|
url_for("notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=self.formsemestre.id, err_ue_id=ue["ue_id"],
|
|
)
|
|
}">saisir le coefficient de cette UE avant de continuer</a></p>
|
|
</div>
|
|
"""
|
|
)
|
|
|
|
|
|
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 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]
|