ScoDoc-PE/app/comp/moy_ue.py

287 lines
11 KiB
Python
Raw Normal View History

# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2022-01-01 14:49:42 +01:00
# Copyright (c) 1999 - 2022 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
#
##############################################################################
2021-12-30 23:58:38 +01:00
"""Fonctions de calcul des moyennes d'UE (classiques ou BUT)
"""
import numpy as np
import pandas as pd
from app import db
from app import models
2021-11-28 16:31:33 +01:00
from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef
from app.comp import moy_mod
2021-12-30 23:58:38 +01:00
from app.models.formsemestre import FormSemestre
2021-11-28 16:31:33 +01:00
from app.scodoc import sco_codes_parcours
def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.DataFrame:
2021-12-30 23:58:38 +01:00
"""Charge les coefs APC des modules de la formation pour le semestre indiqué.
2021-11-28 16:31:33 +01:00
2021-12-30 23:58:38 +01:00
En APC, ces coefs lient les modules à chaque UE.
2021-11-28 16:31:33 +01:00
Résultat: (module_coefs_df, ues, modules)
DataFrame rows = UEs, columns = modules, value = coef.
Considère toutes les UE (sauf sport) et modules du semestre.
Les coefs non définis (pas en base) sont mis à zéro.
2021-11-18 22:46:18 +01:00
Si semestre_idx None, prend toutes les UE de la formation.
"""
ues = (
UniteEns.query.filter_by(formation_id=formation_id)
.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
.order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
2021-11-28 16:31:33 +01:00
)
2021-12-02 12:08:03 +01:00
modules = Module.query.filter_by(formation_id=formation_id).order_by(
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
2021-12-02 12:08:03 +01:00
)
2021-11-18 22:46:18 +01:00
if semestre_idx is not None:
ues = ues.filter_by(semestre_idx=semestre_idx)
modules = modules.filter_by(semestre_id=semestre_idx)
2021-11-28 16:31:33 +01:00
ues = ues.all()
modules = modules.all()
ue_ids = [ue.id for ue in ues]
module_ids = [module.id for module in modules]
2021-11-28 16:31:33 +01:00
module_coefs_df = pd.DataFrame(columns=module_ids, index=ue_ids, dtype=float)
query = (
2021-11-28 16:31:33 +01:00
db.session.query(ModuleUECoef)
.filter(UniteEns.formation_id == formation_id)
.filter(ModuleUECoef.ue_id == UniteEns.id)
)
if semestre_idx is not None:
query = query.filter(UniteEns.semestre_idx == semestre_idx)
for mod_coef in query:
2021-11-28 16:31:33 +01:00
module_coefs_df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef
2021-11-28 16:31:33 +01:00
module_coefs_df.fillna(value=0, inplace=True)
2021-11-28 16:31:33 +01:00
return module_coefs_df, ues, modules
2021-12-05 20:21:51 +01:00
def df_load_modimpl_coefs(
formsemestre: models.FormSemestre, ues=None, modimpls=None
) -> pd.DataFrame:
2021-12-30 23:58:38 +01:00
"""Charge les coefs APC des modules du formsemestre indiqué.
2021-11-28 16:31:33 +01:00
Comme df_load_module_coefs mais prend seulement les UE
et modules du formsemestre.
2021-12-05 20:21:51 +01:00
Si ues et modimpls sont None, prend tous ceux du formsemestre.
2021-11-28 16:31:33 +01:00
Résultat: (module_coefs_df, ues, modules)
DataFrame rows = UEs, columns = modimpl, value = coef.
"""
2021-12-05 20:21:51 +01:00
if ues is None:
ues = formsemestre.query_ues().all()
2021-11-28 16:31:33 +01:00
ue_ids = [x.id for x in ues]
2021-12-05 20:21:51 +01:00
if modimpls is None:
modimpls = formsemestre.modimpls.all()
2021-11-28 16:31:33 +01:00
modimpl_ids = [x.id for x in modimpls]
mod2impl = {m.module.id: m.id for m in modimpls}
modimpl_coefs_df = pd.DataFrame(columns=modimpl_ids, index=ue_ids, dtype=float)
mod_coefs = (
db.session.query(ModuleUECoef)
.filter(ModuleUECoef.module_id == ModuleImpl.module_id)
.filter(ModuleImpl.formsemestre_id == formsemestre.id)
)
for mod_coef in mod_coefs:
modimpl_coefs_df[mod2impl[mod_coef.module_id]][mod_coef.ue_id] = mod_coef.coef
modimpl_coefs_df.fillna(value=0, inplace=True)
return modimpl_coefs_df, ues, modimpls
def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
"""Réuni les notes moyennes des modules du semestre en un "cube"
modimpls_notes : liste des moyennes de module
(DataFrames rendus par compute_module_moy, (etud x UE))
Resultat: ndarray (etud x module x UE)
"""
2022-01-04 14:55:32 +01:00
if len(modimpls_notes) == 0:
return np.zeros((0, 0, 0), dtype=float)
2021-11-28 16:31:33 +01:00
modimpls_notes_arr = [df.values for df in modimpls_notes]
modimpls_notes = np.stack(modimpls_notes_arr)
# passe de (mod x etud x ue) à (etud x mod x UE)
return modimpls_notes.swapaxes(0, 1)
2021-12-30 23:58:38 +01:00
def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
2021-11-28 16:31:33 +01:00
"""Calcule le cube des notes du semestre
(charge toutes les notes, calcule les moyenne des modules
et assemble le cube)
2021-12-08 23:43:07 +01:00
Resultat:
sem_cube : ndarray (etuds x modimpls x UEs)
modimpls_evals_poids dict { modimpl.id : evals_poids }
2021-12-26 19:15:47 +01:00
modimpls_results dict { modimpl.id : ModuleImplResultsAPC }
2021-11-28 16:31:33 +01:00
"""
2021-12-26 19:15:47 +01:00
modimpls_results = {}
2021-12-08 23:43:07 +01:00
modimpls_evals_poids = {}
2021-11-28 16:31:33 +01:00
modimpls_notes = []
for modimpl in formsemestre.modimpls:
2021-12-26 19:15:47 +01:00
mod_results = moy_mod.ModuleImplResultsAPC(modimpl)
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
modimpls_results[modimpl.id] = mod_results
2021-11-28 16:31:33 +01:00
modimpls_notes.append(etuds_moy_module)
2021-12-05 20:21:51 +01:00
return (
notes_sem_assemble_cube(modimpls_notes),
modimpls_evals_poids,
2021-12-26 19:15:47 +01:00
modimpls_results,
2021-12-05 20:21:51 +01:00
)
2021-11-28 16:31:33 +01:00
2021-12-30 23:58:38 +01:00
def compute_ue_moys_apc(
2021-11-28 16:31:33 +01:00
sem_cube: np.array,
etuds: list,
modimpls: list,
ues: list,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs_df: pd.DataFrame,
2021-11-28 16:31:33 +01:00
) -> pd.DataFrame:
2021-12-30 23:58:38 +01:00
"""Calcul de la moyenne d'UE en mode APC (BUT).
2021-11-28 16:31:33 +01:00
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_cube: notes moyennes aux modules
ndarray (etuds x modimpls x UEs)
(floats avec des NaN)
2021-12-30 23:58:38 +01:00
etuds : listes des étudiants (dim. 0 du cube)
2021-11-28 16:31:33 +01:00
modimpls : liste des modules à considérer (dim. 1 du cube)
ues : liste des UE (dim. 2 du cube)
2021-12-30 23:58:38 +01:00
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
modimpl_coefs_df: matrice coefficients (UE x modimpl)
2021-11-28 16:31:33 +01:00
Resultat: DataFrame columns UE, rows etudid
"""
nb_etuds, nb_modules, nb_ues = sem_cube.shape
assert len(etuds) == nb_etuds
assert len(modimpls) == nb_modules
assert len(ues) == nb_ues
assert modimpl_inscr_df.shape[0] == nb_etuds
assert modimpl_inscr_df.shape[1] == nb_modules
assert modimpl_coefs_df.shape[0] == nb_ues
assert modimpl_coefs_df.shape[1] == nb_modules
modimpl_inscr = modimpl_inscr_df.values
modimpl_coefs = modimpl_coefs_df.values
2021-12-14 09:30:47 +01:00
if nb_etuds == 0:
return pd.DataFrame(
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
)
# Duplique les inscriptions sur les UEs:
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues, axis=2)
# Enlève les NaN du numérateur:
# si on veut prendre en compte les modules avec notes neutralisées ?
sem_cube_no_nan = np.nan_to_num(sem_cube, nan=0.0)
# Ne prend pas en compte les notes des étudiants non inscrits au module:
# Annule les notes:
sem_cube_inscrits = np.where(modimpl_inscr_stacked, sem_cube_no_nan, 0.0)
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
modimpl_coefs_etuds = np.where(
modimpl_inscr_stacked, np.stack([modimpl_coefs.T] * nb_etuds), 0.0
)
# Annule les coefs des modules NaN
modimpl_coefs_etuds_no_nan = np.where(np.isnan(sem_cube), 0.0, modimpl_coefs_etuds)
2021-11-28 16:31:33 +01:00
#
# Version vectorisée
#
etud_moy_ue = np.sum(
modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
2021-11-28 16:31:33 +01:00
return pd.DataFrame(
etud_moy_ue, index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
2021-11-28 16:31:33 +01:00
)
2021-12-30 23:58:38 +01:00
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