diff --git a/app/pe/moys/pe_rcstag.py b/app/pe/moys/pe_rcstag.py index d08dc9b5..22d806cb 100644 --- a/app/pe/moys/pe_rcstag.py +++ b/app/pe/moys/pe_rcstag.py @@ -46,9 +46,12 @@ import app.pe.pe_comp as pe_comp from app.pe.moys import pe_tabletags, pe_moytag -class RCSTag(pe_tabletags.TableTag): +class RCSemXTag(pe_tabletags.TableTag): def __init__( - self, rcsemx: pe_rcsemx.RCSemX, sxstags: dict[(str, int) : pe_sxtag.SxTag] + 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 @@ -59,14 +62,19 @@ class RCSTag(pe_tabletags.TableTag): 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 RCSTag (identique au RCSemX sur lequel il s'appuie)""" + """Identifiant du RCSemXTag (identique au RCSemX sur lequel il s'appuie)""" self.rcsemx: pe_rcsemx.RCSemX = rcsemx - """RCSemX associé au RCSTag""" + """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""" @@ -80,20 +88,21 @@ class RCSTag(pe_tabletags.TableTag): # Affichage pour debug pe_affichage.pe_print(f"*** {self.get_repr(verbose=True)}") - # Les données aggrégés (RCRCF + SxTags + # 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 = {} + self.sxstags_aggreges = {} """Les SxTag associés aux SemX aggrégés""" try: for rcf_id in self.semXs_aggreges: - self.sxstags[rcf_id] = sxstags[rcf_id] + 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 = pe_rcs.TYPES_RCS[self.rcs_id[0]]["aggregat"] - sxtag_final = self.sxstags[(sems_dans_aggregat[-1], self.rcs_id[1])] + 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) @@ -123,28 +132,48 @@ class RCSTag(pe_tabletags.TableTag): """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}") + + # Traitement des inscriptions aux semX(tags) + # ****************************************** + # Cube d'inscription (etudids_sorted x compétences_sorted x sxstags) + # indiquant quel sxtag est valide pour chaque étudiant + inscriptions_df, inscriptions_cube = self.compute_inscriptions_comps_cube( + tag, self.etudids_sorted, self.competences_sorted, self.sxstags_aggreges + ) + + # Traitement des notes + # ******************** # 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 + tag, self.etudids_sorted, self.competences_sorted, self.sxstags_aggreges ) - # Calcule des moyennes/coeffs sous forme d'un dataframe""" + # Calcule les moyennes sous forme d'un dataframe en les "aggrégant" + # compétence par compétence moys_competences = compute_notes_competences( - notes_cube, self.etudids_sorted, self.competences_sorted + notes_cube, + inscriptions_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) + + # Traitement des coeffs pour la moyenne générale + # *********************************************** + # Df des coeffs sur tous les SxTags aggrégés coeffs_df, coeffs_cube = self.compute_coeffs_comps_cube( tag, self.etudids_sorted, self.competences_sorted, - self.sxstags, + self.sxstags_aggreges, ) - # Calcule la synthèse des coefficients à prendre en compte pour la moyenne - # générale + # 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 + coeffs_cube, + inscriptions_cube, + notes_cube, + self.etudids_sorted, + self.competences_sorted, ) - self.__aff_profil_coeffs(matrice_coeffs_moy_gen) + pe_affichage.aff_profil_coeffs(matrice_coeffs_moy_gen) # Mémorise les moyennes et les coeff associés self.moyennes_tags[tag] = pe_moytag.MoyennesTag( @@ -162,9 +191,11 @@ class RCSTag(pe_tabletags.TableTag): """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle est basée)""" if verbose: - return self.rcsemx.get_repr(verbose=verbose) + return f"{self.__class__.__name__} basé sur " + self.rcsemx.get_repr( + verbose=verbose + ) else: - return f"{self.__class__.__name__} ({self.rcs_id})" + return f"{self.__class__.__name__} {self.rcs_id}" def compute_notes_comps_cube( self, @@ -287,6 +318,57 @@ class RCSTag(pe_tabletags.TableTag): return coeffs_dfs, coeffs_etudids_x_comps_x_sxtag + def compute_inscriptions_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 etudid x competences x SxTag traduisant quels sxtags est à prendre + en compte pour chaque étudiant. + Contient des 0 et des 1 pour indiquer la prise en compte. + + Args: + tag: Le tag visé + etudids_sorted: Les etudis triés + competences_sorted: Les compétences triées + sxstags: Les SxTag à réunir + """ + # Initialisation + inscriptions_dfs = {} + + for sxtag_id, sxtag in sxstags.items(): + # Partant d'un dataframe vierge + inscription_df = pd.DataFrame( + 0, index=etudids_sorted, columns=competences_sorted + ) + # Stocke les dfs + inscriptions_dfs[sxtag_id] = inscription_df + + for etudid in etudids_sorted: + for sem in self.rcsemx.aggregat: + if etudid in self.semXs_suivis: + semx_suivi = self.semXs_suivis[etudid][sem] + if semx_suivi: + semx_suivi_id = semx_suivi.rcs_id + if semx_suivi_id not in self.sxtags_connus: + pe_affichage.pe_print( + f"Un SxTag est manquant : {semx_suivi_id}" + ) + if semx_suivi_id in inscriptions_dfs: + # Si le sxtag est l'un des siens + inscriptions_dfs[semx_suivi_id].loc[etudid, :] = 1 + + """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 sxstags] + inscriptions_etudids_x_comps_x_sxtag = np.stack( + sxtag_x_etudids_x_comps, axis=-1 + ) + + return inscriptions_dfs, inscriptions_etudids_x_comps_x_sxtag + def _do_taglist(self) -> list[str]: """Synthétise les tags à partir des Sxtags aggrégés. @@ -294,8 +376,8 @@ class RCSTag(pe_tabletags.TableTag): Liste de tags triés par ordre alphabétique """ tags = [] - for frmsem_id in self.sxstags: - tags.extend(self.sxstags[frmsem_id].tags_sorted) + 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]: @@ -307,7 +389,7 @@ class RCSTag(pe_tabletags.TableTag): Un dictionnaire {'acronyme_ue' : 'compétences'} """ dict_competences = {} - for sxtag_id, sxtag in self.sxstags.items(): + for sxtag_id, sxtag in self.sxstags_aggreges.items(): dict_competences |= sxtag.acronymes_ues_to_competences return dict_competences @@ -324,60 +406,43 @@ class RCSTag(pe_tabletags.TableTag): pe_affichage.pe_print(f"--> Compétences :") pe_affichage.pe_print("\n".join(aff_comp)) - def __aff_profil_coeffs(self, matrice_coeffs_moy_gen): - """Extrait de la matrice des coeffs, les différents types d'inscription - et de coefficients (appelés profil) des étudiants et les affiche - (pour debug) - """ - - # Les profils des coeffs d'UE (pour debug) - profils = [] - for i in matrice_coeffs_moy_gen.index: - val = matrice_coeffs_moy_gen.loc[i].fillna("-") - val = " | ".join([str(v) for v in val]) - if val not in profils: - profils += [val] - - # L'affichage - if len(profils) > 1: - profils_aff = "\n" + "\n".join([" " * 10 + prof for prof in profils]) - else: - profils_aff = "\n".join(profils) - pe_affichage.pe_print( - f" > Moyenne calculée avec pour coeffs (de compétences) : {profils_aff}" - ) - def compute_coeffs_competences( coeff_cube: np.array, + inscriptions: 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. + confondues), en fonction des inscriptions. 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 + inscriptions: inscriptions aux UES|Compétences ndarray + (etuds x UEs|compétences x sxtags), des 0 ou des 1 + set_cube: les notes etudids_sorted: liste des étudiants (dim. 0 du cube) - competences_sorted: list + competences_sorted: list (dim. 1 du cube) Returns: Un DataFrame de coefficients (etudids_sorted x compétences_sorted) """ - nb_etuds, nb_comps, nb_semestres = set_cube.shape + nb_etuds, nb_comps, nb_semestres = inscriptions.shape assert nb_etuds == len(etudids_sorted) assert nb_comps == len(competences_sorted) + # Applique le masque des inscriptions aux coeffs et aux notes + coeffs_significatifs = coeff_cube * inscriptions + set_cube_significatif = set_cube * inscriptions + # Quelles entrées du cube contiennent des notes ? - mask = ~np.isnan(set_cube) + mask = ~np.isnan(set_cube_significatif) # 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) + coeffs_cube_no_nan = np.nan_to_num(coeffs_significatifs, nan=0.0) - # Retire les coefficients associées à des données sans notes + # Retire les coefficients associés à des données sans notes coeffs_cube_no_nan = coeffs_cube_no_nan * mask # Somme les coefficients (correspondant à des notes) @@ -395,6 +460,7 @@ def compute_coeffs_competences( def compute_notes_competences( set_cube: np.array, + inscriptions: np.array, etudids_sorted: list, competences_sorted: list, ): @@ -406,11 +472,12 @@ def compute_notes_competences( par aggrégat de plusieurs semestres. Args: - set_cube: notes moyennes aux modules ndarray + set_cube: notes moyennes aux compétences ndarray (etuds x UEs|compétences x sxtags), des floats avec des NaN + inscriptions: inscrptions aux compétences ndarray + (etuds x UEs|compétences x sxtags), des 0 et des 1 etudids_sorted: liste des étudiants (dim. 0 du cube) - competences_sorted: list - tags: liste des tags (dim. 1 du cube) + competences_sorted: list (dim. 1 du cube) Returns: Un DataFrame avec pour columns les moyennes par tags, et pour rows les etudid @@ -419,11 +486,14 @@ def compute_notes_competences( assert nb_etuds == len(etudids_sorted) assert nb_comps == len(competences_sorted) + # Applique le masque d'inscriptions + set_cube_significatif = set_cube * inscriptions + # Quelles entrées du cube contiennent des notes ? - mask = ~np.isnan(set_cube) + mask = ~np.isnan(set_cube_significatif) # 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) + set_cube_no_nan = np.nan_to_num(set_cube_significatif, nan=0.0) # Les moyennes par tag with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) diff --git a/app/pe/pe_affichage.py b/app/pe/pe_affichage.py index b719e5f7..79dfaabf 100644 --- a/app/pe/pe_affichage.py +++ b/app/pe/pe_affichage.py @@ -9,7 +9,7 @@ from flask import g from app import log -PE_DEBUG = False +PE_DEBUG = True # On stocke les logs PE dans g.scodoc_pe_log @@ -41,3 +41,41 @@ def pe_get_log() -> str: # Affichage dans le tableur pe en cas d'absence de notes SANS_NOTE = "-" + + +def aff_profil_coeffs(matrice_coeffs_moy_gen, with_index=False): + """Affiche les différents types de coefficients (appelés profil) + d'une matrice_coeffs_moy_gen (pour debug) + """ + + # Les profils des coeffs d'UE (pour debug) + profils = [] + index_a_profils = {} + for i in matrice_coeffs_moy_gen.index: + val = matrice_coeffs_moy_gen.loc[i].fillna("-") + val = " | ".join([str(v) for v in val]) + if val not in profils: + profils += [val] + index_a_profils[val] = [str(i)] + else: + index_a_profils[val] += [str(i)] + + # L'affichage + if len(profils) > 1: + if with_index: + elmts = [ + " " * 10 + + prof + + " (par ex. " + + ", ".join(index_a_profils[prof][:10]) + + ")" + for prof in profils + ] + else: + elmts = [" " * 10 + prof for prof in profils] + profils_aff = "\n" + "\n".join(elmts) + else: + profils_aff = "\n".join(profils) + pe_print( + f" > Moyenne calculée avec pour coeffs (de compétences) : {profils_aff}" + ) diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index f1539a96..c2082517 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -306,9 +306,11 @@ class JuryPE(object): ) pe_affichage.pe_print("1) Calcul des moyennes des RCSTag") - self.rcss_tags = {} + self.rcsstags = {} for rcs_id, rcsemx in self.rcss_jury.rcsemxs.items(): - self.rcss_tags[rcs_id] = pe_rcstag.RCSTag(rcsemx, self.sxtags) + self.rcsstags[rcs_id] = pe_rcstag.RCSemXTag( + rcsemx, self.sxtags, self.rcss_jury.semXs_suivis + ) # Intègre le bilan des trajectoires tagguées au zip final pe_affichage.pe_print("2) Bilan") @@ -317,7 +319,7 @@ class JuryPE(object): output, engine="openpyxl" ) as writer: onglets = [] - for rcs_tag in self.rcss_tags.values(): + for rcs_tag in self.rcsstags.values(): onglet = rcs_tag.get_repr(verbose=False) if rcs_tag.is_significatif(): df = rcs_tag.to_df() @@ -374,7 +376,7 @@ class JuryPE(object): pe_moytag.CODE_MOY_COMPETENCES, etudiants_diplomes, self.rcss_jury.rcsemxs, - self.rcss_tags, + self.rcsstags, self.rcss_jury.rcsemxs_suivis, ) self.interclasstags[pe_moytag.CODE_MOY_COMPETENCES][nom_rcs] = interclass diff --git a/app/pe/rcss/pe_rcs.py b/app/pe/rcss/pe_rcs.py index f9babeee..435e857d 100644 --- a/app/pe/rcss/pe_rcs.py +++ b/app/pe/rcss/pe_rcs.py @@ -87,18 +87,21 @@ class RCS: tous se terminant par un (form)semestre final. """ - def __init__(self, nom_rcs: str, semestre_final: FormSemestre): - self.nom: str = nom_rcs + def __init__(self, nom: str, semestre_final: FormSemestre): + self.nom: str = nom """Nom du RCS""" assert self.nom in TOUS_LES_RCS, "Le nom d'un RCS doit être un aggrégat" + self.aggregat: list[str] = TYPES_RCS[nom]["aggregat"] + """Aggrégat (liste des nom des semestres aggrégés)""" + self.formsemestre_final: FormSemestre = semestre_final """(Form)Semestre final du RCS""" self.rang_final = self.formsemestre_final.semestre_id """Rang du formsemestre final""" - self.rcs_id: (str, int) = (nom_rcs, semestre_final.formsemestre_id) + self.rcs_id: (str, int) = (nom, semestre_final.formsemestre_id) """Identifiant du RCS sous forme (nom_rcs, id du semestre_terminal)""" self.fid_final: int = self.formsemestre_final.formsemestre_id diff --git a/app/pe/rcss/pe_rcsemx.py b/app/pe/rcss/pe_rcsemx.py index 94d3db5d..e36596fb 100644 --- a/app/pe/rcss/pe_rcsemx.py +++ b/app/pe/rcss/pe_rcsemx.py @@ -26,12 +26,12 @@ class RCSemX(pe_rcs.RCS): incluant des infos sur les redoublements). Args: - nom_rcs: Un nom du RCS (par ex: '5S') + nom: Un nom du RCS (par ex: '5S') semestre_final: Le semestre final du RCS """ - def __init__(self, nom_rcs: str, semestre_final: FormSemestre): - pe_rcs.RCS.__init__(self, nom_rcs, semestre_final) + def __init__(self, nom: str, semestre_final: FormSemestre): + pe_rcs.RCS.__init__(self, nom, semestre_final) self.semXs_aggreges: dict[(str, int) : pe_sxtag.SxTag] = {} """Les semX à aggréger""" diff --git a/app/pe/rcss/pe_trajectoires.py b/app/pe/rcss/pe_trajectoires.py index ed13b17f..4e1a1d1e 100644 --- a/app/pe/rcss/pe_trajectoires.py +++ b/app/pe/rcss/pe_trajectoires.py @@ -25,12 +25,12 @@ class Trajectoire(pe_rcs.RCS): * des S1+S2+(année de césure)+S3 si césure, ... Args: - nom_rcs: Un nom du RCS (par ex: '5S') + nom: Un nom du RCS (par ex: '5S') semestre_final: Le formsemestre final du RCS """ - def __init__(self, nom_rcs: str, semestre_final: FormSemestre): - pe_rcs.RCS.__init__(self, nom_rcs, semestre_final) + def __init__(self, nom: str, semestre_final: FormSemestre): + pe_rcs.RCS.__init__(self, nom, semestre_final) self.semestres_aggreges: dict[int:FormSemestre] = {} """Formsemestres regroupés dans le RCS"""