Support pour plusieurs évaluations de rattrapage en classique et BUT. Avance sur #811.

This commit is contained in:
Emmanuel Viennet 2024-05-21 20:23:10 +02:00
parent 8f25284038
commit 89afb672af
2 changed files with 113 additions and 91 deletions

View File

@ -45,7 +45,6 @@ from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.codes_cursus import UE_SPORT from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc.sco_exceptions import ScoBugCatcher
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
@ -275,20 +274,18 @@ class ModuleImplResults:
* self.evaluations_completes * self.evaluations_completes
).reshape(-1, 1) ).reshape(-1, 1)
def get_evaluations_session2_coefs(self, modimpl: ModuleImpl) -> np.array: def get_evaluations_special_coefs(
"""Coefficients des évaluations de session 2. self, modimpl: ModuleImpl, evaluation_type=Evaluation.EVALUATION_SESSION2
Les évals de session 2 sont réputées "complètes": elles sont toujours ) -> np.array:
"""Coefficients des évaluations de session 2 ou rattrapage.
Les évals de session 2 et rattrapage sont réputées "complètes": elles sont toujours
prises en compte mais seules les notes numériques et ABS sont utilisées. prises en compte mais seules les notes numériques et ABS sont utilisées.
Résultat: 2d-array of floats, shape (nb_evals, 1) Résultat: 2d-array of floats, shape (nb_evals, 1)
""" """
return ( return (
np.array( np.array(
[ [
( (e.coefficient if e.evaluation_type == evaluation_type else 0.0)
e.coefficient
if e.evaluation_type == Evaluation.EVALUATION_SESSION2
else 0.0
)
for e in modimpl.evaluations for e in modimpl.evaluations
], ],
dtype=float, dtype=float,
@ -321,19 +318,16 @@ class ModuleImplResults:
for (etudid, x) in self.evals_notes[evaluation_id].items() for (etudid, x) in self.evals_notes[evaluation_id].items()
} }
def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl) -> Evaluation | None: def get_evaluations_rattrapage(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
"""L'évaluation de rattrapage de ce module, ou None s'il n'en a pas. """Les évaluations de rattrapage de ce module.
Rattrapage: la moyenne du module est la meilleure note entre moyenne Rattrapage: la moyenne du module est la meilleure note entre moyenne
des autres évals et la note eval rattrapage. des autres évals et la moyenne des notes de rattrapage.
""" """
eval_list = [ return [
e e
for e in moduleimpl.evaluations for e in moduleimpl.evaluations
if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE
] ]
if eval_list:
return eval_list[0]
return None
def get_evaluations_session2(self, moduleimpl: ModuleImpl) -> list[Evaluation]: def get_evaluations_session2(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
"""Les évaluations de deuxième session de ce module, ou None s'il n'en a pas. """Les évaluations de deuxième session de ce module, ou None s'il n'en a pas.
@ -421,25 +415,18 @@ class ModuleImplResultsAPC(ModuleImplResults):
) / np.sum(evals_poids_etuds, axis=1) ) / np.sum(evals_poids_etuds, axis=1)
# etuds_moy_module shape: nb_etuds x nb_ues # etuds_moy_module shape: nb_etuds x nb_ues
# Session2 : quand elle existe, remplace la note de module
evals_session2 = self.get_evaluations_session2(modimpl) evals_session2 = self.get_evaluations_session2(modimpl)
evals_rat = self.get_evaluations_rattrapage(modimpl)
if evals_session2: if evals_session2:
# Session2 : quand elle existe, remplace la note de module
# Calcul moyenne notes session2 et remplace (si la note session 2 existe) # Calcul moyenne notes session2 et remplace (si la note session 2 existe)
evals_coefs_s2 = self.get_evaluations_session2_coefs(modimpl) etuds_moy_module_s2 = self._compute_moy_special(
evals_poids_s2 = evals_poids_df.values * evals_coefs_s2 modimpl,
poids_stacked_s2 = np.stack( evals_notes_stacked,
[evals_poids_s2] * nb_etuds evals_poids_df,
) # nb_etuds, nb_evals, nb_ues Evaluation.EVALUATION_SESSION2,
evals_poids_etuds_s2 = np.where(
np.stack([self.evals_notes.values] * nb_ues, axis=2)
> scu.NOTES_NEUTRALISE,
poids_stacked_s2,
0,
) )
etuds_moy_module_s2 = np.sum(
evals_poids_etuds_s2 * evals_notes_stacked, axis=1
) / np.sum(evals_poids_etuds_s2, axis=1)
# Vrai si toutes les UEs ont bien une note de session 2 calculée: # Vrai si toutes les UEs ont bien une note de session 2 calculée:
etuds_use_session2 = np.all(np.isfinite(etuds_moy_module_s2), axis=1) etuds_use_session2 = np.all(np.isfinite(etuds_moy_module_s2), axis=1)
etuds_moy_module = np.where( etuds_moy_module = np.where(
@ -450,29 +437,22 @@ class ModuleImplResultsAPC(ModuleImplResults):
self.etuds_use_session2 = pd.Series( self.etuds_use_session2 = pd.Series(
etuds_use_session2, index=self.evals_notes.index etuds_use_session2, index=self.evals_notes.index
) )
else: elif evals_rat:
# Rattrapage: remplace la note de module ssi elle est supérieure etuds_moy_module_rat = self._compute_moy_special(
eval_rat = self.get_evaluation_rattrapage(modimpl) modimpl,
if eval_rat: evals_notes_stacked,
notes_rat = self.evals_notes[eval_rat.id].values evals_poids_df,
# remplace les notes invalides (ATT, EXC...) par des NaN Evaluation.EVALUATION_RATTRAPAGE,
notes_rat = np.where( )
notes_rat > scu.NOTES_ABSENCE, etuds_ue_use_rattrapage = (
notes_rat / (eval_rat.note_max / 20.0), etuds_moy_module_rat > etuds_moy_module
np.nan, ) # etud x UE
) etuds_moy_module = np.where(
# "Étend" le rattrapage sur les UE: la note de rattrapage est la même etuds_ue_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
# pour toutes les UE mais ne remplace que là où elle est supérieure XXX TODO )
notes_rat_ues = np.stack([notes_rat] * nb_ues, axis=1) self.etuds_use_rattrapage = pd.Series(
# prend le max np.any(etuds_ue_use_rattrapage, axis=1), index=self.evals_notes.index
etuds_use_rattrapage = notes_rat_ues > etuds_moy_module )
etuds_moy_module = np.where(
etuds_use_rattrapage, notes_rat_ues, etuds_moy_module
)
# Serie indiquant que l'étudiant utilise une note de rattrapage sur l'une des UE:
self.etuds_use_rattrapage = pd.Series(
etuds_use_rattrapage.any(axis=1), index=self.evals_notes.index
)
# Application des évaluations bonus: # Application des évaluations bonus:
etuds_moy_module = self.apply_bonus( etuds_moy_module = self.apply_bonus(
etuds_moy_module, etuds_moy_module,
@ -487,6 +467,34 @@ class ModuleImplResultsAPC(ModuleImplResults):
) )
return self.etuds_moy_module return self.etuds_moy_module
def _compute_moy_special(
self,
modimpl: ModuleImpl,
evals_notes_stacked: np.array,
evals_poids_df: pd.DataFrame,
evaluation_type: int,
) -> np.array:
"""Calcul moyenne APC sur évals rattrapage ou session2"""
nb_etuds = self.evals_notes.shape[0]
nb_ues = evals_poids_df.shape[1]
evals_coefs_s2 = self.get_evaluations_special_coefs(
modimpl, evaluation_type=evaluation_type
)
evals_poids_s2 = evals_poids_df.values * evals_coefs_s2
poids_stacked_s2 = np.stack(
[evals_poids_s2] * nb_etuds
) # nb_etuds, nb_evals, nb_ues
evals_poids_etuds_s2 = np.where(
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
poids_stacked_s2,
0,
)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etuds_moy_module_s2 = np.sum(
evals_poids_etuds_s2 * evals_notes_stacked, axis=1
) / np.sum(evals_poids_etuds_s2, axis=1)
return etuds_moy_module_s2
def apply_bonus( def apply_bonus(
self, self,
etuds_moy_module: pd.DataFrame, etuds_moy_module: pd.DataFrame,
@ -628,24 +636,14 @@ class ModuleImplResultsClassic(ModuleImplResults):
evals_coefs_etuds * evals_notes_20, axis=1 evals_coefs_etuds * evals_notes_20, axis=1
) / np.sum(evals_coefs_etuds, axis=1) ) / np.sum(evals_coefs_etuds, axis=1)
# Session2 : quand elle existe, remplace la note de module
evals_session2 = self.get_evaluations_session2(modimpl) evals_session2 = self.get_evaluations_session2(modimpl)
evals_rat = self.get_evaluations_rattrapage(modimpl)
if evals_session2: if evals_session2:
# Calculer la moyenne des évaluations de session2 # Session2 : quand elle existe, remplace la note de module
# n'utilise que les notes valides et ABS (0). # Calcule la moyenne des évaluations de session2
# Même calcul que pour les évals normales, mais avec seulement les etuds_moy_module_s2 = self._compute_moy_special(
# coefs des évals de session 2: modimpl, evals_notes_20, Evaluation.EVALUATION_SESSION2
evals_coefs_s2 = self.get_evaluations_session2_coefs(modimpl).reshape(-1)
coefs_stacked_s2 = np.stack([evals_coefs_s2] * nb_etuds)
# zéro partout sauf si une note ou ABS:
evals_coefs_etuds_s2 = np.where(
self.evals_notes.values > scu.NOTES_NEUTRALISE, coefs_stacked_s2, 0
) )
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etuds_moy_module_s2 = np.sum(
evals_coefs_etuds_s2 * evals_notes_20, axis=1
) / np.sum(evals_coefs_etuds_s2, axis=1)
etuds_use_session2 = np.isfinite(etuds_moy_module_s2) etuds_use_session2 = np.isfinite(etuds_moy_module_s2)
etuds_moy_module = np.where( etuds_moy_module = np.where(
etuds_use_session2, etuds_use_session2,
@ -655,25 +653,19 @@ class ModuleImplResultsClassic(ModuleImplResults):
self.etuds_use_session2 = pd.Series( self.etuds_use_session2 = pd.Series(
etuds_use_session2, index=self.evals_notes.index etuds_use_session2, index=self.evals_notes.index
) )
else: elif evals_rat:
# Rattrapage: remplace la note de module ssi elle est supérieure # Rattrapage: remplace la note de module ssi elle est supérieure
eval_rat = self.get_evaluation_rattrapage(modimpl) # Calcule la moyenne des évaluations de rattrapage
if eval_rat: etuds_moy_module_rat = self._compute_moy_special(
notes_rat = self.evals_notes[eval_rat.id].values modimpl, evals_notes_20, Evaluation.EVALUATION_RATTRAPAGE
# remplace les notes invalides (ATT, EXC...) par des NaN )
notes_rat = np.where( etuds_use_rattrapage = etuds_moy_module_rat > etuds_moy_module
notes_rat > scu.NOTES_ABSENCE, etuds_moy_module = np.where(
notes_rat / (eval_rat.note_max / 20.0), etuds_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
np.nan, )
) self.etuds_use_rattrapage = pd.Series(
# prend le max etuds_use_rattrapage, index=self.evals_notes.index
etuds_use_rattrapage = notes_rat > etuds_moy_module )
etuds_moy_module = np.where(
etuds_use_rattrapage, notes_rat, etuds_moy_module
)
self.etuds_use_rattrapage = pd.Series(
etuds_use_rattrapage, index=self.evals_notes.index
)
# Application des évaluations bonus: # Application des évaluations bonus:
etuds_moy_module = self.apply_bonus( etuds_moy_module = self.apply_bonus(
@ -688,6 +680,28 @@ class ModuleImplResultsClassic(ModuleImplResults):
return self.etuds_moy_module return self.etuds_moy_module
def _compute_moy_special(
self, modimpl: ModuleImpl, evals_notes_20: np.array, evaluation_type: int
) -> np.array:
"""Calcul moyenne sur évals rattrapage ou session2"""
# n'utilise que les notes valides et ABS (0).
# Même calcul que pour les évals normales, mais avec seulement les
# coefs des évals de session 2 ou rattrapage:
nb_etuds = self.evals_notes.shape[0]
evals_coefs = self.get_evaluations_special_coefs(
modimpl, evaluation_type=evaluation_type
).reshape(-1)
coefs_stacked = np.stack([evals_coefs] * nb_etuds)
# zéro partout sauf si une note ou ABS:
evals_coefs_etuds = np.where(
self.evals_notes.values > scu.NOTES_NEUTRALISE, coefs_stacked, 0
)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etuds_moy_module = np.sum(
evals_coefs_etuds * evals_notes_20, axis=1
) / np.sum(evals_coefs_etuds, axis=1)
return etuds_moy_module # array 1d (nb_etuds)
def apply_bonus( def apply_bonus(
self, self,
etuds_moy_module: np.ndarray, etuds_moy_module: np.ndarray,

View File

@ -81,8 +81,9 @@ def test_notes_rattrapage(test_client):
mod_res = res.modimpls_results[moduleimpl_id] mod_res = res.modimpls_results[moduleimpl_id]
moduleimpl = db.session.get(ModuleImpl, moduleimpl_id) moduleimpl = db.session.get(ModuleImpl, moduleimpl_id)
# retrouve l'éval. de rattrapage: # retrouve l'éval. de rattrapage:
eval_rat = mod_res.get_evaluation_rattrapage(moduleimpl) evals_rat = mod_res.get_evaluations_rattrapage(moduleimpl)
assert eval_rat.id == e_rat["id"] assert len(evals_rat) == 1
assert evals_rat[0].id == e_rat["id"]
# Les deux évaluations sont considérées comme complètes: # Les deux évaluations sont considérées comme complètes:
assert len(mod_res.get_evaluations_completes(moduleimpl)) == 2 assert len(mod_res.get_evaluations_completes(moduleimpl)) == 2
@ -176,18 +177,25 @@ def test_notes_rattrapage(test_client):
# Note moyenne: utilise session 2 même si inférieure # Note moyenne: utilise session 2 même si inférieure
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(20.0) assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(20.0)
# Met la note session2 à ABS (None)
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e_session2["id"], etudid=etud["etudid"], note=None evaluation_id=e_session2["id"], etudid=etud["etudid"], note=None
) )
b = sco_bulletins.formsemestre_bulletinetud_dict( b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"] sem["formsemestre_id"], etud["etudid"]
) )
# Note moyenne: revient à note normale # Note moyenne: zéro car ABS
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(10.0) assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(0.0)
# Supprime évaluation session 2 # Supprime note session 2
_, _, _ = G.create_note( _, _, _ = G.create_note(
evaluation_id=e_session2["id"], etudid=etud["etudid"], note=scu.NOTES_SUPPRESS evaluation_id=e_session2["id"], etudid=etud["etudid"], note=scu.NOTES_SUPPRESS
) )
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
# Note moyenne: revient à sa valeur initiale, 10/20
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(10.0)
# Supprime évaluation session 2
evaluation = db.session.get(Evaluation, e_session2["id"]) evaluation = db.session.get(Evaluation, e_session2["id"])
assert evaluation assert evaluation
evaluation.delete() evaluation.delete()