ScoDoc/app/pe/pe_semtag.py

287 lines
11 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
#
##############################################################################
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""
Created on Fri Sep 9 09:15:05 2016
@author: barasc
"""
from app.comp.res_sem import load_formsemestre_results
from app.models import UniteEns
from app.pe import pe_affichage
from app.pe.pe_ressemtag import ResSemTag
import pandas as pd
import numpy as np
from app.pe.pe_rcs import RCS
from app.pe.pe_tabletags import TableTag
from app.pe.pe_moytag import MoyennesTag
class SemTag(TableTag):
def __init__(self, rcs: RCS, res_sems_tags: dict[int, ResSemTag]):
"""Calcule les moyennes/classements par tag à un RCS d'un seul semestre
(ici semestre) de type 'Sx' (par ex. 'S1', 'S2', ...) :
* pour les étudiants non redoublants, ce sont les moyennes/classements
du semestre suivi
* pour les étudiants redoublants, c'est une fusion des moyennes/classements
suivis les différents 'Sx' (donné par dans le rcs)
Les **tags considérés** sont uniquement ceux du dernier semestre du RCS
Args:
rcs: Un RCS (identifié par un nom et l'id de son semestre terminal)
res_sems_tags: Les données sur les résultats des semestres taggués
"""
TableTag.__init__(self)
self.rcs_id = rcs.rcs_id
"""Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)"""
self.rcs = rcs
"""RCS associé au RCS taggué"""
assert self.rcs.nom.startswith(
"S"
), "Un SemTag ne peut être utilisé que pour un RCS de la forme Sx"
self.nom = self.get_repr()
"""Représentation textuelle du RCS taggué"""
# Les données du formsemestre_terminal
self.formsemestre_terminal = rcs.formsemestre_final
"""Le formsemestre terminal"""
# Les résultats du formsemestre terminal
nt = load_formsemestre_results(self.formsemestre_terminal)
self.semestres_aggreges = rcs.semestres_aggreges
"""Les semestres aggrégés"""
self.res_sems_tags = {}
"""Les résultats des semestres taggués (limités aux semestres aggrégés)"""
try:
for frmsem_id in self.semestres_aggreges:
self.res_sems_tags[frmsem_id] = res_sems_tags[frmsem_id]
except:
raise ValueError("Résultats des semestres taggués manquants")
# Les étudiants (etuds, états civils & etudis)
self.add_etuds(nt.etuds)
# Les tags
self.tags_sorted = self.comp_tags_list()
"""Tags (extraits uniquement du semestre terminal de l'aggrégat)"""
# Les UEs
self.ues = self.comp_ues(tag="but")
self.acronymes_ues_sorted = sorted([ue.acronyme for ue in self.ues.values()])
"""UEs extraites du semestre terminal de l'aggrégat (avec
check de concordance sur les UE des semestres_aggrégés)"""
# Les inscriptions aux UEs
self.ues_inscr_parcours_df = self.comp_ues_inscr_parcours(tag="but")
"""Les inscriptions aux UEs (extraites uniquement du semestre terminal)"""
self.moyennes_tags: dict[str, MoyennesTag] = {}
"""Moyennes/classements par tag (qu'ils soient personnalisés ou automatiques)"""
self.moyennes_tags: dict[str, pd.DataFrame] = {}
"""Les notes aux UEs dans différents tags"""
# Masque des inscriptions
inscr_mask = self.ues_inscr_parcours_df.to_numpy()
for tag in self.tags_sorted:
# Cube de note
notes_cube = self.compute_notes_ues_cube(tag, self.acronymes_ues_sorted)
# Calcule des moyennes sous forme d'un dataframe"""
moys_ues = compute_notes_ues(
notes_cube,
self.etudids,
self.acronymes_ues_sorted,
inscr_mask,
)
# Les moyennes
self.moyennes_tags[tag] = MoyennesTag(tag,
self.ues,
moys_ues,
self.ues_inscr_parcours_df)
def __eq__(self, other):
"""Egalité de 2 RCS taggués sur la base de leur identifiant"""
return self.rcs_id == other.rcs_id
def get_repr(self, verbose=False) -> str:
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
est basée)"""
return self.rcs.get_repr(verbose=verbose)
def compute_notes_ues_cube(self, tag, acronymes_ues_sorted):
"""Construit le cube de notes des UEs (etudid x accronyme_ue x semestre_aggregé)
nécessaire au calcul des moyennes du tag pour le RCS Sx
"""
# Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2)
etudids = [etud.etudid for etud in self.etuds]
# acronymes_ues = sorted([ue.acronyme for ue in self.ues.values()])
semestres_id = list(self.res_sems_tags.keys())
dfs = {}
for frmsem_id in semestres_id:
# Partant d'un dataframe vierge
df = pd.DataFrame(np.nan, index=etudids, columns=acronymes_ues_sorted)
# Charge les notes du semestre tag
sem_tag = self.res_sems_tags[frmsem_id]
moys_tag = sem_tag.moyennes_tags[tag]
notes = moys_tag.notes_ues # dataframe etudids x ues
acronymes_ues_sem = list(
notes.columns
) # les acronymes des UEs du semestre tag
# UEs communes à celles du SemTag (celles du dernier semestre du RCS)
ues_communes = list(set(acronymes_ues_sorted) & set(acronymes_ues_sem))
# Etudiants communs
etudids_communs = df.index.intersection(notes.index)
# Recopie
df.loc[etudids_communs, ues_communes] = notes.loc[
etudids_communs, ues_communes
]
# Supprime tout ce qui n'est pas numérique
for col in df.columns:
df[col] = pd.to_numeric(df[col], errors="coerce")
# Stocke le df
dfs[frmsem_id] = df
"""Réunit les notes sous forme d'un cube semestres x etdids x ues"""
semestres_x_etudids_x_ues = [dfs[fid].values for fid in dfs]
etudids_x_ues_x_semestres = np.stack(semestres_x_etudids_x_ues, axis=-1)
return etudids_x_ues_x_semestres
def comp_tags_list(self) -> list[str]:
"""Récupère les tag du semestre taggué associé au semestre final du RCS
Returns:
Une liste de tags triés par ordre alphabétique
"""
tags = []
dernier_frmid = self.formsemestre_terminal.formsemestre_id
dernier_semestre_tag = self.res_sems_tags[dernier_frmid]
tags = dernier_semestre_tag.tags_sorted
pe_affichage.pe_print(f"* Tags : {', '.join(tags)}")
return tags
def comp_ues(self, tag="but") -> dict[int, UniteEns]:
"""Récupère les UEs à aggréger, en s'appuyant sur la moyenne générale
(tag but) du semestre final du RCS
Returns:
Un dictionnaire donnant les UEs
"""
dernier_frmid = self.formsemestre_terminal.formsemestre_id
dernier_semestre_tag = self.res_sems_tags[dernier_frmid]
moy_tag = dernier_semestre_tag.moyennes_tags[tag]
return moy_tag.ues # les UEs
def comp_ues_inscr_parcours(self, tag="but") -> pd.DataFrame:
"""Récupère les informations d'inscription des étudiants aux UEs : ne
conserve que les UEs du semestre terminal (pour les redoublants)
Returns:
Un dataFrame etudids x UE indiquant si un étudiant est inscrit à une UE
"""
dernier_frmid = self.formsemestre_terminal.formsemestre_id
dernier_semestre_tag = self.res_sems_tags[dernier_frmid]
moy_tag = dernier_semestre_tag.moyennes_tags[tag]
return moy_tag.ues_inscr_parcours_df
def compute_notes_ues(
set_cube: np.array,
etudids: list,
acronymes_ues: list,
inscr_mask: np.array,
):
"""Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE
par UE) obtenue par un étudiant à un semestre.
Args:
set_cube: notes moyennes aux modules ndarray
(semestre_ids x etudids x UEs), des floats avec des NaN
etudids: liste des étudiants (dim. 0 du cube)
acronymes_ues: liste des acronymes des ues (dim. 1 du cube)
inscr_mask: masque etudids x UE traduisant les inscriptions des
étudiants aux UE (du semestre terminal)
Returns:
Un DataFrame avec pour columns les moyennes par ues,
et pour rows les etudid
"""
nb_etuds, nb_ues, nb_semestres = set_cube.shape
nb_etuds_mask, nb_ues_mask = inscr_mask.shape
assert nb_etuds == len(etudids)
assert nb_ues == len(acronymes_ues)
assert nb_etuds == nb_etuds_mask
assert nb_ues == nb_ues_mask
# Quelles entrées du cube contiennent des notes ?
mask = ~np.isnan(set_cube)
# Entrées à garder dans le cube en fonction du mask d'inscription
inscr_mask_3D = np.stack([inscr_mask]*nb_semestres, axis=-1)
set_cube = set_cube*inscr_mask_3D
# Enlève les NaN du cube pour les entrées manquantes : NaN -> -1.0
set_cube_no_nan = np.nan_to_num(set_cube, nan=-1.0)
# Les moyennes par ues
# TODO: Pour l'instant un max sans prise en compte des UE capitalisées
etud_moy = np.max(set_cube_no_nan, axis=2)
# Fix les max non calculé -1 -> NaN
etud_moy[etud_moy < 0] = np.NaN
# Le dataFrame
etud_moy_tag_df = pd.DataFrame(
etud_moy,
index=etudids, # les etudids
columns=acronymes_ues, # les tags
)
etud_moy_tag_df.fillna(np.nan)
return etud_moy_tag_df