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 numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from pandas.core.frame import DataFrame
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app import models
|
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
|
"""Charge poids des évaluations d'un module et retourne un dataframe
|
||||||
rows = evaluations, columns = UE, value = poids (float).
|
rows = evaluations, columns = UE, value = poids (float).
|
||||||
Les valeurs manquantes (évaluations sans coef vers des UE) sont
|
Les valeurs manquantes (évaluations sans coef vers des UE) sont
|
||||||
remplies par default_poids.
|
remplies par default_poids.
|
||||||
"""
|
"""
|
||||||
modimpl = models.ModuleImpl.query.get(moduleimpl_id)
|
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||||
evaluations = models.Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
|
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
|
||||||
ues = modimpl.formsemestre.query_ues().all()
|
ues = modimpl.formsemestre.query_ues().all()
|
||||||
ue_ids = [ue.id for ue in ues]
|
ue_ids = [ue.id for ue in ues]
|
||||||
evaluation_ids = [evaluation.id for evaluation in evaluations]
|
evaluation_ids = [evaluation.id for evaluation in evaluations]
|
||||||
df = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
|
df = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
|
||||||
for eval_poids in models.EvaluationUEPoids.query.join(
|
for eval_poids in EvaluationUEPoids.query.join(
|
||||||
models.EvaluationUEPoids.evaluation
|
EvaluationUEPoids.evaluation
|
||||||
).filter_by(moduleimpl_id=moduleimpl_id):
|
).filter_by(moduleimpl_id=moduleimpl_id):
|
||||||
df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids
|
df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids
|
||||||
if default_poids is not None:
|
if default_poids is not None:
|
||||||
@ -73,3 +76,79 @@ def check_moduleimpl_conformity(
|
|||||||
== module_evals_poids
|
== module_evals_poids
|
||||||
)
|
)
|
||||||
return check
|
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")
|
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Etud {self.id} {self.nom} {self.prenom}>"
|
||||||
|
|
||||||
|
|
||||||
class Adresse(db.Model):
|
class Adresse(db.Model):
|
||||||
"""Adresse d'un étudiant
|
"""Adresse d'un étudiant
|
||||||
|
@ -45,6 +45,9 @@ class Evaluation(db.Model):
|
|||||||
numero = db.Column(db.Integer)
|
numero = db.Column(db.Integer)
|
||||||
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
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):
|
def to_dict(self):
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
e.pop("_sa_instance_state", None)
|
e.pop("_sa_instance_state", None)
|
||||||
|
@ -35,6 +35,8 @@ class ModuleImpl(db.Model):
|
|||||||
# formule de calcul moyenne:
|
# formule de calcul moyenne:
|
||||||
computation_expr = db.Column(db.Text())
|
computation_expr = db.Column(db.Text())
|
||||||
|
|
||||||
|
evaluations = db.relationship("Evaluation", lazy="dynamic", backref="moduleimpl")
|
||||||
|
|
||||||
|
|
||||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||||
notes_modules_enseignants = db.Table(
|
notes_modules_enseignants = db.Table(
|
||||||
|
@ -10,6 +10,7 @@ from app import models
|
|||||||
from app.comp import moy_mod
|
from app.comp import moy_mod
|
||||||
from app.comp import moy_ue
|
from app.comp import moy_ue
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
|
from app.scodoc.sco_utils import NOTES_ATTENTE, NOTES_NEUTRALISE
|
||||||
|
|
||||||
"""
|
"""
|
||||||
mapp.set_sco_dept("RT")
|
mapp.set_sco_dept("RT")
|
||||||
@ -132,8 +133,11 @@ def test_modules_coefs(test_client):
|
|||||||
assert len(mod.ue_coefs) == 0
|
assert len(mod.ue_coefs) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_modules_conformity(test_client):
|
def _setup_module_evaluation(ue_coefs=(1.0, 2.0, 3.0)):
|
||||||
"""Vérification coefficients module<->UE vs poids des évaluations"""
|
"""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()
|
G, formation_id, ue1_id, ue2_id, ue3_id, module_id = setup_formation_test()
|
||||||
ue1 = models.UniteEns.query.get(ue1_id)
|
ue1 = models.UniteEns.query.get(ue1_id)
|
||||||
ue2 = models.UniteEns.query.get(ue2_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_ues = 3 # 3 UEs dans ce test
|
||||||
nb_mods = 1 # 1 seul module
|
nb_mods = 1 # 1 seul module
|
||||||
# Coef du module vers les UE
|
# 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}
|
coefs_mod = {ue1.id: c1, ue2.id: c2, ue3.id: c3}
|
||||||
mod.set_ue_coef_dict(coefs_mod)
|
mod.set_ue_coef_dict(coefs_mod)
|
||||||
assert mod.get_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,
|
coefficient=0,
|
||||||
)
|
)
|
||||||
evaluation_id = _e1["evaluation_id"]
|
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
|
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
|
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_id)
|
||||||
evaluation.set_ue_poids_dict({ue1.id: p1, ue2.id: p2})
|
evaluation.set_ue_poids_dict({ue1.id: p1, ue2.id: p2})
|
||||||
assert evaluation.get_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
|
# 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 isinstance(modules_coefficients, pd.DataFrame)
|
||||||
assert modules_coefficients.shape == (nb_ues, nb_mods)
|
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 isinstance(evals_poids, pd.DataFrame)
|
||||||
assert all(evals_poids.dtypes == np.float64)
|
assert all(evals_poids.dtypes == np.float64)
|
||||||
assert evals_poids.shape == (nb_evals, nb_ues)
|
assert evals_poids.shape == (nb_evals, nb_ues)
|
||||||
assert not moy_mod.check_moduleimpl_conformity(
|
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