ScoDoc-Lille/app/comp/moy_ue.py

563 lines
22 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
from app.models import (
DispenseUE,
FormSemestre,
FormSemestreInscription,
Identite,
Module,
ModuleImpl,
ModuleUECoef,
UniteEns,
)
2021-11-28 16:31:33 +01:00
from app.comp import moy_mod
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_preferences
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import ModuleType
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_no_bonus, modules)
2021-11-28 16:31:33 +01:00
DataFrame rows = UEs, columns = modules, value = coef.
Considère toutes les UE sauf bonus et tous les modules du semestre.
2021-11-28 16:31:33 +01:00
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
)
modules = (
Module.query.filter_by(formation_id=formation_id)
.filter(
(Module.module_type == ModuleType.RESSOURCE)
| (Module.module_type == ModuleType.SAE)
2022-01-25 10:45:13 +01:00
| (
(Module.ue_id == UniteEns.id)
& (UniteEns.type == sco_codes_parcours.UE_SPORT)
)
)
.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:
if mod_coef.module_id in module_coefs_df:
module_coefs_df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef
# silently ignore coefs associated to other modules (ie when module_type is changed)
# Initialisation des poids non fixés:
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
# sur toutes les UE)
default_poids = {
mod.id: 1.0
if (mod.module_type == ModuleType.STANDARD) and (mod.ue.type == UE_SPORT)
else 0.0
for mod in modules
}
module_coefs_df.fillna(value=default_poids, 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.
Si ues et modimpls sont None, prend tous ceux du formsemestre (sauf ue bonus).
2021-11-28 16:31:33 +01:00
Résultat: (module_coefs_df, ues, modules)
DataFrame rows = UEs (sans bonus), columns = modimpl, value = coef.
2021-11-28 16:31:33 +01:00
"""
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:
2022-01-25 10:45:13 +01:00
modimpls = formsemestre.modimpls_sorted
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:
2022-02-12 10:38:04 +01:00
try:
modimpl_coefs_df[mod2impl[mod_coef.module_id]][
mod_coef.ue_id
] = mod_coef.coef
except IndexError:
# il peut y avoir en base des coefs sur des modules ou UE
# qui ont depuis été retirés de la formation
2022-02-12 10:38:04 +01:00
pass
# Initialisation des poids non fixés:
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
# sur toutes les UE)
default_poids = {
modimpl.id: 1.0
if (modimpl.module.module_type == ModuleType.STANDARD)
and (modimpl.module.ue.type == UE_SPORT)
else 0.0
for modimpl in formsemestre.modimpls_sorted
}
modimpl_coefs_df.fillna(value=default_poids, inplace=True)
2021-11-28 16:31:33 +01:00
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-17 00:06:21 +01:00
assert len(modimpls_notes)
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)
2022-01-25 10:45:13 +01:00
# passe de (mod x etud x ue) à (etud x mod x ue)
2021-11-28 16:31:33 +01:00
return modimpls_notes.swapaxes(0, 1)
2021-12-30 23:58:38 +01:00
def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
2022-01-16 23:47:52 +01:00
"""Construit le "cube" (tenseur) des notes du semestre.
Charge toutes les notes (sql), calcule les moyennes des modules
et assemble le cube.
etuds: tous les inscrits au semestre (avec dem. et def.)
2022-01-25 10:45:13 +01:00
modimpls: _tous_ les modimpls de ce semestre (y compris bonus sport)
UEs: toutes les UE du semestre (même si pas d'inscrits) SAUF le sport.
2022-01-16 23:47:52 +01:00
2022-01-25 10:45:13 +01:00
Attention: la liste des modimpls inclut les modules des UE sport, mais
elles ne sont pas dans la troisième dimension car elles n'ont pas de
"moyenne d'UE".
Résultat:
2021-12-08 23:43:07 +01:00
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 = []
2022-01-25 10:45:13 +01:00
for modimpl in formsemestre.modimpls_sorted:
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
modimpls_evals_poids[modimpl.id] = evals_poids
2021-11-28 16:31:33 +01:00
modimpls_notes.append(etuds_moy_module)
if len(modimpls_notes) > 0:
2022-01-17 00:06:21 +01:00
cube = notes_sem_assemble_cube(modimpls_notes)
else:
nb_etuds = formsemestre.etuds.count()
cube = np.zeros((nb_etuds, 0, 0), dtype=float)
2021-12-05 20:21:51 +01:00
return (
2022-01-17 00:06:21 +01:00
cube,
2021-12-05 20:21:51 +01:00
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
def load_dispense_ues(
formsemestre: FormSemestre, etudids: pd.Index, ues: list[UniteEns]
) -> set[tuple[int, int]]:
"""Construit l'ensemble des
etudids = modimpl_inscr_df.index, # les etudids
ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport
Résultat: set de (etudid, ue_id).
"""
dispense_ues = set()
ue_sem_by_code = {ue.ue_code: ue for ue in ues}
# Prend toutes les dispenses obtenues par des étudiants de ce formsemestre,
# puis filtre sur inscrits et code d'UE UE
for dispense_ue in DispenseUE.query.join(
Identite, FormSemestreInscription
).filter_by(formsemestre_id=formsemestre.id):
if dispense_ue.etudid in etudids:
# UE dans le semestre avec même code ?
ue = ue_sem_by_code.get(dispense_ue.ue.ue_code)
if ue is not None:
dispense_ues.add((dispense_ue.etudid, ue.id))
return dispense_ues
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,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs_df: pd.DataFrame,
2022-02-17 23:13:55 +01:00
modimpl_mask: np.array,
dispense_ues: set[tuple[int, int]],
block: bool = False,
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).
2022-08-17 18:15:48 +02:00
La moyenne d'UE est un nombre (note/20), ou NaN si pas de notes disponibles
2021-11-28 16:31:33 +01:00
sem_cube: notes moyennes aux modules
ndarray (etuds x modimpls x UEs)
(floats avec des NaN)
2022-01-16 23:47:52 +01:00
etuds : liste des étudiants (dim. 0 du cube)
2022-02-17 23:13:55 +01:00
modimpls : liste des module_impl (dim. 1 du cube)
2021-11-28 16:31:33 +01:00
ues : liste des UE (dim. 2 du cube)
modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl)
2022-01-25 10:45:13 +01:00
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
2022-02-17 23:13:55 +01:00
modimpl_mask: liste de booléens, indiquants le module doit être pris ou pas.
(utilisé pour éliminer les bonus, et pourra servir à cacluler
sur des sous-ensembles de modules)
block: si vrai, ne calcule rien et renvoie des NaNs
2022-02-13 22:08:16 +01:00
Résultat: DataFrame columns UE (sans bonus), rows etudid
2021-11-28 16:31:33 +01:00
"""
2022-01-25 10:45:13 +01:00
nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape
2021-11-28 16:31:33 +01:00
assert len(modimpls) == nb_modules
if block or nb_modules == 0 or nb_etuds == 0 or nb_ues_no_bonus == 0:
2022-01-17 00:06:21 +01:00
return pd.DataFrame(
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
)
assert len(etuds) == nb_etuds
assert modimpl_inscr_df.shape[0] == nb_etuds
assert modimpl_inscr_df.shape[1] == nb_modules
2022-01-25 10:45:13 +01:00
assert modimpl_coefs_df.shape[0] == nb_ues_no_bonus
assert modimpl_coefs_df.shape[1] == nb_modules
modimpl_inscr = modimpl_inscr_df.values
2022-02-17 23:13:55 +01:00
# Met à zéro tous les coefs des modules non sélectionnés dans le masque:
modimpl_coefs = np.where(modimpl_mask, modimpl_coefs_df.values, 0.0)
2022-01-25 10:45:13 +01:00
# Duplique les inscriptions sur les UEs non bonus:
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues_no_bonus, 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)
2022-04-21 22:54:06 +02:00
if modimpl_coefs_etuds_no_nan.dtype == object: # arrive sur des tableaux vides
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
2021-11-28 16:31:33 +01:00
#
# Version vectorisée
#
2022-01-31 22:14:13 +01:00
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_ue = np.sum(
modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
etud_moy_ue_df = pd.DataFrame(
2022-01-25 10:45:13 +01:00
etud_moy_ue,
index=modimpl_inscr_df.index, # les etudids
columns=modimpl_coefs_df.index, # les UE sans les UE bonus sport
2021-11-28 16:31:33 +01:00
)
# Les "dispenses" sont très peu nombreuses et traitées en python:
for dispense_ue in dispense_ues:
etud_moy_ue_df[dispense_ue[1]][dispense_ue[0]] = 0.0
return etud_moy_ue_df
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,
2022-01-25 10:45:13 +01:00
modimpl_mask: np.array,
block: bool = False,
2022-01-16 23:47:52 +01:00
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
"""Calcul de la moyenne d'UE et de la moy. générale en mode classique (DUT, LMD, ...).
2021-12-30 23:58:38 +01:00
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
2022-01-16 23:47:52 +01:00
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]
2021-12-30 23:58:38 +01:00
2022-01-25 10:45:13 +01:00
L'éventuel bonus sport n'est PAS appliqué ici.
Le masque modimpl_mask est un tableau de booléens (un par modimpl) qui
permet de sélectionner un sous-ensemble de modules (SAEs, tout sauf sport, ...).
sem_matrix: notes moyennes aux modules (tous les étuds x tous les modimpls)
2021-12-30 23:58:38 +01:00
ndarray (etuds x modimpls)
(floats avec des NaN)
etuds : listes des étudiants (dim. 0 de la matrice)
2022-01-25 10:45:13 +01:00
ues : liste des UE du semestre
2021-12-30 23:58:38 +01:00
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
modimpl_coefs: vecteur des coefficients de modules
2022-01-25 10:45:13 +01:00
modimpl_mask: masque des modimpls à prendre en compte
block: si vrai, ne calcule rien et renvoie des NaNs
2021-12-30 23:58:38 +01:00
Résultat:
- moyennes générales: pd.Series, index etudid
- moyennes d'UE: DataFrame columns UE, rows etudid
2022-01-16 23:47:52 +01:00
- coefficients d'UE: DataFrame, columns UE, rows etudid
les coefficients effectifs de chaque UE pour chaque étudiant
(sommes de coefs de modules pris en compte)
2021-12-30 23:58:38 +01:00
"""
if (
block or (len(modimpl_mask) == 0) or (sem_matrix.shape[0] == 0)
): # aucun module ou aucun étudiant
2022-02-10 22:19:15 +01:00
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
val = np.nan if block else 0.0
2022-02-10 22:19:15 +01:00
return (
pd.Series(
[val] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
2022-02-10 22:19:15 +01:00
),
pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index),
pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index),
2022-02-10 22:19:15 +01:00
)
2022-01-25 10:45:13 +01:00
# Restreint aux modules sélectionnés:
sem_matrix = sem_matrix[:, modimpl_mask]
modimpl_inscr = modimpl_inscr_df.values[:, modimpl_mask]
modimpl_coefs = modimpl_coefs[modimpl_mask]
2021-12-30 23:58:38 +01:00
nb_etuds, nb_modules = sem_matrix.shape
assert len(modimpl_coefs) == nb_modules
nb_ues = len(ues) # en comptant bonus
2022-01-25 10:45:13 +01:00
2021-12-30 23:58:38 +01:00
# 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
)
2022-04-21 22:54:06 +02:00
if modimpl_coefs_etuds_no_nan.dtype == object: # arrive sur des tableaux vides
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
# --------------------- Calcul des moyennes d'UE
2021-12-30 23:58:38 +01:00
ue_modules = np.array(
2022-01-25 10:45:13 +01:00
[[m.module.ue == ue for m in formsemestre.modimpls_sorted] for ue in ues]
)[..., np.newaxis][:, modimpl_mask, :]
2021-12-30 23:58:38 +01:00
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:
2021-12-30 23:58:38 +01:00
coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2)
2022-04-21 22:54:06 +02:00
if coefs.dtype == object: # arrive sur des tableaux vides
coefs = coefs.astype(np.float)
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
2021-12-30 23:58:38 +01:00
etud_moy_ue_df = pd.DataFrame(
etud_moy_ue, index=modimpl_inscr_df.index, columns=[ue.id for ue in ues]
)
# --------------------- Calcul des moyennes générales
if sco_preferences.get_preference("use_ue_coefs", formsemestre.id):
# Cas avec coefficients d'UE forcés: (on met à zéro l'UE bonus)
etud_coef_ue_df = pd.DataFrame(
{ue.id: ue.coefficient if ue.type != UE_SPORT else 0.0 for ue in ues},
index=modimpl_inscr_df.index,
columns=[ue.id for ue in ues],
)
# remplace NaN par zéros dans les moyennes d'UE
etud_moy_ue_df_no_nan = etud_moy_ue_df.fillna(0.0, inplace=False)
# Si on voulait annuler les coef d'UE dont la moyenne d'UE est NaN
# etud_coef_ue_df_no_nan = etud_coef_ue_df.where(etud_moy_ue_df.notna(), 0.0)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_gen_s = (etud_coef_ue_df * etud_moy_ue_df_no_nan).sum(
axis=1
) / etud_coef_ue_df.sum(axis=1)
else:
# Cas normal: pondère directement les modules
etud_coef_ue_df = pd.DataFrame(
coefs.sum(axis=2).T,
index=modimpl_inscr_df.index, # etudids
columns=[ue.id for ue in ues],
)
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)
2022-01-16 23:47:52 +01:00
return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
2022-02-01 11:37:05 +01:00
def compute_mat_moys_classic(
sem_matrix: np.array,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs: np.array,
modimpl_mask: np.array,
) -> pd.Series:
"""Calcul de la moyenne sur un sous-enemble de modules en formation CLASSIQUE
La moyenne est un nombre (note/20 ou NaN.
Le masque modimpl_mask est un tableau de booléens (un par modimpl) qui
permet de sélectionner un sous-ensemble de modules (ceux de la matière d'intérêt).
sem_matrix: notes moyennes aux modules (tous les étuds x tous les modimpls)
ndarray (etuds x modimpls)
(floats avec des NaN)
etuds : listes des étudiants (dim. 0 de la matrice)
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
modimpl_coefs: vecteur des coefficients de modules
modimpl_mask: masque des modimpls à prendre en compte
Résultat:
- moyennes: pd.Series, index etudid
"""
if (0 == len(modimpl_mask)) or (
sem_matrix.shape[0] == 0
): # aucun module ou aucun étudiant
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
return pd.Series(
[0.0] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
)
# Restreint aux modules sélectionnés:
sem_matrix = sem_matrix[:, modimpl_mask]
modimpl_inscr = modimpl_inscr_df.values[:, modimpl_mask]
modimpl_coefs = modimpl_coefs[modimpl_mask]
nb_etuds, nb_modules = sem_matrix.shape
assert len(modimpl_coefs) == nb_modules
# 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
)
2022-04-21 22:54:06 +02:00
if modimpl_coefs_etuds_no_nan.dtype == object: # arrive sur des tableaux vides
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
with np.errstate(invalid="ignore"): # il peut y avoir des NaN
etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum(
axis=1
) / modimpl_coefs_etuds_no_nan.sum(axis=1)
return pd.Series(etud_moy_mat, index=modimpl_inscr_df.index)
2022-02-01 11:37:05 +01:00
def compute_malus(
formsemestre: FormSemestre,
sem_modimpl_moys: np.array,
ues: list[UniteEns],
modimpl_inscr_df: pd.DataFrame,
) -> pd.DataFrame:
"""Calcul le malus sur les UE
Dans chaque UE, on peut avoir un ou plusieurs modules de MALUS.
2022-02-11 18:27:40 +01:00
Leurs notes sont positives ou négatives.
La somme des notes de malus somme est _soustraite_ à la moyenne de chaque UE.
2022-02-01 11:37:05 +01:00
Arguments:
- sem_modimpl_moys :
notes moyennes aux modules (tous les étuds x tous les modimpls)
floats avec des NaN.
En classique: sem_matrix, ndarray (etuds x modimpls)
En APC: sem_cube, ndarray (etuds x modimpls x UEs non bonus)
- ues: les ues du semestre (incluant le bonus sport)
- modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl)
Résultat: DataFrame de float, index etudid, columns: ue.id (sans NaN)
"""
ues_idx = [ue.id for ue in ues]
malus = pd.DataFrame(index=modimpl_inscr_df.index, columns=ues_idx, dtype=float)
2022-07-01 09:48:08 +02:00
if len(sem_modimpl_moys.flat) == 0: # vide
return malus
if len(sem_modimpl_moys.shape) > 2:
# BUT: ne retient que la 1er composante du malus qui est scalaire
# au sens ou chaque note de malus n'affecte que la moyenne de l'UE
# de rattachement de son module.
sem_modimpl_moys_scalar = sem_modimpl_moys[:, :, 0]
else: # classic
sem_modimpl_moys_scalar = sem_modimpl_moys
2022-02-01 11:37:05 +01:00
for ue in ues:
if ue.type != UE_SPORT:
modimpl_mask = np.array(
[
(m.module.module_type == ModuleType.MALUS)
and (m.module.ue.id == ue.id) # UE de rattachement
2022-02-01 11:37:05 +01:00
for m in formsemestre.modimpls_sorted
]
)
2022-02-10 22:19:15 +01:00
if len(modimpl_mask):
malus_moys = sem_modimpl_moys_scalar[:, modimpl_mask].sum(axis=1)
2022-02-10 22:19:15 +01:00
malus[ue.id] = malus_moys
2022-02-01 11:37:05 +01:00
malus.fillna(0.0, inplace=True)
return malus