forked from ScoDoc/ScoDoc
WIP: chargement notes et calcul moy module
This commit is contained in:
parent
042d5080b2
commit
780a117fbd
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user