WIP: chargement notes et calcul moy module

This commit is contained in:
Emmanuel Viennet 2021-11-20 16:35:09 +01:00
parent 042d5080b2
commit 780a117fbd
5 changed files with 160 additions and 11 deletions

View File

@ -33,25 +33,28 @@ moyenne générale d'une UE.
"""
import numpy as np
import pandas as pd
from pandas.core.frame import DataFrame
from app import db
from app import models
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
from app.scodoc import sco_utils as scu
def df_load_evaluations_poids(moduleimpl_id: int, default_poids=1.0) -> pd.DataFrame:
def df_load_evaluations_poids(moduleimpl_id: int, default_poids=0.0) -> pd.DataFrame:
"""Charge poids des évaluations d'un module et retourne un dataframe
rows = evaluations, columns = UE, value = poids (float).
Les valeurs manquantes (évaluations sans coef vers des UE) sont
remplies par default_poids.
"""
modimpl = models.ModuleImpl.query.get(moduleimpl_id)
evaluations = models.Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
modimpl = ModuleImpl.query.get(moduleimpl_id)
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
ues = modimpl.formsemestre.query_ues().all()
ue_ids = [ue.id for ue in ues]
evaluation_ids = [evaluation.id for evaluation in evaluations]
df = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
for eval_poids in models.EvaluationUEPoids.query.join(
models.EvaluationUEPoids.evaluation
for eval_poids in EvaluationUEPoids.query.join(
EvaluationUEPoids.evaluation
).filter_by(moduleimpl_id=moduleimpl_id):
df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids
if default_poids is not None:
@ -73,3 +76,79 @@ def check_moduleimpl_conformity(
== module_evals_poids
)
return check
def df_load_modimpl_notes(moduleimpl_id: int) -> pd.DataFrame:
"""Construit un dataframe avec toutes les notes des évaluations du module.
colonnes: evaluation_id (le nom de la colonne est l'evaluation_id en str)
index (lignes): etudid
L'ensemble des étudiants est celui des inscrits au module.
Valeurs des notes:
note : float (valeur enregistrée brute, pas normalisée sur 20)
pas de note: NaN
absent: 0.
excusé: NOTES_NEUTRALISE (voir sco_utils)
attente: NOTES_ATTENTE
N'utilise pas de cache ScoDoc.
"""
etudids = [e.etudid for e in ModuleImpl.query.get(moduleimpl_id).inscriptions]
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id)
df = pd.DataFrame(index=etudids, dtype=float) # empty df with all students
for evaluation in evaluations:
eval_df = pd.read_sql(
"""SELECT etudid, value AS "%(evaluation_id)s"
FROM notes_notes
WHERE evaluation_id=%(evaluation_id)s""",
db.engine,
params={"evaluation_id": evaluation.evaluation_id},
index_col="etudid",
)
# Remplace les ABS (NULL en BD, donc NaN ici) par des zéros.
eval_df.fillna(value=0.0, inplace=True)
df = df.merge(eval_df, how="outer", left_index=True, right_index=True)
return df
def compute_module_moy(evals_notes: pd.DataFrame, evals_poids: pd.DataFrame):
"""Calcule les moyennes des étudiants dans ce module
- evals_notes : DataFrame, colonnes: EVALS, Lignes: etudid
valeur: float, ou NOTES_ATTENTE ou NOTES_NEUTRALISE
Les NaN (ABS) doivent avoir déjà été remplacés par des zéros.
- evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
Résultat: DataFrame, colonnes UE, lignes etud
= la note de l'étudiant dans chaque UE pour ce module.
ou NaN si les évaluations (dans lesquelles l'étudiant à des notes)
ne donnent pas de coef vers cette UE.
"""
nb_etuds = len(evals_notes)
nb_ues = evals_poids.shape[1]
etud_moy_module_arr = np.zeros((nb_etuds, nb_ues))
evals_poids_arr = evals_poids.to_numpy().transpose()
evals_notes_arr = evals_notes.values # .to_numpy()
val_neutres = np.array((scu.NOTES_NEUTRALISE, scu.NOTES_ATTENTE))
for i in range(nb_etuds):
note_vect = evals_notes_arr[
i
] # array [note_ue1, note_ue2, ...] de l'étudiant i
# Les poids des évals pour cet étudiant: là où il a des notes non neutralisées
evals_poids_etud_arr = np.where(
np.isin(note_vect, val_neutres, invert=True), evals_poids_arr, 0.0
)
# Calcule la moyenne pondérée sur les notes disponibles
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_module_arr[i] = (note_vect * evals_poids_etud_arr).sum(
axis=1
) / evals_poids_etud_arr.sum(axis=1)
etud_moy_module_df = pd.DataFrame(
etud_moy_module_arr, index=evals_notes.index, columns=evals_poids.columns
)
return etud_moy_module_df

View File

@ -46,6 +46,9 @@ class Identite(db.Model):
#
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
def __repr__(self):
return f"<Etud {self.id} {self.nom} {self.prenom}>"
class Adresse(db.Model):
"""Adresse d'un étudiant

View File

@ -45,6 +45,9 @@ class Evaluation(db.Model):
numero = db.Column(db.Integer)
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
def __repr__(self):
return f"<Evaluation {self.id} {self.jour.isoformat()}"
def to_dict(self):
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)

View File

@ -35,6 +35,8 @@ class ModuleImpl(db.Model):
# formule de calcul moyenne:
computation_expr = db.Column(db.Text())
evaluations = db.relationship("Evaluation", lazy="dynamic", backref="moduleimpl")
# Enseignants (chargés de TD ou TP) d'un moduleimpl
notes_modules_enseignants = db.Table(

View File

@ -10,6 +10,7 @@ from app import models
from app.comp import moy_mod
from app.comp import moy_ue
from app.scodoc import sco_codes_parcours
from app.scodoc.sco_utils import NOTES_ATTENTE, NOTES_NEUTRALISE
"""
mapp.set_sco_dept("RT")
@ -132,8 +133,11 @@ def test_modules_coefs(test_client):
assert len(mod.ue_coefs) == 0
def test_modules_conformity(test_client):
"""Vérification coefficients module<->UE vs poids des évaluations"""
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)
@ -142,7 +146,7 @@ def test_modules_conformity(test_client):
nb_ues = 3 # 3 UEs dans ce test
nb_mods = 1 # 1 seul module
# Coef du module vers les UE
c1, c2, c3 = 1.0, 2.0, 3.0
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
@ -171,19 +175,77 @@ def test_modules_conformity(test_client):
coefficient=0,
)
evaluation_id = _e1["evaluation_id"]
return formation_id, 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()
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.set_ue_poids_dict({ue1.id: p1, ue2.id: p2})
assert evaluation.get_ue_poids_dict() == {ue1.id: p1, ue2.id: p2}
# 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(formation_id)
modules_coefficients, _ues, _modules = moy_ue.df_load_ue_coefs(
formation_id, semestre_idx
)
assert isinstance(modules_coefficients, pd.DataFrame)
assert modules_coefficients.shape == (nb_ues, nb_mods)
evals_poids = moy_mod.df_load_evaluations_poids(moduleimpl_id)
evals_poids = moy_mod.df_load_evaluations_poids(evaluation.moduleimpl_id)
assert isinstance(evals_poids, pd.DataFrame)
assert all(evals_poids.dtypes == np.float64)
assert evals_poids.shape == (nb_evals, nb_ues)
assert not moy_mod.check_moduleimpl_conformity(
modimpl, evals_poids, modules_coefficients
evaluation.moduleimpl, evals_poids, modules_coefficients
)
def test_module_moy():
"""Vérification calcul moyenne d'un module
(calcul bas niveau)
"""
# Repris du notebook CalculNotesBUT.ipynb
data = [ # Les notes de chaque étudiant dans les 2 evals:
{
"EVAL1": 11.0,
"EVAL2": 16.0,
},
{
"EVAL1": np.NaN, # une absence (NaN)
"EVAL2": 17.0,
},
{
"EVAL1": 13.0,
"EVAL2": NOTES_NEUTRALISE, # une abs EXC
},
{
"EVAL1": 14.0,
"EVAL2": 19.0,
},
{
"EVAL1": NOTES_ATTENTE, # une ATT (traitée comme EXC)
"EVAL2": np.NaN, # et une ABS
},
]
evals_notes = pd.DataFrame(
data, index=["etud1", "etud2", "etud3", "etud4", "etud5"]
)
# Poids des évaluations (1 ligne / évaluation)
data = [
{"UE1": 1, "UE2": 0, "UE3": 0},
{"UE1": 2, "UE2": 5, "UE3": 0},
]
evals_poids = pd.DataFrame(data, index=["EVAL1", "EVAL2"], dtype=float)
etud_moy_module_df = moy_mod.compute_module_moy(evals_notes, evals_poids)
NAN = 666.0 # pour pouvoir comparer NaN et NaN (car NaN != NaN)
r = etud_moy_module_df.fillna(NAN)
tuple(r.loc["etud1"]) == (14 + 1 / 3, 16.0, NAN)
tuple(r.loc["etud2"]) == (11 + 1 / 3, 17.0, NAN)
tuple(r.loc["etud3"]) == (13, NAN, NAN)
tuple(r.loc["etud4"]) == (17 + 1 / 3, 19, NAN)
tuple(r.loc["etud5"]) == (0.0, 0.0, NAN)
# note: les notes UE3 sont toutes NAN car les poids vers l'UE3 sont nuls