ScoDoc-PE/app/pe/moys/pe_rcstag.py

430 lines
18 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.models import FormSemestre
from app.pe import pe_affichage
import pandas as pd
import numpy as np
from app.pe.rcss import pe_rcs, pe_rcsemx
import app.pe.moys.pe_sxtag as pe_sxtag
import app.pe.pe_comp as pe_comp
from app.pe.moys import pe_tabletags, pe_moytag
from app.scodoc.sco_utils import ModuleType
class RCSemXTag(pe_tabletags.TableTag):
def __init__(
self,
rcsemx: pe_rcsemx.RCSemX,
sxstags: dict[(str, int) : pe_sxtag.SxTag],
semXs_suivis: dict[int, dict],
):
"""Calcule les moyennes par tag (orientées compétences)
d'un regroupement de SxTag
(RCRCF), pour extraire les classements par tag pour un
groupe d'étudiants donnés. Le groupe d'étudiants est formé par ceux ayant tous
participé au même semestre terminal.
Args:
rcsemx: Le RCSemX (identifié par un nom et l'id de son semestre terminal)
sxstags: Les données sur les SemX taggués
semXs_suivis: Les données indiquant quels SXTags sont à prendre en compte
pour chaque étudiant
"""
pe_tabletags.TableTag.__init__(self)
self.rcs_id: tuple(str, int) = rcsemx.rcs_id
"""Identifiant du RCSemXTag (identique au RCSemX sur lequel il s'appuie)"""
self.rcsemx: pe_rcsemx.RCSemX = rcsemx
"""Le regroupement RCSemX associé au RCSemXTag"""
self.semXs_suivis = semXs_suivis
"""Les semXs suivis par les étudiants"""
self.nom = self.get_repr()
"""Représentation textuelle du RSCtag"""
# Les données du semestre final
self.formsemestre_final: FormSemestre = rcsemx.formsemestre_final
"""Le semestre final"""
self.fid_final: int = rcsemx.formsemestre_final.formsemestre_id
"""Le fid du semestre final"""
# Affichage pour debug
pe_affichage.pe_print(f"*** {self.get_repr(verbose=True)}")
# Les données aggrégés (RCRCF + SxTags)
self.semXs_aggreges: dict[(str, int) : pe_rcsemx.RCSemX] = rcsemx.semXs_aggreges
"""Les SemX aggrégés"""
self.sxstags_aggreges = {}
"""Les SxTag associés aux SemX aggrégés"""
try:
for rcf_id in self.semXs_aggreges:
self.sxstags_aggreges[rcf_id] = sxstags[rcf_id]
except:
raise ValueError("Semestres SxTag manquants")
self.sxtags_connus = sxstags # Tous les sxstags connus
# Les étudiants (etuds, états civils & etudis)
sems_dans_aggregat = rcsemx.aggregat
sxtag_final = self.sxstags_aggreges[(sems_dans_aggregat[-1], self.rcs_id[1])]
self.etuds = sxtag_final.etuds
"""Les étudiants (extraits du semestre final)"""
self.add_etuds(self.etuds)
self.etudids_sorted = sorted(self.etudids)
"""Les étudids triés"""
# Les compétences (extraites de tous les Sxtags)
self.acronymes_ues_to_competences = self._do_acronymes_to_competences()
"""L'association acronyme d'UEs -> compétence (extraites des SxTag aggrégés)"""
self.competences_sorted = sorted(
set(self.acronymes_ues_to_competences.values())
)
"""Compétences (triées par nom, extraites des SxTag aggrégés)"""
aff = pe_affichage.repr_comp_et_ues(self.acronymes_ues_to_competences)
pe_affichage.pe_print(f"--> Compétences : {aff}")
# Les tags
self.tags_sorted = self._do_taglist()
"""Tags extraits de tous les SxTag aggrégés"""
aff_tag = ["👜" + tag for tag in self.tags_sorted]
pe_affichage.pe_print(f"--> Tags : {', '.join(aff_tag)}")
# Les moyennes
self.moyennes_tags: dict[str, pe_moytag.MoyennesTag] = {}
"""Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)"""
for tag in self.tags_sorted:
pe_affichage.pe_print(f"--> Moyennes du tag 👜{tag}")
# Cubes d'inscription (etudids_sorted x compétences_sorted x sxstags),
# de notes et de coeffs pour la moyenne générale
# en "aggrégant" les données des sxstags, compétence par compétence
(
inscr_df,
inscr_cube,
notes_df,
notes_cube,
coeffs_df,
coeffs_cube,
coeffs_rcues_df,
coeffs_rcues_cube,
) = self.assemble_cubes(tag)
# Calcule les moyennes, et synthétise les coeffs
(
moys_competences,
matrice_coeffs_moy_gen,
) = self.compute_notes_et_coeffs_competences(
notes_cube, coeffs_cube, coeffs_rcues_cube, inscr_cube
)
# Affichage des coeffs
aff = pe_affichage.repr_profil_coeffs(
matrice_coeffs_moy_gen, with_index=True
)
pe_affichage.pe_print(f" > Moyenne calculée avec pour coeffs : {aff}")
# Mémorise les moyennes et les coeff associés
self.moyennes_tags[tag] = pe_moytag.MoyennesTag(
tag,
pe_moytag.CODE_MOY_COMPETENCES,
moys_competences,
matrice_coeffs_moy_gen,
)
def __eq__(self, other):
"""Egalité de 2 RCS taggués sur la base de leur identifiant"""
return self.rcs_id == other.sxtag_id
def get_repr(self, verbose=True) -> str:
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
est basée)"""
if verbose:
return f"{self.__class__.__name__} basé sur " + self.rcsemx.get_repr(
verbose=verbose
)
else:
return f"{self.__class__.__name__} {self.rcs_id}"
def assemble_cubes(self, tag):
"""Pour un tag donné, construit les cubes :
* d'inscriptions aux compétences (etudid x competences x SxTag)
* de notes (etudid x competences x SxTag)
* des coeffs pour le calcul des moyennes générales par UE (etudid x competences x SxTag)
* des coeffs pour le calcul des regroupements cohérents d'UE/compétences
nécessaires au calcul des moyennes, en :
* transformant les données des UEs en données de compétences (changement de noms)
* fusionnant les données d'un même semestre, lorsque plusieurs UEs traitent d'une même compétence (cas des RCSx = Sx)
* aggrégeant les données de compétences sur plusieurs semestres (cas des RCSx = xA ou xS)
Args:
tag: Le tag visé
"""
# etudids_sorted: list[int],
# competences_sorted: list[str],
# sxstags: dict[(str, int) : pe_sxtag.SxTag],
inscriptions_dfs = {}
notes_dfs = {}
coeffs_moy_gen_dfs = {}
coeffs_rcue_dfs = {}
for sxtag_id, sxtag in self.sxstags_aggreges.items():
# Partant de dataframes vierges
inscription_df = pd.DataFrame(
np.nan, index=self.etudids_sorted, columns=self.competences_sorted
)
notes_df = pd.DataFrame(
np.nan, index=self.etudids_sorted, columns=self.competences_sorted
)
coeffs_moy_gen_df = pd.DataFrame(
np.nan, index=self.etudids_sorted, columns=self.competences_sorted
)
coeffs_rcue_df = pd.DataFrame(
np.nan, index=self.etudids_sorted, columns=self.competences_sorted
)
# Charge les données du semestre tag (copie car changement de nom de colonnes à venir)
if tag in sxtag.moyennes_tags: # si le tag est présent dans le semestre
moys_tag = sxtag.moyennes_tags[tag]
# Les inscr, les notes, les coeffs
acro_ues_inscr_parcours = sxtag.acro_ues_inscr_parcours
notes = moys_tag.matrice_notes_gen
coeffs_moy_gen = moys_tag.matrice_coeffs_moy_gen # les coeffs
coeffs_rcues = sxtag.coefs_rcue # dictionnaire UE -> coeff
# Traduction des acronymes d'UE en compétences
# comp_to_ues = pe_comp.asso_comp_to_accronymes(self.acronymes_ues_to_competences)
acronymes_ues_columns = notes.columns
for acronyme in acronymes_ues_columns:
# La compétence visée
competence = self.acronymes_ues_to_competences[acronyme] # La comp
# Les étud inscrits à la comp reportés dans l'inscription au RCSemX
comp_inscr = acro_ues_inscr_parcours[
acro_ues_inscr_parcours.notnull()
].index
etudids_communs = list(
inscription_df.index.intersection(comp_inscr)
)
inscription_df.loc[
etudids_communs, competence
] = acro_ues_inscr_parcours.loc[etudids_communs, acronyme]
# Les étud ayant une note à l'acronyme de la comp (donc à la comp)
etuds_avec_notes = notes[notes[acronyme].notnull()].index
etudids_communs = list(
notes_df.index.intersection(etuds_avec_notes)
)
notes_df.loc[etudids_communs, competence] = notes.loc[
etudids_communs, acronyme
]
# Les coeffs pour la moyenne générale
etuds_avec_coeffs = coeffs_moy_gen[
coeffs_moy_gen[acronyme].notnull()
].index
etudids_communs = list(
coeffs_moy_gen_df.index.intersection(etuds_avec_coeffs)
)
coeffs_moy_gen_df.loc[
etudids_communs, competence
] = coeffs_moy_gen.loc[etudids_communs, acronyme]
# Les coeffs des RCUE reportés là où les étudiants ont des notes
etuds_avec_notes = notes[notes[acronyme].notnull()].index
etudids_communs = list(
notes_df.index.intersection(etuds_avec_notes)
)
coeffs_rcue_df.loc[etudids_communs, competence] = coeffs_rcues[
acronyme
]
# Supprime tout ce qui n'est pas numérique
# for col in notes_df.columns:
# notes_df[col] = pd.to_numeric(notes_df[col], errors="coerce")
# Stocke les dfs
inscriptions_dfs[sxtag_id] = inscription_df
notes_dfs[sxtag_id] = notes_df
coeffs_moy_gen_dfs[sxtag_id] = coeffs_moy_gen_df
coeffs_rcue_dfs[sxtag_id] = coeffs_rcue_df
# Réunit les inscriptions sous forme d'un cube etudids x competences x semestres
sxtag_x_etudids_x_comps = [
inscriptions_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges
]
inscriptions_etudids_x_comps_x_sxtag = np.stack(
sxtag_x_etudids_x_comps, axis=-1
)
# Réunit les notes sous forme d'un cube etudids x competences x semestres
sxtag_x_etudids_x_comps = [
notes_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges
]
notes_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1)
# Réunit les coeffs sous forme d'un cube etudids x competences x semestres
sxtag_x_etudids_x_comps = [
coeffs_moy_gen_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges
]
coeffs_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1)
# Normalise les coeffs de rcue par année (pour que le poids des années soit le
# même)
# Réunit les coeffs sous forme d'un cube etudids x competences x semestres
sxtag_x_etudids_x_comps = [
coeffs_rcue_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges
]
coeffs_rcues_etudids_x_comps_x_sxtag = np.stack(
sxtag_x_etudids_x_comps, axis=-1
)
return (
inscriptions_dfs,
inscriptions_etudids_x_comps_x_sxtag,
notes_dfs,
notes_etudids_x_comps_x_sxtag,
coeffs_moy_gen_dfs,
coeffs_etudids_x_comps_x_sxtag,
coeffs_rcue_dfs,
coeffs_rcues_etudids_x_comps_x_sxtag,
)
def _do_taglist(self) -> list[str]:
"""Synthétise les tags à partir des Sxtags aggrégés.
Returns:
Liste de tags triés par ordre alphabétique
"""
tags = []
for frmsem_id in self.sxstags_aggreges:
tags.extend(self.sxstags_aggreges[frmsem_id].tags_sorted)
return sorted(set(tags))
def _do_acronymes_to_competences(self) -> dict[str:str]:
"""Synthétise l'association complète {acronyme_ue: competences}
extraite de toutes les données/associations des SxTags
aggrégés.
Returns:
Un dictionnaire {'acronyme_ue' : 'compétences'}
"""
dict_competences = {}
for sxtag_id, sxtag in self.sxstags_aggreges.items():
dict_competences |= sxtag.acronymes_ues_to_competences
return dict_competences
def compute_notes_et_coeffs_competences(
self,
notes_cube: np.array,
coeffs_cube: np.array,
coeffs_rcue: np.array,
inscr_mask: np.array,
):
"""Calcule la moyenne par compétences (à un tag donné) sur plusieurs semestres (partant du set_cube).
La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles
*Remarque* : Adaptation de moy_ue.compute_ue_moys_apc au cas des moyennes de tag
par aggrégat de plusieurs semestres.
Args:
notes_cube: notes moyennes aux compétences ndarray
(etuds x UEs|compétences x sxtags), des floats avec des NaN
coeffs_cube: coeffs appliqués aux compétences
(etuds x UEs|compétences x sxtags) dans le calcul des moyennes générales,
des floats avec des NaN
coeffs_rcue_cube: coeffs des RCUE appliqués dans les moyennes de RCS
inscr_mask: inscriptions aux compétences ndarray
(etuds x UEs|compétences x sxtags), des 0 et des 1
Returns:
Un DataFrame avec pour columns les moyennes par tags,
et pour rows les etudid
"""
# etudids_sorted: liste des étudiants (dim. 0 du cube)
# competences_sorted: list (dim. 1 du cube)
nb_etuds, nb_comps, nb_semestres = notes_cube.shape
# assert nb_etuds == len(etudids_sorted)
# assert nb_comps == len(competences_sorted)
# Applique le masque d'inscriptions aux notes et aux coeffs
notes_significatives = notes_cube * inscr_mask
coeffs_significatifs = coeffs_cube * inscr_mask
# Enlève les NaN des cubes pour les entrées manquantes
notes_no_nan = np.nan_to_num(notes_significatives, nan=0.0)
coeffs_no_nan = np.nan_to_num(coeffs_significatifs, nan=0.0)
# Les moyennes par tag
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
mask = ~np.isnan(
notes_significatives
) # Quelles entrées contiennent des notes ?
etud_moy_tag = np.sum(notes_no_nan, axis=2) / np.sum(mask, axis=2)
coeffs_pris_en_compte = coeffs_no_nan * mask
coeff_tag = np.sum(coeffs_pris_en_compte, axis=2)
inscr_prise_en_compte = inscr_mask * mask
inscr_prise_en_compte = np.nan_to_num(inscr_prise_en_compte, nan=-1.0)
inscr_tag = np.max(inscr_prise_en_compte, axis=2)
inscr_tag[inscr_tag < 0] = np.NaN # fix les max non calculés (-1) -> Na?
# Le dataFrame des notes moyennes
etud_moy_tag = etud_moy_tag * inscr_tag
etud_moy_tag_df = pd.DataFrame(
etud_moy_tag,
index=self.etudids_sorted, # les etudids
columns=self.competences_sorted, # les competences
)
# Le dataFrame des coeffs
coeff_tag = coeff_tag * inscr_tag # Réapplique le masque des inscriptions
coeffs_df = pd.DataFrame(
coeff_tag, index=self.etudids_sorted, columns=self.competences_sorted
)
return etud_moy_tag_df, coeffs_df