forked from ScoDoc/ScoDoc
553 lines
22 KiB
Python
553 lines
22 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# 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
|
|
#
|
|
##############################################################################
|
|
|
|
"""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,
|
|
)
|
|
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:
|
|
"""Charge les coefs APC des modules de la formation pour le semestre indiqué.
|
|
|
|
En APC, ces coefs lient les modules à chaque UE.
|
|
|
|
Résultat: (module_coefs_df, ues_no_bonus, modules)
|
|
DataFrame rows = UEs, columns = modules, value = coef.
|
|
|
|
Considère toutes les UE sauf bonus et tous les modules du semestre.
|
|
Les coefs non définis (pas en base) sont mis à zéro.
|
|
|
|
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)
|
|
)
|
|
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))
|
|
)
|
|
.order_by(Module.semestre_id, Module.module_type, Module.numero, Module.code)
|
|
)
|
|
if semestre_idx is not None:
|
|
ues = ues.filter_by(semestre_idx=semestre_idx)
|
|
modules = modules.filter_by(semestre_id=semestre_idx)
|
|
ues = ues.all()
|
|
modules = modules.all()
|
|
ue_ids = [ue.id for ue in ues]
|
|
module_ids = [module.id for module in modules]
|
|
module_coefs_df = pd.DataFrame(columns=module_ids, index=ue_ids, dtype=float)
|
|
query = (
|
|
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)
|
|
|
|
return module_coefs_df, ues, modules
|
|
|
|
|
|
def df_load_modimpl_coefs(
|
|
formsemestre: models.FormSemestre, ues=None, modimpls=None
|
|
) -> pd.DataFrame:
|
|
"""Charge les coefs APC des modules du formsemestre indiqué.
|
|
|
|
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).
|
|
Résultat: (module_coefs_df, ues, modules)
|
|
DataFrame rows = UEs (sans bonus), columns = modimpl, value = coef.
|
|
"""
|
|
if ues is None:
|
|
ues = formsemestre.get_ues()
|
|
ue_ids = [x.id for x in ues]
|
|
if modimpls is None:
|
|
modimpls = formsemestre.modimpls_sorted
|
|
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:
|
|
try:
|
|
modimpl_coefs_df.loc[mod_coef.ue_id, mod2impl[mod_coef.module_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
|
|
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)
|
|
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)
|
|
"""
|
|
assert len(modimpls_notes)
|
|
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])}"""
|
|
)
|
|
return modimpls_notes.swapaxes(0, 1)
|
|
|
|
|
|
def notes_sem_load_cube(
|
|
formsemestre: FormSemestre, modimpl_coefs_df: pd.DataFrame
|
|
) -> tuple:
|
|
"""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.)
|
|
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.
|
|
|
|
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:
|
|
sem_cube : ndarray (etuds x modimpls x UEs)
|
|
modimpls_evals_poids dict { modimpl.id : evals_poids }
|
|
modimpls_results dict { modimpl.id : ModuleImplResultsAPC }
|
|
"""
|
|
modimpls_results = {}
|
|
modimpls_evals_poids = {}
|
|
modimpls_notes = []
|
|
etudids, etudids_actifs = formsemestre.etudids_actifs()
|
|
for modimpl in formsemestre.modimpls_sorted:
|
|
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
|
|
evals_poids = modimpl.get_evaluations_poids()
|
|
etuds_moy_module = mod_results.compute_module_moy(evals_poids, modimpl_coefs_df)
|
|
modimpls_results[modimpl.id] = mod_results
|
|
modimpls_evals_poids[modimpl.id] = evals_poids
|
|
modimpls_notes.append(etuds_moy_module)
|
|
if len(modimpls_notes) > 0:
|
|
cube = notes_sem_assemble_cube(modimpls_notes)
|
|
else:
|
|
nb_etuds = formsemestre.etuds.count()
|
|
cube = np.zeros((nb_etuds, 0, 0), dtype=float)
|
|
return (
|
|
cube,
|
|
modimpls_evals_poids,
|
|
modimpls_results,
|
|
)
|
|
|
|
|
|
def compute_ue_moys_apc(
|
|
sem_cube: np.array,
|
|
etuds: list,
|
|
modimpls: list,
|
|
modimpl_inscr_df: pd.DataFrame,
|
|
modimpl_coefs_df: pd.DataFrame,
|
|
modimpl_mask: np.array,
|
|
dispense_ues: set[tuple[int, int]],
|
|
block: bool = False,
|
|
) -> pd.DataFrame:
|
|
"""Calcul de la moyenne d'UE en mode APC (BUT).
|
|
La moyenne d'UE est un nombre (note/20), ou NaN si pas de notes disponibles
|
|
|
|
sem_cube: notes moyennes aux modules
|
|
ndarray (etuds x modimpls x UEs)
|
|
(floats avec des NaN)
|
|
etuds : liste des étudiants (dim. 0 du cube)
|
|
modimpls : liste des module_impl (dim. 1 du cube)
|
|
ues : liste des UE (dim. 2 du cube)
|
|
modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl)
|
|
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
|
|
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
|
|
Résultat: DataFrame columns UE (sans bonus), rows etudid
|
|
"""
|
|
nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape
|
|
assert len(modimpls) == nb_modules
|
|
if block or nb_modules == 0 or nb_etuds == 0 or nb_ues_no_bonus == 0:
|
|
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
|
|
assert modimpl_coefs_df.shape[0] == nb_ues_no_bonus
|
|
assert modimpl_coefs_df.shape[1] == nb_modules
|
|
modimpl_inscr = modimpl_inscr_df.values
|
|
# 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)
|
|
|
|
# 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)
|
|
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)
|
|
#
|
|
# Version vectorisée
|
|
#
|
|
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(
|
|
etud_moy_ue,
|
|
index=modimpl_inscr_df.index, # les etudids
|
|
columns=modimpl_coefs_df.index, # les UE sans les UE bonus sport
|
|
)
|
|
# 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
|
|
|
|
|
|
def compute_ue_moys_classic(
|
|
formsemestre: FormSemestre,
|
|
sem_matrix: np.array,
|
|
ues: list,
|
|
modimpl_inscr_df: pd.DataFrame,
|
|
modimpl_coefs: np.array,
|
|
modimpl_mask: np.array,
|
|
block: bool = False,
|
|
) -> 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, ...).
|
|
|
|
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]
|
|
|
|
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)
|
|
ndarray (etuds x modimpls)
|
|
(floats avec des NaN)
|
|
etuds : listes des étudiants (dim. 0 de la matrice)
|
|
ues : liste des UE du semestre
|
|
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
|
|
block: si vrai, ne calcule rien et renvoie des NaNs
|
|
|
|
Résultat:
|
|
- moyennes générales: pd.Series, index etudid
|
|
- moyennes d'UE: DataFrame columns UE, rows etudid
|
|
- 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)
|
|
"""
|
|
if (
|
|
block or (len(modimpl_mask) == 0) or (sem_matrix.shape[0] == 0)
|
|
): # aucun module ou aucun étudiant
|
|
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
|
|
val = np.nan if block else 0.0
|
|
return (
|
|
pd.Series(
|
|
[val] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
|
|
),
|
|
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
|
|
),
|
|
)
|
|
# 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
|
|
nb_ues = len(ues) # en comptant bonus
|
|
|
|
# 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
|
|
)
|
|
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
|
|
ue_modules = np.array(
|
|
[[m.module.ue == ue for m in formsemestre.modimpls_sorted] for ue in ues]
|
|
)[..., np.newaxis][:, modimpl_mask, :]
|
|
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)
|
|
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
|
|
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)
|
|
|
|
return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
|
|
|
|
|
|
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
|
|
)
|
|
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)
|
|
|
|
|
|
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.
|
|
Leurs notes sont positives ou négatives.
|
|
La somme des notes de malus somme est _soustraite_ à la moyenne de chaque UE.
|
|
|
|
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)
|
|
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
|
|
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
|
|
for m in formsemestre.modimpls_sorted
|
|
]
|
|
)
|
|
if len(modimpl_mask):
|
|
malus_moys = sem_modimpl_moys_scalar[:, modimpl_mask].sum(axis=1)
|
|
malus[ue.id] = malus_moys
|
|
|
|
malus.fillna(0.0, inplace=True)
|
|
return malus
|