ScoDoc/app/comp/moy_mod.py

178 lines
7.1 KiB
Python

# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
#
# 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É)
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 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
) -> tuple[pd.DataFrame, list]:
"""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.
Résultat: (evals_poids, liste de UE du semestre)
"""
modimpl = ModuleImpl.query.get(moduleimpl_id)
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
ues = modimpl.formsemestre.query_ues(with_sport=False).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 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:
df.fillna(value=default_poids, inplace=True)
return df, ues
def check_moduleimpl_conformity(
moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame
) -> 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.
"""
module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0
check = all(
(modules_coefficients[moduleimpl.module.id].to_numpy() != 0)
== 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
Résultat: (evals_notes, liste de évaluations du moduleimpl)
L'ensemble des étudiants est celui des inscrits au SEMESTRE.
Les notes renvoyées 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
absent: NaN
excusé: NOTES_NEUTRALISE (voir sco_utils)
attente: NOTES_ATTENTE
N'utilise pas de cache ScoDoc.
"""
# L'index du dataframe est la liste des étudiants inscrits au semestre:
etudids = [
e.etudid for e in ModuleImpl.query.get(moduleimpl_id).formsemestre.inscriptions
]
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id)
evals_notes = pd.DataFrame(index=etudids, dtype=float) # empty df with all students
for evaluation in evaluations:
eval_df = pd.read_sql_query(
"""SELECT etudid, value AS "%(evaluation_id)s"
FROM notes_notes
WHERE evaluation_id=%(evaluation_id)s""",
db.engine,
params={"evaluation_id": evaluation.id},
index_col="etudid",
dtype=np.float64,
)
evals_notes = evals_notes.merge(
eval_df, how="outer", left_index=True, right_index=True
)
return evals_notes, evaluations
def compute_module_moy(
evals_notes_df: pd.DataFrame,
evals_poids_df: pd.DataFrame,
evaluations: list,
) -> pd.DataFrame:
"""Calcule les moyennes des étudiants dans ce module
- evals_notes : DataFrame, colonnes: EVALS, Lignes: etudid
valeur: notes brutes, float ou NOTES_ATTENTE ou NOTES_NEUTRALISE
Les NaN désignent les ABS.
- evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
- evaluations: séquence d'évaluations (utilisées pour le coef et le barème)
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, nb_evals = evals_notes_df.shape
nb_ues = evals_poids_df.shape[1]
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids
evals_coefs = np.array([e.coefficient for e in evaluations], dtype=float).reshape(
-1, 1
)
evals_poids = evals_poids_df.values * evals_coefs
# -> evals_poids_arr shape : (nb_evals, nb_ues)
assert evals_poids.shape == (nb_evals, nb_ues)
# Remet les notes sur 20 (sauf notes spéciales <= -1000):
evals_notes = np.where(
evals_notes_df.values > -1000, evals_notes_df.values, 0.0
) / [e.note_max / 20.0 for e in evaluations]
# Les poids des évals pour les étudiant: là où il a des notes non neutralisées
# Attention: les NaN (codant les absents) sont remplacés par des 0 dans
# evals_notes_arr mais pas dans evals_poids_etuds_arr
# (la comparaison est toujours false face à un NaN)
# shape: (nb_etuds, nb_evals, nb_ues)
poids_stacked = np.stack([evals_poids] * nb_etuds)
evals_poids_etuds = np.where(
np.stack([evals_notes_df.values] * nb_ues, axis=2) <= -1000.0, 0, poids_stacked
)
# Calcule la moyenne pondérée sur les notes disponibles
evals_notes_stacked = np.stack([evals_notes] * 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
) / np.sum(evals_poids_etuds, axis=1)
etuds_moy_module_df = pd.DataFrame(
etuds_moy_module, index=evals_notes_df.index, columns=evals_poids_df.columns
)
return etuds_moy_module_df