ScoDoc/app/comp/moy_mod.py

726 lines
30 KiB
Python
Raw Normal View History

2021-11-17 10:28:51 +01:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2023-12-31 23:04:06 +01:00
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
2021-11-17 10:28:51 +01:00
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Fonctions de calcul des moyennes de modules (modules, ressources ou SAÉ)
2021-12-30 23:58:38 +01:00
Pour les formations classiques et le BUT
2021-11-17 10:28:51 +01:00
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.
"""
import dataclasses
2021-12-26 19:15:47 +01:00
from dataclasses import dataclass
2021-11-17 10:28:51 +01:00
import numpy as np
import pandas as pd
import sqlalchemy as sa
2021-11-17 10:28:51 +01:00
2023-01-23 23:03:20 +01:00
import app
2021-11-17 10:28:51 +01:00
from app import db
from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
from app.scodoc import sco_cache
from app.scodoc import sco_utils as scu
from app.scodoc.codes_cursus import UE_SPORT
2022-02-01 11:37:05 +01:00
from app.scodoc.sco_utils import ModuleType
2021-11-17 10:28:51 +01:00
2021-12-26 19:15:47 +01:00
@dataclass
class EvaluationEtat:
"""Classe pour stocker quelques infos sur les résultats d'une évaluation"""
evaluation_id: int
nb_attente: int
nb_notes: int # nb notes d'étudiants inscrits au semestre et au modimpl
2021-12-26 19:15:47 +01:00
is_complete: bool
def to_dict(self):
"convert to dict"
return dataclasses.asdict(self)
2021-12-26 19:15:47 +01:00
2021-12-30 23:58:38 +01:00
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.
2021-12-26 19:15:47 +01:00
Les attributs sont tous des objets simples cachables dans Redis;
les caches sont gérés par ResultatsSemestre.
"""
def __init__(
self, moduleimpl: ModuleImpl, etudids: list[int], etudids_actifs: set[int]
):
"""
Args:
- etudids : liste des etudids, qui donne l'index du dataframe
(doit être tous les étudiants inscrits au semestre incluant les DEM et DEF)
- etudids_actifs l'ensemble des étudiants inscrits au semestre, non DEM/DEF.
"""
2021-12-26 19:15:47 +01:00
self.moduleimpl_id = moduleimpl.id
self.module_id = moduleimpl.module.id
self.etudids = None
"liste des étudiants inscrits au SEMESTRE (incluant dem et def)"
2022-01-25 10:45:13 +01:00
2021-12-26 19:15:47 +01:00
self.nb_inscrits_module = None
2022-01-25 10:45:13 +01:00
"nombre d'inscrits (non DEM) à ce module"
2021-12-26 19:15:47 +01:00
self.evaluations_completes = []
"séquence de booléens, indiquant les évals à prendre en compte."
2022-01-07 10:37:48 +01:00
self.evaluations_completes_dict = {}
"{ evaluation.id : bool } indique si à prendre en compte ou non."
2021-12-26 19:15:47 +01:00
self.evaluations_etat = {}
"{ evaluation_id: EvaluationEtat }"
self.etudids_attente = set()
"etudids avec au moins une note ATT dans ce module"
2022-02-06 16:09:17 +01:00
self.en_attente = False
"Vrai si au moins une évaluation a une note en attente"
2021-12-26 19:15:47 +01:00
#
self.evals_notes = None
2022-01-16 23:47:52 +01:00
"""DataFrame, colonnes: EVALS, Lignes: etudid (inscrits au SEMESTRE)
2021-12-26 19:15:47 +01:00
valeur: notes brutes, float ou NOTES_ATTENTE, NOTES_NEUTRALISE,
NOTES_ABSENCE.
Les NaN désignent les notes manquantes (non saisies).
"""
self.etuds_moy_module = None
"""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 a des notes)
ne donnent pas de coef vers cette UE.
"""
self.evals_etudids_sans_note = {}
"""dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions."""
self.evals_type = {}
"""Type de chaque eval { evaluation.id : evaluation.evaluation_type }"""
self.load_notes(etudids, etudids_actifs)
self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index)
"""1 bool par etud, indique si sa moyenne de module vient de la session2"""
self.etuds_use_rattrapage = pd.Series(False, index=self.evals_notes.index)
"""1 bool par etud, indique si sa moyenne de module utilise la note de rattrapage"""
2021-12-26 19:15:47 +01:00
def load_notes(
self, etudids: list[int], etudids_actifs: set[int]
): # ré-écriture de df_load_modimpl_notes
2021-12-26 19:15:47 +01:00
"""Charge toutes les notes de toutes les évaluations du module.
Args:
- etudids : liste des etudids, qui donne l'index du dataframe
(doit être tous les étudiants inscrits au semestre incluant les DEM et DEF)
- etudids_actifs l'ensemble des étudiants inscrits au semestre, non DEM/DEF.
2021-12-26 19:15:47 +01:00
Dataframe evals_notes
colonnes: le nom de la colonne est l'evaluation_id (int)
index (lignes): etudid (int)
L'ensemble des étudiants est celui des inscrits au SEMESTRE.
Les notes sont "brutes" (séries de floats) et peuvent prendre les valeurs:
note : float (valeur enregistrée brute, NON normalisée sur 20)
pas de note: NaN (rien en bd, ou étudiant non inscrit au module)
absent: NOTES_ABSENCE (NULL en bd)
excusé: NOTES_NEUTRALISE (voir sco_utils)
attente: NOTES_ATTENTE
Évaluation "complete" (prise en compte dans les calculs) si:
- soit tous les étudiants inscrits au module ont des notes
2022-01-16 23:47:52 +01:00
- soit elle a été déclarée "à prise en compte immédiate" (publish_incomplete)
ou est une évaluation de rattrapage ou de session 2
2021-12-26 19:15:47 +01:00
Évaluation "attente" (prise en compte dans les calculs, mais il y
manque des notes) ssi il y a des étudiants inscrits au semestre et au module
qui ont des notes ATT.
"""
moduleimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
self.etudids = etudids
2021-12-26 19:15:47 +01:00
# --- Calcul nombre d'inscrits pour déterminer les évaluations "completes":
# on prend les inscrits au module ET au semestre (donc sans démissionnaires)
inscrits_module = {ins.etud.id for ins in moduleimpl.inscriptions}.intersection(
etudids_actifs
2021-12-26 19:15:47 +01:00
)
self.nb_inscrits_module = len(inscrits_module)
# dataFrame vide, index = tous les inscrits au SEMESTRE
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
self.evaluations_completes = []
2022-01-07 10:37:48 +01:00
self.evaluations_completes_dict = {}
self.etudids_attente = set() # empty
self.evals_type = {}
evaluation: Evaluation
2021-12-26 19:15:47 +01:00
for evaluation in moduleimpl.evaluations:
self.evals_type[evaluation.id] = evaluation.evaluation_type
2021-12-26 19:15:47 +01:00
eval_df = self._load_evaluation_notes(evaluation)
# is_complete ssi
# tous les inscrits (non dem) au module ont une note
# ou évaluation déclarée "à prise en compte immédiate"
# ou rattrapage, 2eme session, bonus
# ET pas bloquée par date (is_blocked)
is_blocked = evaluation.is_blocked()
etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
is_complete = (
(evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE)
or (evaluation.publish_incomplete)
or (not etudids_sans_note)
) and not is_blocked
2021-12-26 19:15:47 +01:00
self.evaluations_completes.append(is_complete)
2022-01-07 10:37:48 +01:00
self.evaluations_completes_dict[evaluation.id] = is_complete
self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
2021-12-26 19:15:47 +01:00
# NULL en base => ABS (= -999)
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
# Ce merge ne garde que les étudiants inscrits au module
# et met à NULL (NaN) les notes non présentes
2021-12-26 19:15:47 +01:00
# (notes non saisies ou etuds non inscrits au module):
evals_notes = evals_notes.merge(
eval_df, how="left", left_index=True, right_index=True
)
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
# Nombre de notes (non vides, incluant ATT etc) des inscrits:
nb_notes = eval_notes_inscr.notna().sum()
if is_blocked:
eval_etudids_attente = set()
else:
# Etudiants avec notes en attente:
# = ceux avec note ATT
eval_etudids_attente = set(
eval_notes_inscr.iloc[
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
].index
)
if evaluation.publish_incomplete:
# et en "immédiat", tous ceux sans note
eval_etudids_attente |= etudids_sans_note
# Synthèse pour état du module:
self.etudids_attente |= eval_etudids_attente
2021-12-26 19:15:47 +01:00
self.evaluations_etat[evaluation.id] = EvaluationEtat(
evaluation_id=evaluation.id,
nb_attente=len(eval_etudids_attente),
nb_notes=int(nb_notes),
is_complete=is_complete,
2021-12-26 19:15:47 +01:00
)
# au moins une note en attente (ATT ou manquante en mode "immédiat") dans ce modimpl:
self.en_attente = bool(self.etudids_attente)
2021-12-26 19:15:47 +01:00
# Force columns names to integers (evaluation ids)
evals_notes.columns = pd.Index([int(x) for x in evals_notes.columns], dtype=int)
2021-12-26 19:15:47 +01:00
self.evals_notes = evals_notes
_load_evaluation_notes_q = sa.text(
"""SELECT n.etudid, n.value AS ":evaluation_id"
FROM notes_notes n, notes_moduleimpl_inscription i
WHERE evaluation_id=:evaluation_id
AND n.etudid = i.etudid
AND i.moduleimpl_id = :moduleimpl_id
"""
)
2021-12-26 19:15:47 +01:00
def _load_evaluation_notes(self, evaluation: Evaluation) -> pd.DataFrame:
"""Charge les notes de l'évaluation
Resultat: dataframe, index: etudid ayant une note, valeur: note brute.
"""
with db.engine.begin() as connection:
eval_df = pd.read_sql_query(
self._load_evaluation_notes_q,
connection,
params={
"evaluation_id": evaluation.id,
"moduleimpl_id": evaluation.moduleimpl.id,
},
index_col="etudid",
)
2021-12-26 19:15:47 +01:00
eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)])
return eval_df
def get_evaluations_coefs(self, modimpl: ModuleImpl) -> np.array:
"""Coefficients des évaluations.
Les coefs des évals incomplètes, rattrapage, session 2, bonus sont forcés à zéro.
2021-12-30 23:58:38 +01:00
Résultat: 2d-array of floats, shape (nb_evals, 1)
"""
return (
np.array(
[
(
e.coefficient
if e.evaluation_type == Evaluation.EVALUATION_NORMALE
else 0.0
)
for e in modimpl.evaluations
],
2021-12-30 23:58:38 +01:00
dtype=float,
)
* self.evaluations_completes
).reshape(-1, 1)
def get_evaluations_special_coefs(
self, modimpl: ModuleImpl, evaluation_type=Evaluation.EVALUATION_SESSION2
) -> np.array:
"""Coefficients des évaluations de session 2 ou rattrapage.
Les évals de session 2 et rattrapage sont réputées "complètes": elles sont toujours
prises en compte mais seules les notes numériques et ABS sont utilisées.
Résultat: 2d-array of floats, shape (nb_evals, 1)
"""
return (
np.array(
[
(e.coefficient if e.evaluation_type == evaluation_type else 0.0)
for e in modimpl.evaluations
],
dtype=float,
)
).reshape(-1, 1)
2022-02-06 16:09:17 +01:00
# was _list_notes_evals_titles
2023-08-25 17:58:57 +02:00
def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
2022-02-06 16:09:17 +01:00
"Liste des évaluations complètes"
return [
e for e in moduleimpl.evaluations if self.evaluations_completes_dict[e.id]
]
2021-12-30 23:58:38 +01:00
def get_eval_notes_sur_20(self, moduleimpl: ModuleImpl) -> np.array:
"""Les notes de toutes les évaluations du module, complètes ou non.
Remplace les ATT, EXC, ABS, NaN par zéro et mets les notes sur 20.
2021-12-30 23:58:38 +01:00
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]
def get_eval_notes_dict(self, evaluation_id: int) -> dict:
"""Notes d'une évaluation, brutes, sous forme d'un dict
{ etudid : valeur }
avec les valeurs float, ou "ABS" ou EXC
"""
return {
etudid: scu.fmt_note(x, keep_numeric=True)
for (etudid, x) in self.evals_notes[evaluation_id].items()
}
def get_evaluations_rattrapage(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
"""Les évaluations de rattrapage de ce module.
Rattrapage: la moyenne du module est la meilleure note entre moyenne
des autres évals et la moyenne des notes de rattrapage.
"""
return [
e
for e in moduleimpl.evaluations
if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE
]
def get_evaluations_session2(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
"""Les évaluations de deuxième session de ce module, ou None s'il n'en a pas.
La moyenne des notes de Session 2 remplace la note de moyenne des autres évals.
"""
return [
e
for e in moduleimpl.evaluations
if e.evaluation_type == Evaluation.EVALUATION_SESSION2
]
def get_evaluations_bonus(self, modimpl: ModuleImpl) -> list[Evaluation]:
"""Les évaluations bonus non bloquées de ce module, ou liste vide s'il n'en a pas."""
return [
e
for e in modimpl.evaluations
if e.evaluation_type == Evaluation.EVALUATION_BONUS and not e.is_blocked()
]
def get_evaluations_bonus_idx(self, modimpl: ModuleImpl) -> list[int]:
"""Les indices des évaluations bonus non bloquées"""
return [
i
for (i, e) in enumerate(modimpl.evaluations)
if e.evaluation_type == Evaluation.EVALUATION_BONUS and not e.is_blocked()
]
2021-12-30 23:58:38 +01:00
class ModuleImplResultsAPC(ModuleImplResults):
"Calcul des moyennes de modules à la mode BUT"
2021-12-26 19:15:47 +01:00
def compute_module_moy(
self, evals_poids_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
2021-12-26 19:15:47 +01:00
) -> pd.DataFrame:
"""Calcule les moyennes des étudiants dans ce module
Argument:
evals_poids: DataFrame, colonnes: UEs, lignes: EVALs
modimpl_coefs_df: DataFrame, colonnes: modimpl_id, lignes: ue_id
2021-12-26 19:15:47 +01:00
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 a des notes)
ne donnent pas de coef vers cette UE.
"""
modimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
2021-12-26 19:15:47 +01:00
nb_etuds, nb_evals = self.evals_notes.shape
nb_ues = evals_poids_df.shape[1]
2023-06-21 13:09:29 +02:00
if evals_poids_df.shape[0] != nb_evals:
# compat notes/poids: race condition ?
app.critical_error(
f"""compute_module_moy: evals_poids_df.shape[0] != nb_evals ({
evals_poids_df.shape[0]} != {nb_evals})
"""
)
2021-12-26 19:15:47 +01:00
if nb_etuds == 0:
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
if nb_ues == 0:
return pd.DataFrame(index=self.evals_notes.index, columns=[])
# coefs des évals complètes normales (pas rattr., session 2 ni bonus):
evals_coefs = self.get_evaluations_coefs(modimpl)
2021-12-26 19:15:47 +01:00
evals_poids = evals_poids_df.values * evals_coefs
# -> evals_poids shape : (nb_evals, nb_ues)
assert evals_poids.shape == (nb_evals, nb_ues)
evals_notes_20 = self.get_eval_notes_sur_20(modimpl)
2021-12-30 23:58:38 +01:00
# Les poids des évals pour chaque étudiant: là où il a des notes
# non neutralisées
2021-12-26 19:15:47 +01:00
# (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, nb_ues)
poids_stacked = np.stack([evals_poids] * nb_etuds) # nb_etuds, nb_evals, nb_ues
2021-12-26 19:15:47 +01:00
evals_poids_etuds = np.where(
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
poids_stacked,
0,
)
# Calcule la moyenne pondérée sur les notes disponibles:
2021-12-30 23:58:38 +01:00
evals_notes_stacked = np.stack([evals_notes_20] * nb_ues, axis=2)
# evals_notes_stacked shape: nb_etuds, nb_evals, nb_ues
2021-12-26 19:15:47 +01:00
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etuds_moy_module = np.sum(
evals_poids_etuds * evals_notes_stacked, axis=1
) / np.sum(evals_poids_etuds, axis=1)
# etuds_moy_module shape: nb_etuds x nb_ues
evals_session2 = self.get_evaluations_session2(modimpl)
evals_rat = self.get_evaluations_rattrapage(modimpl)
if evals_session2:
# Session2 : quand elle existe, remplace la note de module
# Calcul moyenne notes session2 et remplace (si la note session 2 existe)
etuds_moy_module_s2 = self._compute_moy_special(
modimpl,
evals_notes_stacked,
evals_poids_df,
Evaluation.EVALUATION_SESSION2,
)
# Vrai si toutes les UEs avec coef non nul ont bien une note de session 2 calculée:
mod_coefs = modimpl_coefs_df[modimpl.id]
etuds_use_session2 = np.all(
np.isfinite(etuds_moy_module_s2[:, mod_coefs != 0]), axis=1
)
etuds_moy_module = np.where(
etuds_use_session2[:, np.newaxis],
etuds_moy_module_s2,
etuds_moy_module,
)
self.etuds_use_session2 = pd.Series(
etuds_use_session2, index=self.evals_notes.index
)
elif evals_rat:
etuds_moy_module_rat = self._compute_moy_special(
modimpl,
evals_notes_stacked,
evals_poids_df,
Evaluation.EVALUATION_RATTRAPAGE,
)
etuds_ue_use_rattrapage = (
etuds_moy_module_rat > etuds_moy_module
) # etud x UE
etuds_moy_module = np.where(
etuds_ue_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
)
self.etuds_use_rattrapage = pd.Series(
np.any(etuds_ue_use_rattrapage, axis=1), index=self.evals_notes.index
)
# Application des évaluations bonus:
etuds_moy_module = self.apply_bonus(
etuds_moy_module,
modimpl,
evals_poids_df,
evals_notes_stacked,
)
2021-12-26 19:15:47 +01:00
self.etuds_moy_module = pd.DataFrame(
etuds_moy_module,
index=self.evals_notes.index,
columns=evals_poids_df.columns,
)
return self.etuds_moy_module
def _compute_moy_special(
self,
modimpl: ModuleImpl,
evals_notes_stacked: np.array,
evals_poids_df: pd.DataFrame,
evaluation_type: int,
) -> np.array:
"""Calcul moyenne APC sur évals rattrapage ou session2"""
nb_etuds = self.evals_notes.shape[0]
nb_ues = evals_poids_df.shape[1]
evals_coefs_s2 = self.get_evaluations_special_coefs(
modimpl, evaluation_type=evaluation_type
)
evals_poids_s2 = evals_poids_df.values * evals_coefs_s2
poids_stacked_s2 = np.stack(
[evals_poids_s2] * nb_etuds
) # nb_etuds, nb_evals, nb_ues
evals_poids_etuds_s2 = np.where(
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
poids_stacked_s2,
0,
)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etuds_moy_module_s2 = np.sum(
evals_poids_etuds_s2 * evals_notes_stacked, axis=1
) / np.sum(evals_poids_etuds_s2, axis=1)
return etuds_moy_module_s2
def apply_bonus(
self,
etuds_moy_module: pd.DataFrame,
modimpl: ModuleImpl,
evals_poids_df: pd.DataFrame,
evals_notes_stacked: np.ndarray,
):
"""Ajoute les points des évaluations bonus.
Il peut y avoir un nb quelconque d'évaluations bonus.
Les points sont directement ajoutés (ils peuvent être négatifs).
"""
evals_bonus = self.get_evaluations_bonus(modimpl)
if not evals_bonus:
return etuds_moy_module
poids_stacked = np.stack([evals_poids_df.values] * len(etuds_moy_module))
for evaluation in evals_bonus:
eval_idx = evals_poids_df.index.get_loc(evaluation.id)
etuds_moy_module += (
evals_notes_stacked[:, eval_idx, :] * poids_stacked[:, eval_idx, :]
)
# Clip dans [0,20]
etuds_moy_module.clip(0, 20, out=etuds_moy_module)
return etuds_moy_module
2021-12-26 19:15:47 +01:00
2022-01-25 10:45:13 +01:00
def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
2021-11-17 10:28:51 +01:00
"""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: 1 si le coef de ce module dans l'UE est non nul, zéro sinon
(sauf pour module bonus, defaut à 1)
Si le module n'est pas une ressource ou une SAE, ne charge pas de poids
et renvoie toujours les poids par défaut.
2022-01-25 10:45:13 +01:00
Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
2021-11-17 10:28:51 +01:00
"""
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
ues = modimpl.formsemestre.get_ues(with_sport=False)
2021-11-17 10:28:51 +01:00
ue_ids = [ue.id for ue in ues]
evaluation_ids = [evaluation.id for evaluation in modimpl.evaluations]
2021-12-26 19:15:47 +01:00
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
if modimpl.module.module_type in (ModuleType.RESSOURCE, ModuleType.SAE):
for ue_poids in EvaluationUEPoids.query.join(
EvaluationUEPoids.evaluation
).filter_by(moduleimpl_id=moduleimpl_id):
if (
ue_poids.evaluation_id in evals_poids.index
and ue_poids.ue_id in evals_poids.columns
):
evals_poids.at[ue_poids.evaluation_id, ue_poids.ue_id] = ue_poids.poids
# ignore poids vers des UEs qui n'existent plus ou sont dans un autre semestre...
2021-11-17 10:28:51 +01:00
2022-01-25 10:45:13 +01:00
# Initialise poids non enregistrés:
2022-02-01 11:37:05 +01:00
default_poids = (
1.0
if modimpl.module.ue.type == UE_SPORT
or modimpl.module.module_type == ModuleType.MALUS
else 0.0
)
2022-01-25 10:45:13 +01:00
if np.isnan(evals_poids.values.flat).any():
ue_coefs = modimpl.module.get_ue_coef_dict()
for ue in ues:
evals_poids.loc[evals_poids[ue.id].isna(), ue.id] = (
1 if ue_coefs.get(ue.id, default_poids) > 0 else 0
2022-01-25 10:45:13 +01:00
)
2021-12-26 19:15:47 +01:00
return evals_poids, ues
2021-11-17 10:28:51 +01:00
# appelé par ModuleImpl.check_apc_conformity()
2021-12-26 19:15:47 +01:00
def moduleimpl_is_conforme(
moduleimpl, evals_poids: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
2021-11-17 10:28:51 +01:00
) -> bool:
"""Vérifie que les évaluations de ce moduleimpl sont bien conformes
au PN.
Un module est dit *conforme* si et seulement si la somme des poids de ses
évaluations vers une UE de coefficient non nul est non nulle.
2021-12-26 19:15:47 +01:00
Arguments:
evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
modimpl_coefs_df: DataFrame, cols: modimpl_id, lignes: UEs du formsemestre
2022-01-25 10:45:13 +01:00
NB: les UEs dans evals_poids sont sans le bonus sport
2021-11-17 10:28:51 +01:00
"""
nb_evals, nb_ues = evals_poids.shape
if nb_evals == 0:
return True # modules vides conformes
if nb_ues == 0:
return False # situation absurde (pas d'UE)
if len(modimpl_coefs_df) != nb_ues:
# il arrive (#bug) que le cache ne soit pas à jour...
sco_cache.invalidate_formsemestre()
return app.critical_error("moduleimpl_is_conforme: err 1")
2022-09-08 10:28:12 +02:00
if moduleimpl.id not in modimpl_coefs_df:
2022-09-08 10:28:12 +02:00
# soupçon de bug cache coef ?
sco_cache.invalidate_formsemestre()
return app.critical_error("moduleimpl_is_conforme: err 2")
2022-09-08 10:28:12 +02:00
module_evals_poids = evals_poids.transpose().sum(axis=1) != 0
return all((modimpl_coefs_df[moduleimpl.id] != 0).eq(module_evals_poids))
2021-12-30 23:58:38 +01:00
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 = db.session.get(ModuleImpl, self.moduleimpl_id)
2021-12-30 23:58:38 +01:00
nb_etuds, nb_evals = self.evals_notes.shape
if nb_etuds == 0:
return pd.Series()
evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1)
2023-01-23 23:03:20 +01:00
if evals_coefs.shape != (nb_evals,):
app.critical_error("compute_module_moy: vals_coefs.shape != nb_evals")
2021-12-30 23:58:38 +01:00
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)
evals_session2 = self.get_evaluations_session2(modimpl)
evals_rat = self.get_evaluations_rattrapage(modimpl)
if evals_session2:
# Session2 : quand elle existe, remplace la note de module
# Calcule la moyenne des évaluations de session2
etuds_moy_module_s2 = self._compute_moy_special(
modimpl, evals_notes_20, Evaluation.EVALUATION_SESSION2
)
etuds_use_session2 = np.isfinite(etuds_moy_module_s2)
etuds_moy_module = np.where(
etuds_use_session2,
etuds_moy_module_s2,
etuds_moy_module,
)
self.etuds_use_session2 = pd.Series(
etuds_use_session2, index=self.evals_notes.index
)
elif evals_rat:
# Rattrapage: remplace la note de module ssi elle est supérieure
# Calcule la moyenne des évaluations de rattrapage
etuds_moy_module_rat = self._compute_moy_special(
modimpl, evals_notes_20, Evaluation.EVALUATION_RATTRAPAGE
)
etuds_use_rattrapage = etuds_moy_module_rat > etuds_moy_module
etuds_moy_module = np.where(
etuds_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
)
self.etuds_use_rattrapage = pd.Series(
etuds_use_rattrapage, index=self.evals_notes.index
)
# Application des évaluations bonus:
etuds_moy_module = self.apply_bonus(
etuds_moy_module,
modimpl,
evals_notes_20,
)
2021-12-30 23:58:38 +01:00
self.etuds_moy_module = pd.Series(
etuds_moy_module,
index=self.evals_notes.index,
)
2021-12-30 23:58:38 +01:00
return self.etuds_moy_module
def _compute_moy_special(
self, modimpl: ModuleImpl, evals_notes_20: np.array, evaluation_type: int
) -> np.array:
"""Calcul moyenne sur évals rattrapage ou session2"""
# n'utilise que les notes valides et ABS (0).
# Même calcul que pour les évals normales, mais avec seulement les
# coefs des évals de session 2 ou rattrapage:
nb_etuds = self.evals_notes.shape[0]
evals_coefs = self.get_evaluations_special_coefs(
modimpl, evaluation_type=evaluation_type
).reshape(-1)
coefs_stacked = np.stack([evals_coefs] * nb_etuds)
# zéro partout sauf si une note ou ABS:
evals_coefs_etuds = np.where(
self.evals_notes.values > scu.NOTES_NEUTRALISE, coefs_stacked, 0
)
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)
return etuds_moy_module # array 1d (nb_etuds)
def apply_bonus(
self,
etuds_moy_module: np.ndarray,
modimpl: ModuleImpl,
evals_notes_20: np.ndarray,
):
"""Ajoute les points des évaluations bonus.
Il peut y avoir un nb quelconque d'évaluations bonus.
Les points sont directement ajoutés (ils peuvent être négatifs).
"""
evals_bonus_idx = self.get_evaluations_bonus_idx(modimpl)
if not evals_bonus_idx:
return etuds_moy_module
for eval_idx in evals_bonus_idx:
etuds_moy_module += evals_notes_20[:, eval_idx]
# Clip dans [0,20]
etuds_moy_module.clip(0, 20, out=etuds_moy_module)
return etuds_moy_module