ScoDoc/app/comp/bonus_spo.py

488 lines
19 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 datetime
import numpy as np
import pandas as pd
from app.models.formsemestre import FormSemestre
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
- etud_moy_gen: Series, index etudid, valeur float (moyenne générale avant bonus)
- etud_moy_ue: DataFrame columns UE (sans sport), rows etudid (moyennes avant bonus)
etud_moy_gen et etud_moy_ue ne sont PAS modifiés (mais utilisés par certains bonus non additifs).
"""
# Si vrai, en APC, si le bonus UE est None, reporte le bonus moy gen:
apc_apply_bonus_mg_to_ues = True
# Si True, reporte toujours le bonus moy gen sur les UE (même en formations classiques)
apply_bonus_mg_to_ues = False
# 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,
etud_moy_gen,
etud_moy_ue,
):
self.formsemestre = formsemestre
self.ues = ues
self.etud_moy_gen = etud_moy_gen
self.etud_moy_ue = etud_moy_ue
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.bonus_ues is None and (
(self.apc_apply_bonus_mg_to_ues and self.formsemestre.formation.is_apc())
or self.apply_bonus_mg_to_ues
):
# 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 BonusSportAdditif(BonusSport):
"""Bonus sport simples calcule un bonus à partir des notes moyennes de modules
de l'UE sport, et ce bonus est soit ajouté à la moyenne générale (formations classiques),
soit ajouté à chaque 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(BonusSportAdditif):
"""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(BonusSportAdditif):
"""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 # tous les points sont comptés
proportion_point = 1.0
class BonusStDenis(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(BonusSportAdditif):
"""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 BonusTours(BonusDirect):
"""Calcul bonus sport & culture IUT Tours.
Les notes des UE bonus (ramenées sur 20) sont sommées
et 1/40 (2,5%) est ajouté aux moyennes: soit à la moyenne générale,
soit pour le BUT à chaque moyenne d'UE.
Le bonus total est limité à 1 point.
"""
name = "bonus_tours"
bonus_moy_gen_limit = 1.0 #
seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés
proportion_point = 1.0 / 40.0
# Bonus simple, mais avec changement de paramètres en 2010 !
class BonusLille(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT Villeneuve d'Ascq
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Lille 1 (sports, 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 4% (2% avant août 2010) de ces points cumulés
s'ajoutent à la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
name = "bonus_lille"
seuil_moy_gen = 10.0 # points comptés au dessus de 10.
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
# La date du semestre ?
if self.formsemestre.date_debut > datetime.date(2010, 8, 1):
self.proportion_point = 0.04
else:
self.proportion_point = 0.02
return super().compute_bonus(
sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
)
class BonusSportMultiplicatif(BonusSport):
"""Bonus sport qui multiplie les moyennes d'UE par un facteur"""
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
amplitude = 0.005 # multiplie les points au dessus du seuil
# C'est un bonus "multiplicatif": on l'exprime en additif,
# sur chaque moyenne d'UE m_0
# Augmenter de 5% correspond à multiplier par a=1.05
# m_1 = a . m_0
# m_1 = m_0 + bonus
# bonus = m_0 (a - 1)
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:
notes = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
notes = np.nan_to_num(notes, copy=False)
factor = (notes - self.seuil_moy_gen) * self.amplitude # 5% si note=20
factor[factor <= 0] = 0.0 # note < seuil_moy_gen, pas de bonus
# S'applique qu'aux moyennes d'UE
bonus = self.etud_moy_ue * factor
self.bonus_ues = bonus # DataFrame
if not self.formsemestre.formation.is_apc():
# s'applique à la moyenne générale
self.bonus_moy_gen = bonus
class BonusGrenobleIUT1(BonusSportMultiplicatif):
"""Bonus IUT1 de Grenoble
À compter de sept. 2021:
La note de sport est sur 20, et on calcule une bonification (en %)
qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant
la formule : bonification (en %) = (note-10)*0,5.
Bonification qui ne s'applique que si la note est >10.
(Une note de 10 donne donc 0% de bonif ; note de 20 : 5% de bonif)
Avant sept. 2021, la note de sport allait de 0 à 5 points (sur 20).
Chaque point correspondait à 0.25% d'augmentation de la moyenne
générale.
Par exemple : note de sport 2/5 : la moyenne générale était augmentée de 0.5%.
"""
name = "bonus_iut1grenoble_2017"
# C'est un bonus "multiplicatif": on l'exprime en additif,
# sur chaque moyenne d'UE m_0
# Augmenter de 5% correspond à multiplier par a=1.05
# m_1 = a . m_0
# m_1 = m_0 + bonus
# bonus = m_0 (a - 1)
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus, avec réglage différent suivant la date"""
if self.formsemestre.date_debut > datetime.date(2021, 7, 15):
self.seuil_moy_gen = 10.0
self.amplitude = 0.005
else: # anciens semestres
self.seuil_moy_gen = 0.0
self.amplitude = 1 / 400.0
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
class BonusLeHavre(BonusSportMultiplicatif):
"""Bonus sport IUT du Havre sur moyenne générale et UE
Les points des modules bonus au dessus de 10/20 sont ajoutés,
et les moyennes d'UE augmentées de 5% de ces points.
"""
name = "bonus_iutlh"
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
amplitude = 0.005 # multiplie les points au dessus du seuil
class BonusNantes(BonusSportAdditif):
"""IUT de Nantes (Septembre 2018)
Nous avons différents types de bonification
(sport, culture, engagement citoyen).
Nous ajoutons aux moyennes une bonification de 0,2 pour chaque item
la bonification totale ne doit pas excéder les 0,5 point.
Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications.
Dans ScoDoc: on a déclarera une UE "sport&culture" dans laquelle on aura des modules
pour chaque activité (Sport, Associations, ...)
avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20, mais en fait ce sera la
valeur de la bonification: entrer 0,1/20 signifiera un bonus de 0,1 point la moyenne générale)
"""
name = "bonus_nantes"
seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés
proportion_point = 1 # multiplie les points au dessus du seuil
bonus_moy_gen_limit = 0.5 # plafonnement à 0.5 points
class BonusRoanne(BonusSportAdditif):
"""IUT de Roanne.
Le bonus est compris entre 0 et 0.35 point
et est toujours appliqué aux UEs.
"""
name = "bonus_iutr"
seuil_moy_gen = 0.0
bonus_moy_gen_limit = 0.35 # plafonnement à 0.35 points
apply_bonus_mg_to_ues = True # sur les UE, même en DUT et LP
class BonusVilleAvray(BonusSport):
"""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