forked from ScoDoc/ScoDoc
WIP: unification calculs
This commit is contained in:
parent
dc9bba3f04
commit
1a472bd19d
@ -10,52 +10,11 @@
|
||||
import datetime
|
||||
from flask import url_for, g
|
||||
|
||||
from app.comp import moy_ue, moy_sem, inscr_mod
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc import sco_bulletins_json
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_utils import fmt_note
|
||||
from app.comp.res_sem import ResultatsSemestre, NotesTableCompat
|
||||
|
||||
|
||||
class ResultatsSemestreBUT(NotesTableCompat):
|
||||
"""Résultats BUT: organisation des calculs"""
|
||||
|
||||
_cached_attrs = NotesTableCompat._cached_attrs + ()
|
||||
|
||||
def __init__(self, formsemestre):
|
||||
super().__init__(formsemestre)
|
||||
|
||||
if not self.load_cached():
|
||||
self.compute()
|
||||
self.store()
|
||||
|
||||
def compute(self):
|
||||
"Charge les notes et inscriptions et calcule toutes les moyennes"
|
||||
(
|
||||
self.sem_cube,
|
||||
self.modimpls_evals_poids,
|
||||
self.modimpls_results,
|
||||
) = moy_ue.notes_sem_load_cube(self.formsemestre)
|
||||
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
||||
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
self.formsemestre, ues=self.ues, modimpls=self.modimpls
|
||||
)
|
||||
# l'idx de la colonne du mod modimpl.id est
|
||||
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
||||
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
||||
self.etud_moy_ue = moy_ue.compute_ue_moys(
|
||||
self.sem_cube,
|
||||
self.etuds,
|
||||
self.modimpls,
|
||||
self.ues,
|
||||
self.modimpl_inscr_df,
|
||||
self.modimpl_coefs_df,
|
||||
)
|
||||
self.etud_moy_gen = moy_sem.compute_sem_moys(
|
||||
self.etud_moy_ue, self.modimpl_coefs_df
|
||||
)
|
||||
self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
|
||||
|
||||
class BulletinBUT(ResultatsSemestreBUT):
|
||||
|
@ -27,6 +27,8 @@
|
||||
|
||||
"""Fonctions de calcul des moyennes de modules (modules, ressources ou SAÉ)
|
||||
|
||||
Pour les formations classiques et le BUT
|
||||
|
||||
Rappel: pour éviter les confusions, on appelera *poids* les coefficients d'une
|
||||
évaluation dans un module, et *coefficients* ceux utilisés pour le calcul de la
|
||||
moyenne générale d'une UE.
|
||||
@ -49,10 +51,11 @@ class EvaluationEtat:
|
||||
is_complete: bool
|
||||
|
||||
|
||||
class ModuleImplResultsAPC:
|
||||
"""Les notes des étudiants d'un moduleimpl.
|
||||
Les poids des évals sont à part car on a a besoin sans les notes pour les tableaux
|
||||
de bord.
|
||||
class ModuleImplResults:
|
||||
"""Classe commune à toutes les formations (standard et APC).
|
||||
Les notes des étudiants d'un moduleimpl.
|
||||
Les poids des évals sont à part car on en a besoin sans les notes pour les
|
||||
tableaux de bord.
|
||||
Les attributs sont tous des objets simples cachables dans Redis;
|
||||
les caches sont gérés par ResultatsSemestre.
|
||||
"""
|
||||
@ -181,6 +184,31 @@ class ModuleImplResultsAPC:
|
||||
)
|
||||
]
|
||||
|
||||
def get_evaluations_coefs(self, moduleimpl: ModuleImpl) -> np.array:
|
||||
"""Coefficients des évaluations, met à zéro ceux des évals incomplètes.
|
||||
Résultat: 2d-array of floats, shape (nb_evals, 1)
|
||||
"""
|
||||
return (
|
||||
np.array(
|
||||
[e.coefficient for e in moduleimpl.evaluations],
|
||||
dtype=float,
|
||||
)
|
||||
* self.evaluations_completes
|
||||
).reshape(-1, 1)
|
||||
|
||||
def get_eval_notes_sur_20(self, moduleimpl: ModuleImpl) -> np.array:
|
||||
"""Les notes des évaluations,
|
||||
remplace les ATT, EXC, ABS, NaN par zéro et mets les notes sur 20.
|
||||
Résultat: 2d array of floats, shape nb_etuds x nb_evaluations
|
||||
"""
|
||||
return np.where(
|
||||
self.evals_notes.values > scu.NOTES_ABSENCE, self.evals_notes.values, 0.0
|
||||
) / [e.note_max / 20.0 for e in moduleimpl.evaluations]
|
||||
|
||||
|
||||
class ModuleImplResultsAPC(ModuleImplResults):
|
||||
"Calcul des moyennes de modules à la mode BUT"
|
||||
|
||||
def compute_module_moy(
|
||||
self,
|
||||
evals_poids_df: pd.DataFrame,
|
||||
@ -200,22 +228,14 @@ class ModuleImplResultsAPC:
|
||||
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids
|
||||
if nb_etuds == 0:
|
||||
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
||||
# Coefficients des évaluations, met à zéro ceux des évals incomplètes:
|
||||
evals_coefs = (
|
||||
np.array(
|
||||
[e.coefficient for e in moduleimpl.evaluations],
|
||||
dtype=float,
|
||||
)
|
||||
* self.evaluations_completes
|
||||
).reshape(-1, 1)
|
||||
evals_coefs = self.get_evaluations_coefs(moduleimpl)
|
||||
evals_poids = evals_poids_df.values * evals_coefs
|
||||
# -> evals_poids shape : (nb_evals, nb_ues)
|
||||
assert evals_poids.shape == (nb_evals, nb_ues)
|
||||
# Remplace les notes ATT, EXC, ABS, NaN par zéro et mets les notes sur 20:
|
||||
evals_notes = np.where(
|
||||
self.evals_notes.values > scu.NOTES_ABSENCE, self.evals_notes.values, 0.0
|
||||
) / [e.note_max / 20.0 for e in moduleimpl.evaluations]
|
||||
# Les poids des évals pour les étudiant: là où il a des notes non neutralisées
|
||||
evals_notes_20 = self.get_eval_notes_sur_20(moduleimpl)
|
||||
|
||||
# Les poids des évals pour chaque étudiant: là où il a des notes
|
||||
# non neutralisées
|
||||
# (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui)
|
||||
# Note: les NaN sont remplacés par des 0 dans evals_notes
|
||||
# et dans dans evals_poids_etuds
|
||||
@ -228,7 +248,7 @@ class ModuleImplResultsAPC:
|
||||
0,
|
||||
)
|
||||
# Calcule la moyenne pondérée sur les notes disponibles:
|
||||
evals_notes_stacked = np.stack([evals_notes] * nb_ues, axis=2)
|
||||
evals_notes_stacked = np.stack([evals_notes_20] * nb_ues, axis=2)
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etuds_moy_module = np.sum(
|
||||
evals_poids_etuds * evals_notes_stacked, axis=1
|
||||
@ -288,3 +308,45 @@ def moduleimpl_is_conforme(
|
||||
== module_evals_poids
|
||||
)
|
||||
return check
|
||||
|
||||
|
||||
class ModuleImplResultsClassic(ModuleImplResults):
|
||||
"Calcul des moyennes de modules des formations classiques"
|
||||
|
||||
def compute_module_moy(self) -> pd.Series:
|
||||
"""Calcule les moyennes des étudiants dans ce module
|
||||
|
||||
Résultat: Series, lignes etud
|
||||
= la note (moyenne) de l'étudiant pour ce module.
|
||||
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
|
||||
ne donnent pas de coef.
|
||||
"""
|
||||
modimpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||
nb_etuds, nb_evals = self.evals_notes.shape
|
||||
if nb_etuds == 0:
|
||||
return pd.Series()
|
||||
evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1)
|
||||
assert evals_coefs.shape == (nb_evals,)
|
||||
evals_notes_20 = self.get_eval_notes_sur_20(modimpl)
|
||||
# Les coefs des évals pour chaque étudiant: là où il a des notes
|
||||
# non neutralisées
|
||||
# (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui)
|
||||
# Note: les NaN sont remplacés par des 0 dans evals_notes
|
||||
# et dans dans evals_poids_etuds
|
||||
# (rappel: la comparaison est toujours False face à un NaN)
|
||||
# shape: (nb_etuds, nb_evals)
|
||||
coefs_stacked = np.stack([evals_coefs] * nb_etuds)
|
||||
evals_coefs_etuds = np.where(
|
||||
self.evals_notes.values > scu.NOTES_NEUTRALISE, coefs_stacked, 0
|
||||
)
|
||||
# Calcule la moyenne pondérée sur les notes disponibles:
|
||||
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)
|
||||
|
||||
self.etuds_moy_module = pd.Series(
|
||||
etuds_moy_module,
|
||||
index=self.evals_notes.index,
|
||||
)
|
||||
return self.etuds_moy_module
|
||||
|
@ -31,7 +31,7 @@ import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def compute_sem_moys(etud_moy_ue_df, modimpl_coefs_df):
|
||||
def compute_sem_moys_apc(etud_moy_ue_df, modimpl_coefs_df):
|
||||
"""Calcule la moyenne générale indicative
|
||||
= moyenne des moyennes d'UE, pondérée par la somme de leurs coefs
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Fonctions de calcul des moyennes d'UE
|
||||
"""Fonctions de calcul des moyennes d'UE (classiques ou BUT)
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@ -34,13 +34,14 @@ 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_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.DataFrame:
|
||||
"""Charge les coefs des modules de la formation pour le semestre indiqué.
|
||||
"""Charge les coefs APC des modules de la formation pour le semestre indiqué.
|
||||
|
||||
Ces coefs lient les modules à chaque UE.
|
||||
En APC, ces coefs lient les modules à chaque UE.
|
||||
|
||||
Résultat: (module_coefs_df, ues, modules)
|
||||
DataFrame rows = UEs, columns = modules, value = coef.
|
||||
@ -85,7 +86,7 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
|
||||
def df_load_modimpl_coefs(
|
||||
formsemestre: models.FormSemestre, ues=None, modimpls=None
|
||||
) -> pd.DataFrame:
|
||||
"""Charge les coefs des modules du formsemestre indiqué.
|
||||
"""Charge les coefs APC des modules du formsemestre indiqué.
|
||||
|
||||
Comme df_load_module_coefs mais prend seulement les UE
|
||||
et modules du formsemestre.
|
||||
@ -126,7 +127,7 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
|
||||
return modimpls_notes.swapaxes(0, 1)
|
||||
|
||||
|
||||
def notes_sem_load_cube(formsemestre):
|
||||
def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
||||
"""Calcule le cube des notes du semestre
|
||||
(charge toutes les notes, calcule les moyenne des modules
|
||||
et assemble le cube)
|
||||
@ -151,7 +152,7 @@ def notes_sem_load_cube(formsemestre):
|
||||
)
|
||||
|
||||
|
||||
def compute_ue_moys(
|
||||
def compute_ue_moys_apc(
|
||||
sem_cube: np.array,
|
||||
etuds: list,
|
||||
modimpls: list,
|
||||
@ -159,7 +160,7 @@ def compute_ue_moys(
|
||||
modimpl_inscr_df: pd.DataFrame,
|
||||
modimpl_coefs_df: pd.DataFrame,
|
||||
) -> pd.DataFrame:
|
||||
"""Calcul de la moyenne d'UE
|
||||
"""Calcul de la moyenne d'UE en mode APC (BUT).
|
||||
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
|
||||
@ -168,11 +169,11 @@ def compute_ue_moys(
|
||||
sem_cube: notes moyennes aux modules
|
||||
ndarray (etuds x modimpls x UEs)
|
||||
(floats avec des NaN)
|
||||
etuds : lites des étudiants (dim. 0 du cube)
|
||||
etuds : listes 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)
|
||||
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
|
||||
modimpl_coefs_df: matrice coefficients (UE x modimpl)
|
||||
|
||||
Resultat: DataFrame columns UE, rows etudid
|
||||
"""
|
||||
@ -214,3 +215,70 @@ def compute_ue_moys(
|
||||
return pd.DataFrame(
|
||||
etud_moy_ue, index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
|
||||
)
|
||||
|
||||
|
||||
def compute_ue_moys_classic(
|
||||
formsemestre: FormSemestre,
|
||||
sem_matrix: np.array,
|
||||
ues: list,
|
||||
modimpl_inscr_df: pd.DataFrame,
|
||||
modimpl_coefs: np.array,
|
||||
) -> pd.DataFrame:
|
||||
"""Calcul de la moyenne d'UE en mode classique.
|
||||
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_matrix: notes moyennes aux modules
|
||||
ndarray (etuds x modimpls)
|
||||
(floats avec des NaN)
|
||||
etuds : listes des étudiants (dim. 0 de la matrice)
|
||||
ues : liste des UE
|
||||
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
|
||||
modimpl_coefs: vecteur des coefficients de modules
|
||||
|
||||
Résultat:
|
||||
- moyennes générales: pd.Series, index etudid
|
||||
- moyennes d'UE: DataFrame columns UE, rows etudid
|
||||
"""
|
||||
nb_etuds, nb_modules = sem_matrix.shape
|
||||
assert len(modimpl_coefs) == nb_modules
|
||||
nb_ues = len(ues)
|
||||
modimpl_inscr = modimpl_inscr_df.values
|
||||
# Enlève les NaN du numérateur:
|
||||
sem_matrix_no_nan = np.nan_to_num(sem_matrix, nan=0.0)
|
||||
# Ne prend pas en compte les notes des étudiants non inscrits au module:
|
||||
# Annule les notes:
|
||||
sem_matrix_inscrits = np.where(modimpl_inscr, sem_matrix_no_nan, 0.0)
|
||||
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
|
||||
modimpl_coefs_etuds = np.where(
|
||||
modimpl_inscr, np.stack([modimpl_coefs.T] * nb_etuds), 0.0
|
||||
)
|
||||
# Annule les coefs des modules NaN (nb_etuds x nb_mods)
|
||||
modimpl_coefs_etuds_no_nan = np.where(
|
||||
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds
|
||||
)
|
||||
# Calcul des moyennes générales:
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etud_moy_gen = np.sum(
|
||||
modimpl_coefs_etuds_no_nan * sem_matrix_inscrits, axis=1
|
||||
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
|
||||
etud_moy_gen_s = pd.Series(etud_moy_gen, index=modimpl_inscr_df.index)
|
||||
# Calcul des moyennes d'UE
|
||||
ue_modules = np.array(
|
||||
[[m.module.ue == ue for m in formsemestre.modimpls] for ue in ues]
|
||||
)[..., np.newaxis]
|
||||
modimpl_coefs_etuds_no_nan_stacked = np.stack(
|
||||
[modimpl_coefs_etuds_no_nan.T] * nb_ues
|
||||
)
|
||||
# nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions
|
||||
coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2)
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etud_moy_ue = (
|
||||
np.sum(coefs * sem_matrix_inscrits, axis=2) / np.sum(coefs, axis=2)
|
||||
).T
|
||||
etud_moy_ue_df = pd.DataFrame(
|
||||
etud_moy_ue, index=modimpl_inscr_df.index, columns=[ue.id for ue in ues]
|
||||
)
|
||||
return etud_moy_gen_s, etud_moy_ue_df
|
||||
|
65
app/comp/res_but.py
Normal file
65
app/comp/res_but.py
Normal file
@ -0,0 +1,65 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Résultats semestres BUT
|
||||
"""
|
||||
|
||||
from app.comp import moy_ue, moy_sem, inscr_mod
|
||||
from app.comp.res_sem import NotesTableCompat
|
||||
|
||||
|
||||
class ResultatsSemestreBUT(NotesTableCompat):
|
||||
"""Résultats BUT: organisation des calculs"""
|
||||
|
||||
_cached_attrs = NotesTableCompat._cached_attrs + (
|
||||
"modimpl_coefs_df",
|
||||
"modimpls_evals_poids",
|
||||
"sem_cube",
|
||||
)
|
||||
|
||||
def __init__(self, formsemestre):
|
||||
super().__init__(formsemestre)
|
||||
|
||||
if not self.load_cached():
|
||||
self.compute()
|
||||
self.store()
|
||||
|
||||
def compute(self):
|
||||
"Charge les notes et inscriptions et calcule les moyennes d'UE et gen."
|
||||
(
|
||||
self.sem_cube,
|
||||
self.modimpls_evals_poids,
|
||||
self.modimpls_results,
|
||||
) = moy_ue.notes_sem_load_cube(self.formsemestre)
|
||||
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
||||
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
self.formsemestre, ues=self.ues, modimpls=self.modimpls
|
||||
)
|
||||
# l'idx de la colonne du mod modimpl.id est
|
||||
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
||||
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
||||
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
|
||||
self.sem_cube,
|
||||
self.etuds,
|
||||
self.modimpls,
|
||||
self.ues,
|
||||
self.modimpl_inscr_df,
|
||||
self.modimpl_coefs_df,
|
||||
)
|
||||
self.etud_moy_gen = moy_sem.compute_sem_moys_apc(
|
||||
self.etud_moy_ue, self.modimpl_coefs_df
|
||||
)
|
||||
self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
|
||||
|
||||
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
|
||||
"""La moyenne de l'étudiant dans le moduleimpl
|
||||
En APC, il s'agit d'une moyenne indicative sans valeur.
|
||||
Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM)
|
||||
"""
|
||||
mod_idx = self.modimpl_coefs_df.columns.get_loc(moduleimpl_id)
|
||||
etud_idx = self.etud_index[etudid]
|
||||
# moyenne sur les UE:
|
||||
return self.sem_cube[etud_idx, mod_idx].mean()
|
95
app/comp/res_classic.py
Normal file
95
app/comp/res_classic.py
Normal file
@ -0,0 +1,95 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Résultats semestres classiques (non APC)
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from app.comp import moy_mod, moy_ue, moy_sem, inscr_mod
|
||||
from app.comp.res_sem import NotesTableCompat
|
||||
from app.models.formsemestre import FormSemestre
|
||||
|
||||
|
||||
class ResultatsSemestreClassic(NotesTableCompat):
|
||||
"""Résultats du semestre (formation classique): organisation des calculs."""
|
||||
|
||||
_cached_attrs = NotesTableCompat._cached_attrs + (
|
||||
"modimpl_coefs",
|
||||
"modimpl_idx",
|
||||
"sem_matrix",
|
||||
)
|
||||
|
||||
def __init__(self, formsemestre):
|
||||
super().__init__(formsemestre)
|
||||
|
||||
if not self.load_cached():
|
||||
self.compute()
|
||||
self.store()
|
||||
# recalculé (aussi rapide que de les cacher)
|
||||
self.moy_min = self.etud_moy_gen.min()
|
||||
self.moy_max = self.etud_moy_gen.max()
|
||||
self.moy_moy = self.etud_moy_gen.mean()
|
||||
|
||||
def compute(self):
|
||||
"Charge les notes et inscriptions et calcule les moyennes d'UE et gen."
|
||||
self.sem_matrix, self.modimpls_results = notes_sem_load_matrix(
|
||||
self.formsemestre
|
||||
)
|
||||
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
||||
self.modimpl_coefs = np.array(
|
||||
[m.module.coefficient for m in self.formsemestre.modimpls]
|
||||
)
|
||||
self.modimpl_idx = {m.id: i for i, m in enumerate(self.formsemestre.modimpls)}
|
||||
"l'idx de la colonne du mod modimpl.id est modimpl_idx[modimpl.id]"
|
||||
|
||||
self.etud_moy_gen, self.etud_moy_ue = moy_ue.compute_ue_moys_classic(
|
||||
self.formsemestre,
|
||||
self.sem_matrix,
|
||||
self.ues,
|
||||
self.modimpl_inscr_df,
|
||||
self.modimpl_coefs,
|
||||
)
|
||||
self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
|
||||
|
||||
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
|
||||
"""La moyenne de l'étudiant dans le moduleimpl
|
||||
Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM)
|
||||
"""
|
||||
return self.modimpls_results[moduleimpl_id].etuds_moy_module.get(etudid, "NI")
|
||||
|
||||
|
||||
def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple:
|
||||
"""Calcule la matrice des notes du semestre
|
||||
(charge toutes les notes, calcule les moyenne des modules
|
||||
et assemble la matrice)
|
||||
Resultat:
|
||||
sem_matrix : 2d-array (etuds x modimpls)
|
||||
modimpls_results dict { modimpl.id : ModuleImplResultsClassic }
|
||||
"""
|
||||
modimpls_results = {}
|
||||
modimpls_notes = []
|
||||
for modimpl in formsemestre.modimpls:
|
||||
mod_results = moy_mod.ModuleImplResultsClassic(modimpl)
|
||||
etuds_moy_module = mod_results.compute_module_moy()
|
||||
modimpls_results[modimpl.id] = mod_results
|
||||
modimpls_notes.append(etuds_moy_module)
|
||||
return (
|
||||
notes_sem_assemble_matrix(modimpls_notes),
|
||||
modimpls_results,
|
||||
)
|
||||
|
||||
|
||||
def notes_sem_assemble_matrix(modimpls_notes: list[pd.Series]) -> np.ndarray:
|
||||
"""Réuni les notes moyennes des modules du semestre en une matrice
|
||||
|
||||
modimpls_notes : liste des moyennes de module
|
||||
(Series rendus par compute_module_moy, index: etud)
|
||||
Resultat: ndarray (etud x module)
|
||||
"""
|
||||
modimpls_notes_arr = [s.values for s in modimpls_notes]
|
||||
modimpls_notes = np.stack(modimpls_notes_arr)
|
||||
# passe de (mod x etud) à (etud x mod)
|
||||
return modimpls_notes.T
|
@ -9,10 +9,10 @@ from functools import cached_property
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from app.comp.aux import StatsMoyenne
|
||||
from app.models import ModuleImpl
|
||||
from app.models import FormSemestre, ModuleImpl
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_cache import ResultatsSemestreCache
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF
|
||||
|
||||
# Il faut bien distinguer
|
||||
# - ce qui est caché de façon persistente (via redis):
|
||||
@ -25,17 +25,14 @@ from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
#
|
||||
class ResultatsSemestre:
|
||||
_cached_attrs = (
|
||||
"sem_cube",
|
||||
"modimpl_inscr_df",
|
||||
"modimpl_coefs_df",
|
||||
"etud_moy_ue",
|
||||
"modimpls_evals_poids",
|
||||
"modimpls_results",
|
||||
"etud_moy_gen",
|
||||
"etud_moy_gen_ranks",
|
||||
"etud_moy_gen",
|
||||
"etud_moy_ue",
|
||||
"modimpl_inscr_df",
|
||||
"modimpls_results",
|
||||
)
|
||||
|
||||
def __init__(self, formsemestre):
|
||||
def __init__(self, formsemestre: FormSemestre):
|
||||
self.formsemestre = formsemestre
|
||||
# BUT ou standard ? (apc == "approche par compétences")
|
||||
self.is_apc = formsemestre.formation.is_apc()
|
||||
@ -70,7 +67,7 @@ class ResultatsSemestre:
|
||||
@cached_property
|
||||
def etuds(self):
|
||||
"Liste des inscrits au semestre, sans les démissionnaires"
|
||||
# nb: si les liste des inscrits change, ResultatsSemestre devient invalide
|
||||
# nb: si la liste des inscrits change, ResultatsSemestre devient invalide
|
||||
return self.formsemestre.get_inscrits(include_dem=False)
|
||||
|
||||
@cached_property
|
||||
@ -119,12 +116,16 @@ class ResultatsSemestre:
|
||||
# Pour raccorder le code des anciens codes qui attendent une NoteTable
|
||||
class NotesTableCompat(ResultatsSemestre):
|
||||
"""Implementation partielle de NotesTable WIP TODO
|
||||
Accès aux notes et rangs.
|
||||
|
||||
Les méthodes définies dans cette classe sont là
|
||||
pour conserver la compatibilité abvec les codes anciens et
|
||||
il n'est pas recommandé de les utiliser dans de nouveaux
|
||||
développements (API malcommode et peu efficace).
|
||||
"""
|
||||
|
||||
_cached_attrs = ResultatsSemestre._cached_attrs + ()
|
||||
|
||||
def __init__(self, formsemestre):
|
||||
def __init__(self, formsemestre: FormSemestre):
|
||||
super().__init__(formsemestre)
|
||||
nb_etuds = len(self.etuds)
|
||||
self.bonus = defaultdict(lambda: 0.0) # XXX TODO
|
||||
@ -132,6 +133,30 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
self.mod_rangs = {
|
||||
m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.modimpls
|
||||
}
|
||||
self.moy_min = "NA"
|
||||
self.moy_max = "NA"
|
||||
|
||||
def get_etudids(self, sorted=False) -> list[int]:
|
||||
"""Liste des etudids inscrits, incluant les démissionnaires.
|
||||
Si sorted, triée par moy. générale décroissante
|
||||
Sinon, triée par ordre alphabetique de NOM
|
||||
"""
|
||||
# Note: pour avoir les inscrits non triés,
|
||||
# utiliser [ ins.etudid for ins in self.formsemestre.inscriptions ]
|
||||
if sorted:
|
||||
# Tri par moy. generale décroissante
|
||||
return [x[-1] for x in self.T]
|
||||
|
||||
return [x["etudid"] for x in self.inscrlist]
|
||||
|
||||
@cached_property
|
||||
def inscrlist(self) -> list[dict]: # utilisé par PE seulement
|
||||
"""Liste de dict etud, avec démissionnaires
|
||||
classée dans l'ordre alphabétique de noms.
|
||||
"""
|
||||
etuds = self.formsemestre.get_inscrits(include_dem=True)
|
||||
etuds.sort(key=lambda e: e.sort_key)
|
||||
return [e.to_dict_scodoc7() for e in etuds]
|
||||
|
||||
@cached_property
|
||||
def stats_moy_gen(self):
|
||||
@ -161,6 +186,33 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
else:
|
||||
return [m.to_dict() for m in self.modimpls if m.module.ue.id == ue_id]
|
||||
|
||||
def get_etud_decision_sem(self, etudid: int) -> dict:
|
||||
"""Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
|
||||
{ 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
|
||||
Si état défaillant, force le code a DEF
|
||||
"""
|
||||
if self.get_etud_etat(etudid) == DEF:
|
||||
return {
|
||||
"code": DEF,
|
||||
"assidu": False,
|
||||
"event_date": "",
|
||||
"compense_formsemestre_id": None,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"code": ATT, # XXX TODO
|
||||
"assidu": True, # XXX TODO
|
||||
"event_date": "",
|
||||
"compense_formsemestre_id": None,
|
||||
}
|
||||
|
||||
def get_etud_etat(self, etudid: int) -> str:
|
||||
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
|
||||
ins = self.formsemestre.etuds_inscriptions.get(etudid, None)
|
||||
if ins is None:
|
||||
return ""
|
||||
return ins.etat
|
||||
|
||||
def get_etud_moy_gen(self, etudid): # -> float | str
|
||||
"""Moyenne générale de cet etudiant dans ce semestre.
|
||||
Prend en compte les UE capitalisées. (TODO)
|
||||
@ -169,38 +221,27 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
"""
|
||||
return self.etud_moy_gen[etudid]
|
||||
|
||||
def get_moduleimpls_attente(self):
|
||||
return [] # XXX TODO
|
||||
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
|
||||
"""La moyenne de l'étudiant dans le moduleimpl
|
||||
En APC, il s'agira d'une moyenne indicative sans valeur.
|
||||
Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM)
|
||||
"""
|
||||
raise NotImplementedError() # virtual method
|
||||
|
||||
def get_etud_rang(self, etudid):
|
||||
return self.etud_moy_gen_ranks[etudid]
|
||||
|
||||
def get_etud_rang_group(self, etudid, group_id):
|
||||
return (None, 0) # XXX unimplemented TODO
|
||||
|
||||
def get_etud_ue_status(self, etudid, ue_id):
|
||||
def get_etud_ue_status(self, etudid: int, ue_id: int):
|
||||
return {
|
||||
"cur_moy_ue": self.results.etud_moy_ue[ue_id][etudid],
|
||||
"is_capitalized": False, # XXX TODO
|
||||
}
|
||||
|
||||
def get_etud_mod_moy(self, moduleimpl_id, etudid):
|
||||
mod_idx = self.results.modimpl_coefs_df.columns.get_loc(moduleimpl_id)
|
||||
etud_idx = self.results.etud_index[etudid]
|
||||
# moyenne sur les UE:
|
||||
self.results.sem_cube[etud_idx, mod_idx].mean()
|
||||
def get_etud_rang(self, etudid: int):
|
||||
return self.etud_moy_gen_ranks.get(etudid, 99999) # XXX
|
||||
|
||||
def get_mod_stats(self, moduleimpl_id):
|
||||
return {
|
||||
"moy": "-",
|
||||
"max": "-",
|
||||
"min": "-",
|
||||
"nb_notes": "-",
|
||||
"nb_missing": "-",
|
||||
"nb_valid_evals": "-",
|
||||
}
|
||||
def get_etud_rang_group(self, etudid: int, group_id: int):
|
||||
return (None, 0) # XXX unimplemented TODO
|
||||
|
||||
def get_evals_in_mod(self, moduleimpl_id):
|
||||
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
|
||||
"liste des évaluations valides dans un module"
|
||||
mi = ModuleImpl.query.get(moduleimpl_id)
|
||||
evals_results = []
|
||||
for e in mi.evaluations:
|
||||
@ -219,3 +260,78 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
}
|
||||
evals_results.append(d)
|
||||
return evals_results
|
||||
|
||||
def get_moduleimpls_attente(self):
|
||||
return [] # XXX TODO
|
||||
|
||||
def get_mod_stats(self, moduleimpl_id):
|
||||
return {
|
||||
"moy": "-",
|
||||
"max": "-",
|
||||
"min": "-",
|
||||
"nb_notes": "-",
|
||||
"nb_missing": "-",
|
||||
"nb_valid_evals": "-",
|
||||
}
|
||||
|
||||
def get_nom_short(self, etudid):
|
||||
"formatte nom d'un etud (pour table recap)"
|
||||
etud = self.identdict[etudid]
|
||||
return (
|
||||
(etud["nom_usuel"] or etud["nom"]).upper()
|
||||
+ " "
|
||||
+ etud["prenom"].capitalize()[:2]
|
||||
+ "."
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def T(self):
|
||||
return self.get_table_moyennes_triees()
|
||||
|
||||
def get_table_moyennes_triees(self) -> list:
|
||||
"""Result: liste de tuples
|
||||
moy_gen, moy_ue_0, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
|
||||
"""
|
||||
table_moyennes = []
|
||||
etuds_inscriptions = self.formsemestre.etuds_inscriptions
|
||||
|
||||
for etudid in etuds_inscriptions:
|
||||
moy_gen = self.etud_moy_gen.get(etudid, False)
|
||||
if moy_gen is False:
|
||||
# pas de moyenne: démissionnaire ou def
|
||||
t = ["-"] + ["0.00"] * len(self.ues) + ["NI"] * len(self.modimpls)
|
||||
else:
|
||||
moy_ues = self.etud_moy_ue.loc[etudid]
|
||||
t = [moy_gen] + list(moy_ues)
|
||||
# TODO UE capitalisées: ne pas afficher moyennes modules
|
||||
for modimpl in self.modimpls:
|
||||
val = self.get_etud_mod_moy(modimpl.id, etudid)
|
||||
t.append(val)
|
||||
t.append(etudid)
|
||||
table_moyennes.append(t)
|
||||
# tri par moyennes décroissantes,
|
||||
# en laissant les démissionnaires à la fin, par ordre alphabetique
|
||||
etuds = [ins.etud for ins in etuds_inscriptions.values()]
|
||||
etuds.sort(key=lambda e: e.sort_key)
|
||||
self._rang_alpha = {e.id: i for i, e in enumerate(etuds)}
|
||||
table_moyennes.sort(key=self._row_key)
|
||||
return table_moyennes
|
||||
|
||||
def _row_key(self, x):
|
||||
"""clé de tri par moyennes décroissantes,
|
||||
en laissant les demissionnaires à la fin, par ordre alphabetique.
|
||||
(moy_gen, rang_alpha)
|
||||
"""
|
||||
try:
|
||||
moy = -float(x[0])
|
||||
except (ValueError, TypeError):
|
||||
moy = 1000.0
|
||||
return (moy, self._rang_alpha[x[-1]])
|
||||
|
||||
@cached_property
|
||||
def identdict(self) -> dict:
|
||||
"""{ etudid : etud_dict } pour tous les inscrits au semestre"""
|
||||
return {
|
||||
ins.etud.id: ins.etud.to_dict_scodoc7()
|
||||
for ins in self.formsemestre.inscriptions
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ from app import models
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.scodoc import notesdb as ndb
|
||||
|
||||
|
||||
class Identite(db.Model):
|
||||
@ -105,10 +106,25 @@ class Identite(db.Model):
|
||||
r.append("-".join([x.lower().capitalize() for x in fields]))
|
||||
return " ".join(r)
|
||||
|
||||
@cached_property
|
||||
def sort_key(self) -> tuple:
|
||||
"clé pour tris par ordre alphabétique"
|
||||
return (self.nom_usuel or self.nom).lower(), self.prenom.lower()
|
||||
|
||||
def get_first_email(self, field="email") -> str:
|
||||
"Le mail associé à la première adrese de l'étudiant, ou None"
|
||||
return self.adresses[0].email or None if self.adresses.count() > 0 else None
|
||||
|
||||
def to_dict_scodoc7(self):
|
||||
"""Représentation dictionnaire,
|
||||
compatible ScoDoc7 mais sans infos admission
|
||||
"""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators: (backward compat)
|
||||
e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"])
|
||||
return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty
|
||||
|
||||
def to_dict_bul(self, include_urls=True):
|
||||
"""Infos exportées dans les bulletins"""
|
||||
from app.scodoc import sco_photos
|
||||
|
@ -3,6 +3,7 @@
|
||||
"""ScoDoc models: formsemestre
|
||||
"""
|
||||
import datetime
|
||||
from functools import cached_property
|
||||
|
||||
import flask_sqlalchemy
|
||||
|
||||
@ -251,7 +252,7 @@ class FormSemestre(db.Model):
|
||||
etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
|
||||
)
|
||||
|
||||
def get_inscrits(self, include_dem=False) -> list:
|
||||
def get_inscrits(self, include_dem=False) -> list[Identite]:
|
||||
"""Liste des étudiants inscrits à ce semestre
|
||||
Si all, tous les étudiants, avec les démissionnaires.
|
||||
"""
|
||||
@ -260,6 +261,11 @@ class FormSemestre(db.Model):
|
||||
else:
|
||||
return [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
|
||||
|
||||
@cached_property
|
||||
def etuds_inscriptions(self) -> dict:
|
||||
"""Map { etudid : inscription }"""
|
||||
return {ins.etud.id: ins for ins in self.inscriptions}
|
||||
|
||||
|
||||
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
|
||||
notes_formsemestre_responsables = db.Table(
|
||||
|
@ -202,7 +202,7 @@ class NotesTable:
|
||||
self.inscrlist.sort(key=itemgetter("nomp"))
|
||||
|
||||
# { etudid : rang dans l'ordre alphabetique }
|
||||
self.rang_alpha = {e["etudid"]: i for i, e in enumerate(self.inscrlist)}
|
||||
self._rang_alpha = {e["etudid"]: i for i, e in enumerate(self.inscrlist)}
|
||||
|
||||
self.bonus = scu.DictDefault(defaultvalue=0)
|
||||
# Notes dans les modules { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } }
|
||||
@ -366,7 +366,7 @@ class NotesTable:
|
||||
moy = -float(x[0])
|
||||
except (ValueError, TypeError):
|
||||
moy = 1000.0
|
||||
return (moy, self.rang_alpha[x[-1]])
|
||||
return (moy, self._rang_alpha[x[-1]])
|
||||
|
||||
def get_etudids(self, sorted=False):
|
||||
if sorted:
|
||||
|
@ -37,6 +37,7 @@ from flask import make_response
|
||||
|
||||
from app import log
|
||||
from app.but import bulletin_but
|
||||
from app.comp.res_classic import ResultatsSemestreClassic
|
||||
from app.models import FormSemestre
|
||||
from app.models.etudiants import Identite
|
||||
|
||||
@ -303,6 +304,8 @@ def make_formsemestre_recapcomplet(
|
||||
args={"formsemestre_id": formsemestre_id}
|
||||
)[0]
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
||||
# XXX EXPERIMENTAL
|
||||
# nt = ResultatsSemestreClassic(formsemestre)
|
||||
modimpls = nt.get_modimpls_dict()
|
||||
ues = nt.get_ues_stat_dict() # incluant le(s) UE de sport
|
||||
#
|
||||
|
@ -71,7 +71,7 @@ def test_ue_moy(test_client):
|
||||
# Recalcul des moyennes
|
||||
sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre)
|
||||
etuds = formsemestre.etuds.all()
|
||||
etud_moy_ue = moy_ue.compute_ue_moys(
|
||||
etud_moy_ue = moy_ue.compute_ue_moys_apc(
|
||||
sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df
|
||||
)
|
||||
return etud_moy_ue
|
||||
@ -114,7 +114,7 @@ def test_ue_moy(test_client):
|
||||
# Recalcule les notes:
|
||||
sem_cube, _, _ = moy_ue.notes_sem_load_cube(formsemestre)
|
||||
etuds = formsemestre.etuds.all()
|
||||
etud_moy_ue = moy_ue.compute_ue_moys(
|
||||
etud_moy_ue = moy_ue.compute_ue_moys_apc(
|
||||
sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df
|
||||
)
|
||||
assert etud_moy_ue[ue1.id][etudid] == n1
|
||||
|
Loading…
Reference in New Issue
Block a user