WIP: calcul moyennes UE BUT

This commit is contained in:
Emmanuel Viennet 2021-11-28 16:31:33 +01:00
parent be8925e163
commit 09eb73be4a
6 changed files with 212 additions and 142 deletions

View File

@ -88,7 +88,7 @@ def df_load_modimpl_notes(moduleimpl_id: int) -> pd.DataFrame:
Résultat: (evals_notes, liste de évaluations du moduleimpl)
L'ensemble des étudiants est celui des inscrits au module.
L'ensemble des étudiants est celui des inscrits au SEMESTRE.
Les notes renvoyées sont "brutes" (séries de floats) et peuvent prendre les valeurs:
note : float (valeur enregistrée brute, non normalisée sur 20)
@ -99,7 +99,10 @@ def df_load_modimpl_notes(moduleimpl_id: int) -> pd.DataFrame:
N'utilise pas de cache ScoDoc.
"""
etudids = [e.etudid for e in ModuleImpl.query.get(moduleimpl_id).inscriptions]
# L'index du dataframe est la liste des étudiants inscrits au semestre:
etudids = [
e.etudid for e in ModuleImpl.query.get(moduleimpl_id).formsemestre.inscriptions
]
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id)
evals_notes = pd.DataFrame(index=etudids, dtype=float) # empty df with all students
@ -165,10 +168,10 @@ def compute_module_moy(
# Calcule la moyenne pondérée sur les notes disponibles
evals_notes_stacked = np.stack([evals_notes] * nb_ues, axis=2)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_module = np.sum(
etuds_moy_module = np.sum(
evals_poids_etuds * evals_notes_stacked, axis=1
) / np.sum(evals_poids_etuds, axis=1)
etud_moy_module_df = pd.DataFrame(
etud_moy_module, index=evals_notes_df.index, columns=evals_poids_df.columns
etuds_moy_module_df = pd.DataFrame(
etuds_moy_module, index=evals_notes_df.index, columns=evals_poids_df.columns
)
return etud_moy_module_df
return etuds_moy_module_df

View File

@ -32,28 +32,148 @@ import pandas as pd
from app import db
from app import models
from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef
from app.comp import moy_mod
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_codes_parcours
def df_load_ue_coefs(formation_id: int, semestre_idx: int) -> pd.DataFrame:
"""Load coefs of all modules in formation and returns a DataFrame
rows = UEs, columns = modules, value = coef.
On considère toutes les UE et modules du semestre.
Unspecified coefs (not defined in db) are set to zero.
def df_load_module_coefs(formation_id: int, semestre_idx: int) -> pd.DataFrame:
"""Charge les coefs des modules de la formation pour le semestre indiqué.
Ces coefs lient les modules à chaque UE.
Résultat: (module_coefs_df, ues, modules)
DataFrame rows = UEs, columns = modules, value = coef.
Considère toutes les UE (sauf sport) et modules du semestre.
Les coefs non définis (pas en base) sont mis à zéro.
Si semestre_idx None, prend toutes les UE de la formation.
"""
ues = models.UniteEns.query.filter_by(formation_id=formation_id)
modules = models.Module.query.filter_by(formation_id=formation_id)
ues = UniteEns.query.filter_by(formation_id=formation_id).filter(
UniteEns.type != sco_codes_parcours.UE_SPORT
)
modules = Module.query.filter_by(formation_id=formation_id)
if semestre_idx is not None:
ues = ues.filter_by(semestre_idx=semestre_idx)
modules = modules.filter_by(semestre_id=semestre_idx)
ues = ues.all()
modules = modules.all()
ue_ids = [ue.id for ue in ues]
module_ids = [module.id for module in modules]
df = pd.DataFrame(columns=module_ids, index=ue_ids, dtype=float)
module_coefs_df = pd.DataFrame(columns=module_ids, index=ue_ids, dtype=float)
for mod_coef in (
db.session.query(models.ModuleUECoef)
.filter(models.UniteEns.formation_id == formation_id)
.filter(models.ModuleUECoef.ue_id == models.UniteEns.id)
db.session.query(ModuleUECoef)
.filter(UniteEns.formation_id == formation_id)
.filter(ModuleUECoef.ue_id == UniteEns.id)
):
df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef
df.fillna(value=0, inplace=True)
return df, ues, modules
module_coefs_df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef
module_coefs_df.fillna(value=0, inplace=True)
return module_coefs_df, ues, modules
def df_load_modimpl_coefs(formsemestre: models.FormSemestre) -> pd.DataFrame:
"""Charge les coefs des modules du formsemestre indiqué.
Comme df_load_module_coefs mais prend seulement les UE
et modules du formsemestre.
Résultat: (module_coefs_df, ues, modules)
DataFrame rows = UEs, columns = modimpl, value = coef.
"""
ues = formsemestre.query_ues().all()
ue_ids = [x.id for x in ues]
modimpls = formsemestre.modimpls.all()
modimpl_ids = [x.id for x in modimpls]
mod2impl = {m.module.id: m.id for m in modimpls}
modimpl_coefs_df = pd.DataFrame(columns=modimpl_ids, index=ue_ids, dtype=float)
mod_coefs = (
db.session.query(ModuleUECoef)
.filter(ModuleUECoef.module_id == ModuleImpl.module_id)
.filter(ModuleImpl.formsemestre_id == formsemestre.id)
)
for mod_coef in mod_coefs:
modimpl_coefs_df[mod2impl[mod_coef.module_id]][mod_coef.ue_id] = mod_coef.coef
modimpl_coefs_df.fillna(value=0, inplace=True)
return modimpl_coefs_df, ues, modimpls
def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
"""Réuni les notes moyennes des modules du semestre en un "cube"
modimpls_notes : liste des moyennes de module
(DataFrames rendus par compute_module_moy, (etud x UE))
Resultat: ndarray (etud x module x UE)
"""
modimpls_notes_arr = [df.values for df in modimpls_notes]
modimpls_notes = np.stack(modimpls_notes_arr)
# passe de (mod x etud x ue) à (etud x mod x UE)
return modimpls_notes.swapaxes(0, 1)
def notes_sem_load_cube(formsemestre_id):
"""Calcule le cube des notes du semestre
(charge toutes les notes, calcule les moyenne des modules
et assemble le cube)
Resultat: ndarray (etuds x modimpls x UEs)
"""
formsemestre = FormSemestre.query.get(formsemestre_id)
modimpls_notes = []
for modimpl in formsemestre.modimpls:
evals_notes, evaluations = moy_mod.df_load_modimpl_notes(modimpl.id)
evals_poids, ues = moy_mod.df_load_evaluations_poids(modimpl.id)
etuds_moy_module = moy_mod.compute_module_moy(
evals_notes, evals_poids, evaluations
)
modimpls_notes.append(etuds_moy_module)
return notes_sem_assemble_cube(modimpls_notes)
def compute_ue_moys(
sem_cube: np.array,
etuds: list,
modimpls: list,
ues: list,
module_inscr_df: pd.DataFrame,
module_coefs_df: pd.DataFrame,
) -> pd.DataFrame:
"""Calcul de la moyenne d'UE
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
NI non inscrit à (au moins un) module de cette UE
NA pas de notes disponibles
ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici]
sem_cube: notes moyennes aux modules
ndarray (etuds x modimpls x UEs)
(floats avec des NaN)
etuds : lites des étudiants (dim. 0 du cube)
modimpls : liste des modules à considérer (dim. 1 du cube)
ues : liste des UE (dim. 2 du cube)
module_inscr_df: matrice d'inscription du semestre (etud x modimpl)
module_coefs_df: matrice coefficients (UE x modimpl)
Resultat: DataFrame columns UE, rows etudid
"""
nb_etuds, nb_modules, nb_ues = sem_cube.shape
assert len(etuds) == nb_etuds
assert len(modimpls) == nb_modules
assert len(ues) == nb_ues
assert module_inscr_df.shape[0] == nb_etuds
assert module_inscr_df.shape[1] == nb_modules
assert module_coefs_df.shape[0] == nb_ues
assert module_coefs_df.shape[1] == nb_modules
module_inscr = module_inscr_df.values
modules_coefs = module_coefs_df.values
#
# version non vectorisée sur les etuds:
etud_moy_ue = np.zeros((nb_etuds, nb_ues))
for i in range(nb_etuds):
coefs = module_inscr[i] * modules_coefs
etud_moy_ue[i] = (sem_cube[i].transpose() * coefs).sum(axis=1) / coefs.sum(
axis=1
)
return pd.DataFrame(
etud_moy_ue, index=module_inscr_df.index, columns=module_coefs_df.index
)

View File

@ -83,6 +83,12 @@ class FormSemestre(db.Model):
"FormsemestreEtape", cascade="all,delete", backref="formsemestre"
)
modimpls = db.relationship("ModuleImpl", backref="formsemestre", lazy="dynamic")
etuds = db.relationship(
"Identite",
secondary="notes_formsemestre_inscription",
viewonly=True,
lazy="dynamic",
)
# Ancien id ScoDoc7 pour les migrations de bases anciennes
# ne pas utiliser après migrate_scodoc7_dept_archives

View File

@ -232,6 +232,8 @@ def _make_table_notes(
is_apc = module.formation.get_parcours().APC_SAE
if is_apc:
evals_poids, ues = moy_mod.df_load_evaluations_poids(moduleimpl_id)
if not ues:
is_apc = False
else:
evals_poids, ues = None, None
sem = sco_formsemestre.get_formsemestre(modimpl["formsemestre_id"])
@ -772,11 +774,11 @@ def _add_apc_columns(
# on va y ajouter une clé par UE du semestre
evals_notes, evaluations = moy_mod.df_load_modimpl_notes(moduleimpl_id)
etud_moy_module = moy_mod.compute_module_moy(evals_notes, evals_poids, evaluations)
etuds_moy_module = moy_mod.compute_module_moy(evals_notes, evals_poids, evaluations)
for row in rows:
for ue in ues:
moy_ue = etud_moy_module[ue.id].get(row["etudid"], "?")
moy_ue = etuds_moy_module[ue.id].get(row["etudid"], "?")
row[f"moy_ue_{ue.id}"] = scu.fmt_note(moy_ue, keep_numeric=keep_numeric)
row[f"_moy_ue_{ue.id}_class"] = "moy_ue"
for ue in ues:

View File

@ -76,7 +76,7 @@ def table_modules_ue_coefs(formation_id, semestre_idx=None):
_ = models.Formation.query.get_or_404(formation_id) # check
if semestre_idx == "":
semestre_idx = None
df, ues, modules = moy_ue.df_load_ue_coefs(formation_id, semestre_idx)
df, ues, modules = moy_ue.df_load_module_coefs(formation_id, semestre_idx)
# Titre des modules, en ligne
col_titres_mods = [
{

View File

@ -1,17 +1,17 @@
"""
Test modèles évaluations avec poids BUT
et calcul moyennes modules
"""
import numpy as np
import pandas as pd
from app.models.etudiants import Identite
from tests.unit import sco_fake_gen
from tests.unit import setup
from app import db
from app import models
from app.comp import moy_mod
from app.comp import moy_ue
from app.models import Evaluation
from app.scodoc import sco_codes_parcours, sco_saisie_notes
from app.scodoc import sco_saisie_notes
from app.scodoc.sco_utils import NOTES_ATTENTE, NOTES_NEUTRALISE
"""
@ -23,43 +23,9 @@ login_user(admin_user)
"""
def setup_formation_test():
G = sco_fake_gen.ScoFake(verbose=False)
_f = G.create_formation(
acronyme="F3",
titre="Formation 2",
titre_officiel="Titre officiel 2",
type_parcours=sco_codes_parcours.ParcoursBUT.TYPE_PARCOURS,
)
_ue1 = G.create_ue(
formation_id=_f["formation_id"], acronyme="UE1", titre="ue 1", semestre_idx=2
)
_ue2 = G.create_ue(
formation_id=_f["formation_id"], acronyme="UE2", titre="ue 2", semestre_idx=2
)
_ue3 = G.create_ue(
formation_id=_f["formation_id"], acronyme="UE3", titre="ue 3", semestre_idx=2
)
# une 4eme UE en dehors du semestre 2
_ = G.create_ue(
formation_id=_f["formation_id"], acronyme="UE41", titre="ue 41", semestre_idx=4
)
_mat = G.create_matiere(ue_id=_ue1["ue_id"], titre="matière test")
_mod = G.create_module(
matiere_id=_mat["matiere_id"],
code="TSM1",
coefficient=1.0,
titre="module test",
ue_id=_ue1["ue_id"],
formation_id=_f["formation_id"],
semestre_id=2,
)
return G, _f["id"], _ue1["id"], _ue2["id"], _ue3["id"], _mod["id"]
def test_evaluation_poids(test_client):
"""Association de poids vers les UE"""
G, formation_id, ue1_id, ue2_id, ue3_id, module_id = setup_formation_test()
G, formation_id, ue1_id, ue2_id, ue3_id, module_ids = setup.build_formation_test()
sem = G.create_formsemestre(
formation_id=formation_id,
semestre_id=1,
@ -67,7 +33,7 @@ def test_evaluation_poids(test_client):
date_fin="30/06/2021",
) # formsemestre_id=716
mi = G.create_moduleimpl(
module_id=module_id,
module_id=module_ids[0],
formsemestre_id=sem["formsemestre_id"],
)
moduleimpl_id = mi["id"]
@ -114,10 +80,10 @@ def test_evaluation_poids(test_client):
def test_modules_coefs(test_client):
"""Coefs vers les UE (BUT)"""
G, formation_id, ue1_id, ue2_id, ue3_id, module_id = setup_formation_test()
G, formation_id, ue1_id, ue2_id, ue3_id, module_ids = setup.build_formation_test()
ue1 = models.UniteEns.query.get(ue1_id)
ue2 = models.UniteEns.query.get(ue2_id)
mod = models.Module.query.get(module_id)
mod = models.Module.query.get(module_ids[0])
coef = 2.5
mod.set_ue_coef(ue1, coef)
db.session.commit()
@ -135,64 +101,27 @@ def test_modules_coefs(test_client):
assert len(mod.ue_coefs) == 0
def _setup_module_evaluation(ue_coefs=(1.0, 2.0, 3.0)):
"""Utilisé dans plusieurs tests:
- création formation 3 UE, 1 module
- 1 semestre, 1 moduleimpl, 1 eval
"""
G, formation_id, ue1_id, ue2_id, ue3_id, module_id = setup_formation_test()
ue1 = models.UniteEns.query.get(ue1_id)
ue2 = models.UniteEns.query.get(ue2_id)
ue3 = models.UniteEns.query.get(ue3_id)
mod = models.Module.query.get(module_id)
nb_ues = 3 # 3 UEs dans ce test
nb_mods = 1 # 1 seul module
# Coef du module vers les UE
c1, c2, c3 = ue_coefs
coefs_mod = {ue1.id: c1, ue2.id: c2, ue3.id: c3}
mod.set_ue_coef_dict(coefs_mod)
assert mod.get_ue_coef_dict() == coefs_mod
# Mise en place:
sem = G.create_formsemestre(
formation_id=formation_id,
semestre_id=2,
date_debut="01/01/2021",
date_fin="30/06/2021",
)
mi = G.create_moduleimpl(
module_id=module_id,
formsemestre_id=sem["formsemestre_id"],
)
moduleimpl_id = mi["id"]
modimpl = models.ModuleImpl.query.get(moduleimpl_id)
assert modimpl.formsemestre.formation.get_parcours().APC_SAE # BUT
# Check ModuleImpl
ues = modimpl.formsemestre.query_ues().all()
assert len(ues) == 3
#
_e1 = G.create_evaluation(
moduleimpl_id=moduleimpl_id,
jour="01/01/2021",
description="evaluation 1",
coefficient=0,
)
evaluation_id = _e1["evaluation_id"]
return G, formation_id, sem, evaluation_id, ue1, ue2, ue3
def test_module_conformity(test_client):
"""Vérification coefficients module<->UE vs poids des évaluations"""
_, formation_id, _, evaluation_id, ue1, ue2, ue3 = _setup_module_evaluation()
(
_,
formation_id,
_,
evaluation_ids,
ue1,
ue2,
ue3,
) = setup.build_modules_with_evaluations()
semestre_idx = 2
nb_ues = 3 # 3 UEs dans ce test
nb_mods = 1 # 1 seul module
nb_evals = 1 # 1 seule evaluation pour l'instant
p1, p2, p3 = 1.0, 2.0, 0.0 # poids de l'éval vers les UE 1, 2 et 3
evaluation = models.Evaluation.query.get(evaluation_id)
evaluation = models.Evaluation.query.get(evaluation_ids[0])
evaluation.set_ue_poids_dict({ue1.id: p1, ue2.id: p2, ue3.id: p3})
assert evaluation.get_ue_poids_dict() == {ue1.id: p1, ue2.id: p2, ue3.id: p3}
# On n'est pas conforme car p3 est nul alors que c3 est non nul
modules_coefficients, _ues, _modules = moy_ue.df_load_ue_coefs(
modules_coefficients, _ues, _modules = moy_ue.df_load_module_coefs(
formation_id, semestre_idx
)
assert isinstance(modules_coefficients, pd.DataFrame)
@ -247,11 +176,11 @@ def test_module_moy_elem(test_client):
Evaluation(note_max=20.0, coefficient=1.0),
Evaluation(note_max=20.0, coefficient=1.0),
]
etud_moy_module_df = moy_mod.compute_module_moy(
etuds_moy_module_df = moy_mod.compute_module_moy(
evals_notes_df.fillna(0.0), evals_poids_df, evaluations
)
NAN = 666.0 # pour pouvoir comparer NaN et NaN (car NaN != NaN)
r = etud_moy_module_df.fillna(NAN)
r = etuds_moy_module_df.fillna(NAN)
assert tuple(r.loc["etud1"]) == (14 + 1 / 3, 16.0, NAN)
assert tuple(r.loc["etud2"]) == (11 + 1 / 3, 17.0, NAN)
assert tuple(r.loc["etud3"]) == (13, NAN, NAN)
@ -263,11 +192,19 @@ def test_module_moy_elem(test_client):
def test_module_moy(test_client):
"""Test calcul moyenne module avec saisie des notes via ScoDoc"""
coef_e1, coef_e2 = 7.0, 11.0 # coefficients des évaluations
G, formation_id, sem, evaluation1_id, ue1, ue2, ue3 = _setup_module_evaluation()
(
G,
formation_id,
sem,
evaluation1_ids,
ue1,
ue2,
ue3,
) = setup.build_modules_with_evaluations()
etud = G.create_etud(nom="test")
G.inscrit_etudiant(sem, etud)
etudid = etud["etudid"]
evaluation1 = models.Evaluation.query.get(evaluation1_id)
evaluation1 = models.Evaluation.query.get(evaluation1_ids[0])
# Crée une deuxième évaluation dans le même moduleimpl:
evaluation2_id = G.create_evaluation(
moduleimpl_id=evaluation1.moduleimpl_id,
@ -300,51 +237,53 @@ def test_module_moy(test_client):
assert evals_poids.shape == (nb_evals, nb_ues)
evals_notes, evaluations = moy_mod.df_load_modimpl_notes(moduleimpl_id)
assert evals_notes[str(evaluations[0].id)].dtype == np.float64
etud_moy_module = moy_mod.compute_module_moy(
etuds_moy_module = moy_mod.compute_module_moy(
evals_notes, evals_poids, evaluations
)
return etud_moy_module
return etuds_moy_module
# --- Notes ordinaires:
note1, note2 = 11.0, 12.0
sum_copo1 = e1p1 * coef_e1 + e2p1 * coef_e2 # coefs vers UE1
sum_copo2 = e1p2 * coef_e1 + e2p2 * coef_e2 #
etud_moy_module = change_notes(note1, note2)
moy_ue1 = etud_moy_module[ue1.id][etudid]
etuds_moy_module = change_notes(note1, note2)
moy_ue1 = etuds_moy_module[ue1.id][etudid]
assert moy_ue1 == ((note1 * e1p1 * coef_e1) + (note2 * e2p1 * coef_e2)) / sum_copo1
moy_ue2 = etud_moy_module[ue2.id][etudid]
moy_ue2 = etuds_moy_module[ue2.id][etudid]
assert moy_ue2 == ((note1 * e1p2 * coef_e1) + (note2 * e2p2 * coef_e2)) / sum_copo2
moy_ue3 = etud_moy_module[ue3.id][etudid]
moy_ue3 = etuds_moy_module[ue3.id][etudid]
assert np.isnan(moy_ue3) # car les poids vers UE3 sont nuls
# --- Une Note ABS (comptée comme zéro)
etud_moy_module = change_notes(None, note2)
assert etud_moy_module[ue1.id][etudid] == (note2 * e2p1 * coef_e2) / sum_copo1
assert etud_moy_module[ue2.id][etudid] == (note2 * e2p2 * coef_e2) / sum_copo2
assert np.isnan(etud_moy_module[ue3.id][etudid])
etuds_moy_module = change_notes(None, note2)
assert etuds_moy_module[ue1.id][etudid] == (note2 * e2p1 * coef_e2) / sum_copo1
assert etuds_moy_module[ue2.id][etudid] == (note2 * e2p2 * coef_e2) / sum_copo2
assert np.isnan(etuds_moy_module[ue3.id][etudid])
# --- Deux notes ABS
etud_moy_module = change_notes(None, None)
assert etud_moy_module[ue1.id][etudid] == 0.0
assert etud_moy_module[ue2.id][etudid] == 0.0
assert np.isnan(etud_moy_module[ue3.id][etudid])
etuds_moy_module = change_notes(None, None)
assert etuds_moy_module[ue1.id][etudid] == 0.0
assert etuds_moy_module[ue2.id][etudid] == 0.0
assert np.isnan(etuds_moy_module[ue3.id][etudid])
# --- Note EXC
etud_moy_module = change_notes(NOTES_ATTENTE, note2)
assert np.isnan(etud_moy_module[ue1.id][etudid]) # car l'eval 2 ne touche que l'UE2
assert etud_moy_module[ue2.id][etudid] == note2
assert np.isnan(etud_moy_module[ue3.id][etudid])
etuds_moy_module = change_notes(NOTES_ATTENTE, note2)
assert np.isnan(
etuds_moy_module[ue1.id][etudid]
) # car l'eval 2 ne touche que l'UE2
assert etuds_moy_module[ue2.id][etudid] == note2
assert np.isnan(etuds_moy_module[ue3.id][etudid])
# --- Toutes notes ATT (ATT se traite comme EXC)
etud_moy_module = change_notes(NOTES_NEUTRALISE, NOTES_NEUTRALISE)
assert np.isnan(etud_moy_module[ue1.id][etudid])
assert np.isnan(etud_moy_module[ue2.id][etudid])
assert np.isnan(etud_moy_module[ue3.id][etudid])
etuds_moy_module = change_notes(NOTES_NEUTRALISE, NOTES_NEUTRALISE)
assert np.isnan(etuds_moy_module[ue1.id][etudid])
assert np.isnan(etuds_moy_module[ue2.id][etudid])
assert np.isnan(etuds_moy_module[ue3.id][etudid])
# --- Barème sur 37
evaluation2.note_max = 37.0
note1, note2 = 11.0, 12.0
note_2_37 = note2 / 20 * 37
etud_moy_module = change_notes(note1, note_2_37)
moy_ue1 = etud_moy_module[ue1.id][etudid]
etuds_moy_module = change_notes(note1, note_2_37)
moy_ue1 = etuds_moy_module[ue1.id][etudid]
assert moy_ue1 == ((note1 * e1p1 * coef_e1) + (note2 * e2p1 * coef_e2)) / sum_copo1
moy_ue2 = etud_moy_module[ue2.id][etudid]
moy_ue2 = etuds_moy_module[ue2.id][etudid]
assert moy_ue2 == ((note1 * e1p2 * coef_e1) + (note2 * e2p2 * coef_e2)) / sum_copo2
moy_ue3 = etud_moy_module[ue3.id][etudid]
moy_ue3 = etuds_moy_module[ue3.id][etudid]
assert np.isnan(moy_ue3) # car les poids vers UE3 sont nuls