ScoDoc/app/pe/pe_rcstag.py

393 lines
15 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 FormSemestre
from app.pe import pe_affichage
import pandas as pd
import numpy as np
import app.pe.rcss.pe_rcs as pe_rcs
import app.pe.rcss.pe_rcrcf as pe_rcrcf
import app.pe.pe_sxtag as pe_sxtag
import app.pe.pe_comp as pe_comp
from app.pe.pe_tabletags import TableTag
from app.pe.pe_moytag import MoyennesTag
class RCSTag(TableTag):
def __init__(self, rcrcf: pe_rcs.RCS, sxstags: dict[(str, int) : pe_sxtag.SxTag]):
"""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:
rcs: Un RCS (identifié par un nom et l'id de son semestre terminal)
sxstags: Les données sur les RCF taggués
"""
TableTag.__init__(self)
self.rcs_id: tuple(str, int) = rcrcf.rcs_id
"""Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)"""
self.rcrcf: pe_rcrcf.RCRCF = rcrcf
"""RCRCF associé au RCS taggué"""
self.nom = self.get_repr()
"""Représentation textuelle du RCS taggué"""
# Les données du semestre final
self.formsemestre_terminal: FormSemestre = rcrcf.formsemestre_final
"""Le semestre final"""
self.fid_final: int = rcrcf.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.rcfs_aggreges = rcrcf.rcfs_aggreges
"""Les RCFs aggrégés"""
self.sxstags = {}
"""Les SxTag associés aux RCF aggrégés"""
try:
for rcf_id in self.rcfs_aggreges:
self.sxstags[rcf_id] = sxstags[rcf_id]
except:
raise ValueError("Semestres SxTag manquants")
# Les étudiants (etuds, états civils & etudis)
sxtag_final = self.sxstags[self.rcs_id]
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)"""
pe_affichage.pe_print(
f"* Association UEs -> compétences : {self.acronymes_ues_to_competences}"
)
self.competences_sorted = sorted(self.acronymes_ues_to_competences.values())
"""Compétences (triées par nom, extraites des SxTag aggrégés)"""
pe_affichage.pe_print(f"* Compétences : {', '.join(self.competences_sorted)}")
# Les tags
self.tags_sorted = self._do_taglist()
"""Tags extraits de tous les SxTag aggrégés"""
pe_affichage.pe_print(f"* Tags : {', '.join(self.tags_sorted)}")
# Les moyennes
self.moyennes_tags: dict[str, MoyennesTag] = {}
"""Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)"""
for tag in self.tags_sorted:
# Cube de notes (etudids_sorted x compétences_sorted x sxstags)
notes_df, notes_cube = self.compute_notes_comps_cube(
tag, self.etudids_sorted, self.competences_sorted, self.sxstags
)
# Calcule des moyennes/coeffs sous forme d'un dataframe"""
moys_competences = compute_notes_competences(
notes_cube, self.etudids_sorted, self.competences_sorted
)
# Cube de coeffs pour la moyenne générale,
# traduisant les inscriptions des étudiants aux UEs (etudids_sorted x compétences_sorted x sxstags)
coeffs_df, coeffs_cube = self.compute_coeffs_comps_cube(
tag,
self.etudids_sorted,
self.competences_sorted,
self.sxstags,
)
# Calcule la synthèse des coefficients à prendre en compte pour la moyenne
# générale
matrice_coeffs_moy_gen = compute_coeffs_competences(
coeffs_cube, notes_cube, self.etudids_sorted, self.competences_sorted
)
# Mémorise les moyennes et les coeff associés
self.moyennes_tags[tag] = MoyennesTag(
tag, 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 self.rcrcf.get_repr(verbose=verbose)
else:
return f"{self.__class__.__name__} ({self.rcs_id})"
def compute_notes_comps_cube(
self,
tag,
etudids_sorted: list[int],
competences_sorted: list[str],
sxstags: dict[(str, int) : pe_sxtag.SxTag],
):
"""Pour un tag donné, construit le cube de notes (etudid x competences x SxTag)
nécessaire au calcul des moyennes,
en remplaçant les données d'UE (obtenus du SxTag) par les compétences
Args:
tag: Le tag visé
etudids_sorted: Les etudis triés (dim 0)
competences_sorted: Les compétences triées (dim 1)
sxstags: Les SxTag à réunir
"""
notes_dfs = {}
for sxtag_id, sxtag in sxstags.items():
# Partant d'un dataframe vierge
notes_df = pd.DataFrame(
np.nan, index=etudids_sorted, columns=competences_sorted
)
# Charge les notes du semestre tag (copie car changement de nom de colonnes à venir)
moys_tag = sxtag.moyennes_tags[tag]
notes = moys_tag.matrice_notes.copy() # avec une copie
# Traduction des acronymes d'UE en compétences
acronymes_ues_columns = notes.columns
acronymes_to_comps = [
self.acronymes_ues_to_competences[acro]
for acro in acronymes_ues_columns
]
notes.columns = acronymes_to_comps
# Les étudiants et les compétences communes
etudids_communs, comp_communes = pe_comp.find_index_and_columns_communs(
notes_df, notes
)
# Recopie des notes et des coeffs
notes_df.loc[etudids_communs, comp_communes] = notes.loc[
etudids_communs, comp_communes
]
# 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
notes_dfs[sxtag_id] = notes_df
"""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 sxstags]
notes_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1)
return notes_dfs, notes_etudids_x_comps_x_sxtag
def compute_coeffs_comps_cube(
self,
tag,
etudids_sorted: list[int],
competences_sorted: list[str],
sxstags: dict[(str, int) : pe_sxtag.SxTag],
):
"""Pour un tag donné, construit
le cube de coeffs (etudid x competences x SxTag) (traduisant les inscriptions
des étudiants aux UEs en fonction de leur parcours)
qui s'applique aux différents SxTag
en remplaçant les données d'UE (obtenus du SxTag) par les compétences
Args:
tag: Le tag visé
etudids_sorted: Les etudis triés
competences_sorted: Les compétences triées
sxstags: Les SxTag à réunir
"""
coeffs_dfs = {}
for sxtag_id, sxtag in sxstags.items():
# Partant d'un dataframe vierge
coeffs_df = pd.DataFrame(
np.nan, index=etudids_sorted, columns=competences_sorted
)
moys_tag = sxtag.moyennes_tags[tag]
# Charge les notes et les coeffs du semestre tag
coeffs = moys_tag.matrice_coeffs_moy_gen.copy() # les coeffs
# Traduction des acronymes d'UE en compétences
acronymes_ues_columns = coeffs.columns
acronymes_to_comps = [
self.acronymes_ues_to_competences[acro]
for acro in acronymes_ues_columns
]
coeffs.columns = acronymes_to_comps
# Les étudiants et les compétences communes
etudids_communs, comp_communes = pe_comp.find_index_and_columns_communs(
coeffs_df, coeffs
)
# Recopie des notes et des coeffs
coeffs_df.loc[etudids_communs, comp_communes] = coeffs.loc[
etudids_communs, comp_communes
]
# Stocke les dfs
coeffs_dfs[sxtag_id] = coeffs_df
"""Réunit les coeffs sous forme d'un cube etudids x competences x semestres"""
sxtag_x_etudids_x_comps = [coeffs_dfs[sxtag_id] for sxtag_id in sxstags]
coeffs_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1)
return coeffs_dfs, coeffs_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:
tags.extend(self.sxstags[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.items():
dict_competences |= sxtag.acronymes_ues_to_competences
return dict_competences
def compute_coeffs_competences(
coeff_cube: np.array,
set_cube: np.array,
etudids_sorted: list,
competences_sorted: list,
):
"""Calcule les coeffs à utiliser pour la moyenne générale (toutes compétences
confondues), en fonction des notes (set_cube) aggrégées.
Args:
coeffs_cube: coeffs impliqués dans la moyenne générale (semestres par semestres)
set_cube: notes moyennes aux modules ndarray
(etuds x UEs|compétences x sxtags), des floats avec des NaN
etudids_sorted: liste des étudiants (dim. 0 du cube)
competences_sorted: list
Returns:
Un DataFrame de coefficients (etudids_sorted x compétences_sorted)
"""
nb_etuds, nb_comps, nb_semestres = set_cube.shape
assert nb_etuds == len(etudids_sorted)
assert nb_comps == len(competences_sorted)
# Quelles entrées du cube contiennent des notes ?
mask = ~np.isnan(set_cube)
# Enlève les NaN du cube de notes pour les entrées manquantes
coeffs_cube_no_nan = np.nan_to_num(coeff_cube, nan=0.0)
# Retire les coefficients associées à des données sans notes
coeffs_cube_no_nan = coeffs_cube_no_nan * mask
# Somme les coefficients (correspondant à des notes)
coeff_tag = np.sum(coeffs_cube_no_nan, axis=2)
# Le dataFrame des coeffs
coeffs_df = pd.DataFrame(
coeff_tag, index=etudids_sorted, columns=competences_sorted
)
# Remet à Nan les coeffs à 0
coeffs_df.fillna(np.nan)
return coeffs_df
def compute_notes_competences(
set_cube: np.array,
etudids_sorted: list,
competences_sorted: list,
):
"""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:
set_cube: notes moyennes aux modules ndarray
(etuds x UEs|compétences x sxtags), des floats avec des NaN
etudids_sorted: liste des étudiants (dim. 0 du cube)
competences_sorted: list
tags: liste des tags (dim. 1 du cube)
Returns:
Un DataFrame avec pour columns les moyennes par tags,
et pour rows les etudid
"""
nb_etuds, nb_comps, nb_semestres = set_cube.shape
assert nb_etuds == len(etudids_sorted)
assert nb_comps == len(competences_sorted)
# Quelles entrées du cube contiennent des notes ?
mask = ~np.isnan(set_cube)
# Enlève les NaN du cube de notes pour les entrées manquantes
set_cube_no_nan = np.nan_to_num(set_cube, nan=0.0)
# Les moyennes par tag
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_tag = np.sum(set_cube_no_nan, axis=2) / np.sum(mask, axis=2)
# Le dataFrame des notes moyennes
etud_moy_tag_df = pd.DataFrame(
etud_moy_tag,
index=etudids_sorted, # les etudids
columns=competences_sorted, # les competences
)
etud_moy_tag_df.fillna(np.nan)
return etud_moy_tag_df