ScoDoc/app/comp/moy_ue.py

551 lines
22 KiB
Python
Raw Permalink Normal View History

# -*- 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.
#
# 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
import app
from app import db
from app import models
from app.models import (
FormSemestre,
Module,
ModuleImpl,
ModuleUECoef,
UniteEns,
)
2021-11-28 16:31:33 +01:00
from app.comp import moy_mod
from app.scodoc import codes_cursus
from app.scodoc import sco_preferences
from app.scodoc.codes_cursus 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 != codes_cursus.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)
| ((Module.ue_id == UniteEns.id) & (UniteEns.type == codes_cursus.UE_SPORT))
)
2022-12-17 03:23:52 +01:00
.order_by(Module.semestre_id, Module.module_type, 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.get_ues()
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]
try:
modimpls_notes = np.stack(modimpls_notes_arr)
# passe de (mod x etud x ue) à (etud x mod x ue)
except ValueError:
app.critical_error(
f"""notes_sem_assemble_cube: shapes {
", ".join([x.shape for x in modimpls_notes_arr])}"""
)
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 = []
etudids, etudids_actifs = formsemestre.etudids_actifs()
2022-01-25 10:45:13 +01:00
for modimpl in formsemestre.modimpls_sorted:
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
evals_poids = modimpl.get_evaluations_poids()
2021-12-26 19:15:47 +01:00
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
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, dtype=float
),
pd.DataFrame(
columns=[ue.id for ue in ues], index=modimpl_inscr_df.index, dtype=float
),
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 or 0.0) if ue.type != UE_SPORT else 0.0
for ue in ues
},
index=modimpl_inscr_df.index,
columns=[ue.id for ue in ues],
dtype=float,
)
# 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],
dtype=float,
)
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