WIP: prise en compte des UE capitalisées (! calculs erronés/en cours)

This commit is contained in:
Emmanuel Viennet 2022-02-07 16:32:04 +01:00
parent 844a90ed62
commit a9df233c2e
7 changed files with 215 additions and 17 deletions

View File

@ -34,6 +34,13 @@ class ValidationsSemestre(ResultatsCache):
"""Décisions sur des UEs dans ce semestre:
{ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}}
"""
self.ue_capitalisees: pd.DataFrame = None
"""DataFrame, index etudid
formsemestre_id : origine de l'UE capitalisée
is_external : vrai si validation effectuée dans un semestre extérieur
ue_id : dans le semestre origine (pas toujours de la même formation)
ue_code : code de l'UE, moy_ue : note enregistrée,
event_date : date de la validation (jury)."""
if not self.load_cached():
self.compute()

View File

@ -12,6 +12,7 @@ from app.comp import moy_ue, moy_sem, inscr_mod
from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
from app.models.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT
@ -93,11 +94,14 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
# Moyenne générale indicative:
# (note: le bonus sport a déjà été appliqué aux moyenens d'UE, et impacte
# (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(
self.etud_moy_ue, self.modimpl_coefs_df
)
# --- UE capitalisées
self.apply_capitalisation()
# --- Classements:
self.compute_rangs()
@ -110,3 +114,12 @@ class ResultatsSemestreBUT(NotesTableCompat):
etud_idx = self.etud_index[etudid]
# moyenne sur les UE:
return self.sem_cube[etud_idx, mod_idx].mean()
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()

View File

@ -6,15 +6,24 @@
"""Résultats semestres classiques (non APC)
"""
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
@ -104,6 +113,9 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.bonus = (
bonus_mg # compat nt, utilisé pour l'afficher sur les bulletins
)
# --- UE capitalisées
self.apply_capitalisation()
# --- Classements:
self.compute_rangs()
@ -132,6 +144,42 @@ class ResultatsSemestreClassic(NotesTableCompat):
),
}
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(
"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée %s impossible à déterminer
pour l'étudiant <a href="%s" class="discretelink">%s</a></p>
<p>Il faut <a href="%s">saisir le coefficient de cette UE avant de continuer</a></p>
</div>
"""
% (
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
@ -165,3 +213,29 @@ def notes_sem_assemble_matrix(modimpls_notes: list[pd.Series]) -> np.ndarray:
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]

View File

@ -8,17 +8,22 @@ from collections import defaultdict, Counter
from functools import cached_property
import numpy as np
import pandas as pd
from flask import g
from app.comp.aux_stats import StatsMoyenne
from app.comp import moy_sem
from app.comp.res_cache import ResultatsCache
from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, Identite, ModuleImpl
from app.models import FormSemestreUECoef
from app.models.ues import UniteEns
from app.scodoc import sco_utils as scu
from app.scodoc import sco_evaluations
from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF
from app.scodoc.sco_exceptions import ScoValueError
# Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis):
@ -53,6 +58,7 @@ class ResultatsSemestre(ResultatsCache):
"Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }"
self.etud_coef_ue_df = None
"""coefs d'UE effectifs pour chaque étudiant (pour form. classiques)"""
self.validations = None
def compute(self):
"Charge les notes et inscriptions et calcule toutes les moyennes"
@ -155,6 +161,115 @@ class ResultatsSemestre(ResultatsCache):
"""
return self.etud_moy_ue > (seuil - scu.NOTES_TOLERANCE)
def apply_capitalisation(self):
"""Recalcule la moyenne générale pour prendre en compte d'éventuelles
UE capitalisées.
"""
# Supposant qu'il y a peu d'UE capitalisées,
# on va soustraire la moyenne d'UE et ajouter celle de l'UE capitalisée.
# return # XXX XXX XXX
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
ue_capitalisees = self.validations.ue_capitalisees
ue_by_code = {}
for etudid in ue_capitalisees.index:
recompute_mg = False
# ue_codes = set(ue_capitalisees.loc[etudid]["ue_code"])
# for ue_code in ue_codes:
# ue = ue_by_code.get(ue_code)
# if ue is None:
# ue = self.formsemestre.query_ues.filter_by(ue_code=ue_code)
# ue_by_code[ue_code] = ue
# Quand il y a une capitalisation, vérifie toutes les UEs
sum_notes_ue = 0.0
sum_coefs_ue = 0.0
for ue in self.formsemestre.query_ues():
ue_cap = self.get_etud_ue_status(etudid, ue.id)
if ue_cap["is_capitalized"]:
recompute_mg = True
coef = ue_cap["coef_ue"]
if not np.isnan(ue_cap["moy"]):
sum_notes_ue += ue_cap["moy"] * coef
sum_coefs_ue += coef
if recompute_mg and sum_coefs_ue > 0.0:
# On doit prendre en compte une ou plusieurs UE capitalisées
# et donc recalculer la moyenne générale
self.etud_moy_gen[etudid] = sum_notes_ue / sum_coefs_ue
def _get_etud_ue_cap(self, etudid, ue):
""""""
capitalisations = self.validations.ue_capitalisees.loc[etudid]
if isinstance(capitalisations, pd.DataFrame):
ue_cap = capitalisations[capitalisations["ue_code"] == ue.ue_code]
if isinstance(ue_cap, pd.DataFrame) and not ue_cap.empty:
# si plusieurs fois capitalisée, prend le max
cap_idx = ue_cap["moy_ue"].values.argmax()
ue_cap = ue_cap.iloc[cap_idx]
else:
if capitalisations["ue_code"] == ue.ue_code:
ue_cap = capitalisations
else:
ue_cap = None
return ue_cap
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
"""L'état de l'UE pour cet étudiant.
Result: dict
"""
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
ue = UniteEns.query.get(ue_id) # TODO cacher nos UEs ?
cur_moy_ue = self.etud_moy_ue[ue_id][etudid]
moy_ue = cur_moy_ue
is_capitalized = False
if etudid in self.validations.ue_capitalisees.index:
ue_cap = self._get_etud_ue_cap(etudid, ue)
if ue_cap is not None and not ue_cap.empty:
if ue_cap["moy_ue"] > cur_moy_ue:
moy_ue = ue_cap["moy_ue"]
is_capitalized = True
if is_capitalized:
coef_ue = 1.0
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
return {
"is_capitalized": is_capitalized,
"is_external": ue_cap["is_external"] if is_capitalized else ue.is_external,
"coef_ue": coef_ue,
"cur_moy_ue": cur_moy_ue,
"moy": moy_ue,
"event_date": ue_cap["event_date"] if is_capitalized else None,
"ue": ue.to_dict(),
"formsemestre_id": ue_cap["formsemestre_id"] if is_capitalized else None,
"capitalized_ue_id": ue_cap["ue_id"] if is_capitalized else None,
}
def get_etud_ue_cap_coef(self, etudid, ue, ue_cap):
"""Calcule le coefficient d'une UE capitalisée, pour cet étudiant,
injectée dans le semestre courant.
ue : ue du semestre courant
ue_cap = resultat de formsemestre_get_etud_capitalisation
{ 'ue_id' (dans le semestre source),
'ue_code', 'moy', 'event_date','formsemestre_id' }
"""
# 1- Coefficient explicitement déclaré dans le semestre courant pour cette UE ?
ue_coef_db = FormSemestreUECoef.query.filter_by(
formsemestre_id=self.formsemestre.id, ue_id=ue.id
).first()
if ue_coef_db is not None:
return ue_coef_db.coefficient
# En APC: somme des coefs des modules vers cette UE
# En classique: Capitalisation UE externe: quel coef appliquer ?
# En ScoDoc 7, calculait la somme des coefs dans l'UE du semestre d'origine
# ici si l'étudiant est inscrit dans le semestre courant,
# somme des coefs des modules de l'UE auxquels il est inscrit
return self.compute_etud_ue_coef(etudid, ue)
# Pour raccorder le code des anciens codes qui attendent une NoteTable
class NotesTableCompat(ResultatsSemestre):
@ -189,7 +304,6 @@ class NotesTableCompat(ResultatsSemestre):
self.moy_moy = "NA"
self.expr_diagnostics = ""
self.parcours = self.formsemestre.formation.get_parcours()
self.validations = None
def get_etudids(self, sorted=False) -> list[int]:
"""Liste des etudids inscrits, incluant les démissionnaires.
@ -353,16 +467,6 @@ class NotesTableCompat(ResultatsSemestre):
"ects_fond": 0.0, # not implemented (anciennemment pour école ingé)
}
def get_etud_ue_status(self, etudid: int, ue_id: int):
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
return {
"is_capitalized": False, # XXX TODO
"is_external": False, # XXX TODO
"coef_ue": coef_ue, # XXX TODO
"cur_moy_ue": self.etud_moy_ue[ue_id][etudid],
"moy": self.etud_moy_ue[ue_id][etudid],
}
def get_etud_rang(self, etudid: int):
return self.etud_moy_gen_ranks.get(etudid, 99999) # XXX

View File

@ -427,10 +427,12 @@ class FormSemestreUECoef(db.Model):
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
index=True,
)
ue_id = db.Column(
db.Integer,
db.ForeignKey("notes_ue.id"),
index=True,
)
coefficient = db.Column(db.Float, nullable=False)

View File

@ -703,11 +703,12 @@ class NotesTable:
ue_status = {
'est_inscrit' : True si étudiant inscrit à au moins un module de cette UE
'moy' : moyenne, avec capitalisation eventuelle
'capitalized_ue_id' : id de l'UE capitalisée
'coef_ue' : coef de l'UE utilisé pour le calcul de la moyenne générale
(la somme des coefs des modules, ou le coef d'UE capitalisée,
ou encore le coef d'UE si l'option use_ue_coefs est active)
'cur_moy_ue' : moyenne de l'UE en cours (sans considérer de capitalisation)
'cur_coef_ue': coefficient de l'UE courante
'cur_coef_ue': coefficient de l'UE courante (inutilisé ?)
'is_capitalized' : True|False,
'ects_pot' : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
'ects_pot_fond': 0. si UE non fondamentale, = ects_pot sinon,

View File

@ -165,10 +165,7 @@ def _comp_ects_capitalises_by_ue_code(nt, etudid):
for ue in ues:
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
if ue_status["is_capitalized"]:
try:
ects_val = float(ue_status["ue"]["ects"])
except (ValueError, TypeError):
ects_val = 0.0
ects_val = float(ue_status["ue"]["ects"] or 0.0)
ects_by_ue_code[ue["ue_code"]] = ects_val
return ects_by_ue_code