323 lines
13 KiB
Python
323 lines
13 KiB
Python
|
##############################################################################
|
||
|
# ScoDoc
|
||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||
|
# See LICENSE
|
||
|
##############################################################################
|
||
|
|
||
|
"""Classes spécifiques de calcul du bonus sport, culture ou autres activités
|
||
|
|
||
|
Les classes de Bonus fournissent deux méthodes:
|
||
|
- get_bonus_ues()
|
||
|
- get_bonus_moy_gen()
|
||
|
|
||
|
|
||
|
"""
|
||
|
import numpy as np
|
||
|
import pandas as pd
|
||
|
|
||
|
from app import db
|
||
|
from app import models
|
||
|
from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef
|
||
|
from app.comp import moy_mod
|
||
|
from app.models.formsemestre import FormSemestre
|
||
|
from app.scodoc import bonus_sport
|
||
|
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||
|
from app.scodoc.sco_utils import ModuleType
|
||
|
|
||
|
|
||
|
def get_bonus_sport_class_from_name(dept_id):
|
||
|
"""La classe de bonus sport pour le département indiqué.
|
||
|
Note: en ScoDoc 9, le bonus sport est défini gloabelement et
|
||
|
ne dépend donc pas du département.
|
||
|
Résultat: une sous-classe de BonusSport
|
||
|
"""
|
||
|
raise NotImplementedError()
|
||
|
|
||
|
|
||
|
class BonusSport:
|
||
|
"""Calcul du bonus sport.
|
||
|
|
||
|
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)
|
||
|
- ues: les ues du semestre (incluant le bonus sport)
|
||
|
- modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl)
|
||
|
- modimpl_coefs: les coefs des modules
|
||
|
En classique: 1d ndarray de float (modimpl)
|
||
|
En APC: 2d ndarray de float, (modimpl x UE) <= attention à transposer
|
||
|
"""
|
||
|
|
||
|
# Si vrai, en APC, si le bonus UE est None, reporte le bonus moy gen:
|
||
|
apc_apply_bonus_mg_to_ues = True
|
||
|
# Attributs virtuels:
|
||
|
seuil_moy_gen = None
|
||
|
proportion_point = None
|
||
|
bonus_moy_gen_limit = None
|
||
|
|
||
|
name = "virtual"
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
formsemestre: FormSemestre,
|
||
|
sem_modimpl_moys: np.array,
|
||
|
ues: list,
|
||
|
modimpl_inscr_df: pd.DataFrame,
|
||
|
modimpl_coefs: np.array,
|
||
|
):
|
||
|
self.formsemestre = formsemestre
|
||
|
self.ues = ues
|
||
|
self.etuds_idx = modimpl_inscr_df.index # les étudiants inscrits au semestre
|
||
|
self.bonus_ues: pd.DataFrame = None # virtual
|
||
|
self.bonus_moy_gen: pd.Series = None # virtual
|
||
|
# Restreint aux modules standards des UE de type "sport":
|
||
|
modimpl_mask = np.array(
|
||
|
[
|
||
|
(m.module.module_type == ModuleType.STANDARD)
|
||
|
and (m.module.ue.type == UE_SPORT)
|
||
|
for m in formsemestre.modimpls_sorted
|
||
|
]
|
||
|
)
|
||
|
self.modimpls_spo = [
|
||
|
modimpl
|
||
|
for i, modimpl in enumerate(formsemestre.modimpls_sorted)
|
||
|
if modimpl_mask[i]
|
||
|
]
|
||
|
"liste des modimpls sport"
|
||
|
|
||
|
# Les moyennes des modules "sport": (une par UE en APC)
|
||
|
sem_modimpl_moys_spo = sem_modimpl_moys[:, modimpl_mask]
|
||
|
# Les inscriptions aux modules sport:
|
||
|
modimpl_inscr_spo = modimpl_inscr_df.values[:, modimpl_mask]
|
||
|
# Les coefficients des modules sport (en apc: nb_mod_sport x nb_ue)
|
||
|
modimpl_coefs_spo = modimpl_coefs[modimpl_mask]
|
||
|
# sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport)
|
||
|
# ou (nb_etuds, nb_mod_sport, nb_ues)
|
||
|
nb_etuds, nb_mod_sport = sem_modimpl_moys_spo.shape[:2]
|
||
|
nb_ues = len(ues)
|
||
|
# Enlève les NaN du numérateur:
|
||
|
sem_modimpl_moys_no_nan = np.nan_to_num(sem_modimpl_moys_spo, nan=0.0)
|
||
|
|
||
|
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
|
||
|
if formsemestre.formation.is_apc():
|
||
|
# BUT
|
||
|
nb_ues_no_bonus = sem_modimpl_moys.shape[2]
|
||
|
# Duplique les inscriptions sur les UEs non bonus:
|
||
|
modimpl_inscr_spo_stacked = np.stack(
|
||
|
[modimpl_inscr_spo] * nb_ues_no_bonus, axis=2
|
||
|
)
|
||
|
# Ne prend pas en compte les notes des étudiants non inscrits au module:
|
||
|
# Annule les notes:
|
||
|
sem_modimpl_moys_inscrits = np.where(
|
||
|
modimpl_inscr_spo_stacked, sem_modimpl_moys_no_nan, 0.0
|
||
|
)
|
||
|
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
|
||
|
modimpl_coefs_etuds = np.where(
|
||
|
modimpl_inscr_spo_stacked,
|
||
|
np.stack([modimpl_coefs_spo.T] * nb_etuds),
|
||
|
0.0,
|
||
|
)
|
||
|
else:
|
||
|
# Formations classiques
|
||
|
# Ne prend pas en compte les notes des étudiants non inscrits au module:
|
||
|
# Annule les notes:
|
||
|
sem_modimpl_moys_inscrits = np.where(
|
||
|
modimpl_inscr_spo, sem_modimpl_moys_no_nan, 0.0
|
||
|
)
|
||
|
modimpl_coefs_spo = modimpl_coefs_spo.T
|
||
|
modimpl_coefs_etuds = np.where(
|
||
|
modimpl_inscr_spo, np.stack([modimpl_coefs_spo] * nb_etuds), 0.0
|
||
|
)
|
||
|
# Annule les coefs des modules NaN (nb_etuds x nb_mod_sport)
|
||
|
modimpl_coefs_etuds_no_nan = np.where(
|
||
|
np.isnan(sem_modimpl_moys_spo), 0.0, modimpl_coefs_etuds
|
||
|
)
|
||
|
#
|
||
|
self.compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||
|
|
||
|
def compute_bonus(
|
||
|
self,
|
||
|
sem_modimpl_moys_inscrits: np.ndarray,
|
||
|
modimpl_coefs_etuds_no_nan: np.ndarray,
|
||
|
):
|
||
|
"""Calcul des bonus: méthode virtuelle à écraser.
|
||
|
Arguments:
|
||
|
- sem_modimpl_moys_inscrits:
|
||
|
ndarray (nb_etuds, mod_sport) ou en APC (nb_etuds, mods_sport, nb_ue)
|
||
|
les notes aux modules sports auxquel l'étudiant est inscrit, 0 sinon. Pas de nans.
|
||
|
- modimpl_coefs_etuds_no_nan:
|
||
|
les coefficients: float ndarray
|
||
|
|
||
|
Résultat: None
|
||
|
"""
|
||
|
raise NotImplementedError("méthode virtuelle")
|
||
|
|
||
|
def get_bonus_ues(self) -> pd.Series:
|
||
|
"""Les bonus à appliquer aux UE
|
||
|
Résultat: DataFrame de float, index etudid, columns: ue.id
|
||
|
"""
|
||
|
if (
|
||
|
self.formsemestre.formation.is_apc()
|
||
|
and self.apc_apply_bonus_mg_to_ues
|
||
|
and self.bonus_ues is None
|
||
|
):
|
||
|
# reporte uniformément le bonus moyenne générale sur les UEs
|
||
|
# (assure la compatibilité de la plupart des anciens bonus avec le BUT)
|
||
|
# ues = self.formsemestre.query_ues(with_sport=False)
|
||
|
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
|
||
|
bonus_moy_gen = self.get_bonus_moy_gen()
|
||
|
bonus_ues = np.stack([bonus_moy_gen.values] * len(ues_idx), axis=1)
|
||
|
return pd.DataFrame(bonus_ues, index=self.etuds_idx, columns=ues_idx)
|
||
|
|
||
|
return self.bonus_ues
|
||
|
|
||
|
def get_bonus_moy_gen(self):
|
||
|
"""Le bonus à appliquer à la moyenne générale.
|
||
|
Résultat: Series de float, index etudid
|
||
|
"""
|
||
|
return self.bonus_moy_gen
|
||
|
|
||
|
|
||
|
class BonusSportSimples(BonusSport):
|
||
|
"""Les bonus sport simples calcule un bonus à partir des notes moyennes de modules
|
||
|
de l'UE sport, et ce bonus est soit appliqué sur la moyenne générale (formations classiques),
|
||
|
soit réparti sur les UE (formations APC).
|
||
|
|
||
|
Le bonus est par défaut calculé comme:
|
||
|
Les points au-dessus du seuil (par défaut) 10 sur 20 obtenus dans chacun des
|
||
|
modules optionnels sont cumulés et une fraction de ces points cumulés s'ajoute
|
||
|
à la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||
|
"""
|
||
|
|
||
|
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
|
||
|
proportion_point = 0.05 # multiplie les points au dessus du seuil
|
||
|
|
||
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||
|
"""calcul du bonus"""
|
||
|
bonus_moy_gen_arr = np.sum(
|
||
|
np.where(
|
||
|
sem_modimpl_moys_inscrits > self.seuil_moy_gen,
|
||
|
(sem_modimpl_moys_inscrits - self.seuil_moy_gen)
|
||
|
* self.proportion_point,
|
||
|
0.0,
|
||
|
),
|
||
|
axis=1,
|
||
|
)
|
||
|
# en APC, applati la moyenne gen. XXX pourrait être fait en amont
|
||
|
if len(bonus_moy_gen_arr.shape) > 1:
|
||
|
bonus_moy_gen_arr = bonus_moy_gen_arr.sum(axis=1)
|
||
|
# Bonus moyenne générale, et 0 sur les UE
|
||
|
self.bonus_moy_gen = pd.Series(
|
||
|
bonus_moy_gen_arr, index=self.etuds_idx, dtype=float
|
||
|
)
|
||
|
if self.bonus_moy_gen_limit is not None:
|
||
|
# Seuil: bonus (sur moy. gen.) limité à bonus_moy_gen_limit points
|
||
|
self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_moy_gen_limit)
|
||
|
|
||
|
# Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs.
|
||
|
|
||
|
|
||
|
# bonus_ue = np.stack([modimpl_coefs_spo.T] * nb_ues)
|
||
|
|
||
|
|
||
|
class BonusIUTV(BonusSportSimples):
|
||
|
"""Calcul bonus modules optionels (sport, culture), règle IUT Villetaneuse
|
||
|
|
||
|
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||
|
de l'Université Paris 13 (sports, musique, deuxième langue,
|
||
|
culture, etc) non rattachés à une unité d'enseignement. Les points
|
||
|
au-dessus de 10 sur 20 obtenus dans chacune des matières
|
||
|
optionnelles sont cumulés et 5% de ces points cumulés s'ajoutent à
|
||
|
la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||
|
"""
|
||
|
|
||
|
name = "bonus_iutv"
|
||
|
pass # oui, c'ets le bonus par défaut
|
||
|
|
||
|
|
||
|
class BonusDirect(BonusSportSimples):
|
||
|
"""Bonus direct: les points sont directement ajoutés à la moyenne générale.
|
||
|
Les coefficients sont ignorés: tous les points de bonus sont sommés.
|
||
|
(rappel: la note est ramenée sur 20 avant application).
|
||
|
"""
|
||
|
|
||
|
name = "bonus_direct"
|
||
|
seuil_moy_gen = 0.0 # seuls le spoints au dessus du seuil sont comptés
|
||
|
proportion_point = 1.0
|
||
|
|
||
|
|
||
|
class BonusIUTStDenis(BonusIUTV):
|
||
|
"""Semblable à bonus_iutv mais sans coefficients et total limité à 0.5 points."""
|
||
|
|
||
|
name = "bonus_iut_stdenis"
|
||
|
bonus_moy_gen_limit = 0.5
|
||
|
|
||
|
|
||
|
class BonusColmar(BonusSportSimples):
|
||
|
"""Calcul bonus modules optionels (sport, culture), règle IUT Colmar.
|
||
|
|
||
|
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||
|
de l'U.H.A. (sports, musique, deuxième langue, culture, etc) non
|
||
|
rattachés à une unité d'enseignement. Les points au-dessus de 10
|
||
|
sur 20 obtenus dans chacune des matières optionnelles sont cumulés
|
||
|
dans la limite de 10 points. 5% de ces points cumulés s'ajoutent à
|
||
|
la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||
|
"""
|
||
|
|
||
|
# note: cela revient à dire que l'on ajoute 5% des points au dessus de 10,
|
||
|
# et qu'on limite à 5% de 10, soit 0.5 points
|
||
|
# ce bonus est donc strictement identique à celui de St Denis (BonusIUTStDenis)
|
||
|
name = "bonus_colmar"
|
||
|
bonus_moy_gen_limit = 0.5
|
||
|
|
||
|
|
||
|
class BonusVilleAvray:
|
||
|
"""Calcul bonus modules optionels (sport, culture), règle IUT Ville d'Avray
|
||
|
|
||
|
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||
|
de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement.
|
||
|
Si la note est >= 10 et < 12, bonus de 0.1 point
|
||
|
Si la note est >= 12 et < 16, bonus de 0.2 point
|
||
|
Si la note est >= 16, bonus de 0.3 point
|
||
|
Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
|
||
|
l'étudiant.
|
||
|
"""
|
||
|
|
||
|
name = "bonus_iutva"
|
||
|
|
||
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||
|
"""calcul du bonus"""
|
||
|
# Calcule moyenne pondérée des notes de sport:
|
||
|
bonus_moy_gen_arr = np.sum(
|
||
|
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
|
||
|
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
|
||
|
bonus_moy_gen_arr[bonus_moy_gen_arr >= 10.0] = 0.1
|
||
|
bonus_moy_gen_arr[bonus_moy_gen_arr >= 12.0] = 0.2
|
||
|
bonus_moy_gen_arr[bonus_moy_gen_arr >= 16.0] = 0.3
|
||
|
|
||
|
# Bonus moyenne générale, et 0 sur les UE
|
||
|
self.bonus_moy_gen = pd.Series(
|
||
|
bonus_moy_gen_arr, index=self.etuds_idx, dtype=float
|
||
|
)
|
||
|
if self.bonus_moy_gen_limit is not None:
|
||
|
# Seuil: bonus (sur moy. gen.) limité à bonus_moy_gen_limit points
|
||
|
self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_moy_gen_limit)
|
||
|
|
||
|
# Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs.
|
||
|
|
||
|
|
||
|
def get_bonus_class_dict(start=BonusSport, d=None):
|
||
|
"""Dictionnaire des classes de bonus
|
||
|
(liste les sous-classes de BonusSport ayant un nom)
|
||
|
Resultat: { name : class }
|
||
|
"""
|
||
|
if d is None:
|
||
|
d = {}
|
||
|
if start.name != "virtual":
|
||
|
d[start.name] = start
|
||
|
for subclass in start.__subclasses__():
|
||
|
get_bonus_class_dict(subclass, d=d)
|
||
|
return d
|