From 746314b2fbfd7cd8975e4d8311e4c7f5af4358c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Wed, 21 Feb 2024 20:02:38 +0100 Subject: [PATCH] =?UTF-8?q?Etat=20interm=C3=A9diaire=20sur=20les=20intercl?= =?UTF-8?q?assements=20et=20la=20synth=C3=A8se=20du=20jury=20(donn=C3=A9es?= =?UTF-8?q?=20sans=20notes=20ou=20tags=20=C3=A0=20revoir)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/moys/__init__.py | 0 app/pe/moys/pe_interclasstag.py | 325 +++++++++++++++++++++ app/pe/{pe_moytag.py => moys/pe_moy.py} | 108 +------ app/pe/moys/pe_moytag.py | 155 ++++++++++ app/pe/{ => moys}/pe_rcstag.py | 20 +- app/pe/{ => moys}/pe_ressemtag.py | 27 +- app/pe/{ => moys}/pe_sxtag.py | 16 +- app/pe/moys/pe_tabletags.py | 204 +++++++++++++ app/pe/pe_interclasstag.py | 160 ---------- app/pe/pe_jury.py | 372 +++++++++++------------- app/pe/pe_tabletags.py | 122 -------- app/pe/rcss/pe_rcsemx.py | 2 +- 12 files changed, 897 insertions(+), 614 deletions(-) create mode 100644 app/pe/moys/__init__.py create mode 100644 app/pe/moys/pe_interclasstag.py rename app/pe/{pe_moytag.py => moys/pe_moy.py} (58%) create mode 100644 app/pe/moys/pe_moytag.py rename app/pe/{ => moys}/pe_rcstag.py (97%) rename app/pe/{ => moys}/pe_ressemtag.py (95%) rename app/pe/{ => moys}/pe_sxtag.py (97%) create mode 100644 app/pe/moys/pe_tabletags.py delete mode 100644 app/pe/pe_interclasstag.py delete mode 100644 app/pe/pe_tabletags.py diff --git a/app/pe/moys/__init__.py b/app/pe/moys/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/pe/moys/pe_interclasstag.py b/app/pe/moys/pe_interclasstag.py new file mode 100644 index 000000000..92114f41d --- /dev/null +++ b/app/pe/moys/pe_interclasstag.py @@ -0,0 +1,325 @@ +############################################################################## +# +# 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 Thu Sep 8 09:36:33 2016 + +@author: barasc +""" + +import pandas as pd +import numpy as np + +from app.models import Identite +from app.pe.moys import pe_tabletags, pe_moy, pe_moytag, pe_sxtag +from app.pe.rcss import pe_rcs +import app.pe.pe_comp as pe_comp + + +class InterClassTag(pe_tabletags.TableTag): + """ + Interclasse l'ensemble des étudiants diplômés à une année + donnée (celle du jury), pour un RCS donné (par ex: 'S2', '3S'), qu'il soit + de type SemX ou RCSemX, + en reportant les moyennes obtenues sur à la version tagguée + du RCS (de type SxTag ou RCSTag). + Sont ensuite calculés les classements (uniquement) + sur les étudiants diplômes. + + Args: + nom_rcs: Le nom de l'aggrégat + type_interclassement: Le type d'interclassement (par UE ou par compétences) + etudiants_diplomes: L'identité des étudiants diplômés + rcss: Un dictionnaire {(nom_rcs, fid_final): RCS} donnant soit + les SemX soit les RCSemX recencés par le jury PE + rcstag: Un dictionnaire {(nom_rcs, fid_final): RCSTag} donnant + soit les SxTag (associés aux SemX) + soit les RCSTags (associés au RCSemX) calculés par le jury PE + suivis: Un dictionnaire associé à chaque étudiant son rcss + (de la forme ``{etudid: {nom_rcs: RCS_suivi}}``) + """ + + def __init__( + self, + nom_rcs: str, + type_interclassement: str, + etudiants_diplomes: dict[int, Identite], + rcss: dict[(str, int) : pe_rcs.RCS], + rcstags: dict[(str, int) : pe_tabletags.TableTag], + suivis: dict[int:dict], + ): + pe_tabletags.TableTag.__init__(self) + + self.nom_rcs: str = nom_rcs + """Le nom du RCS interclassé""" + + # Le type d'interclassement + self.type = type_interclassement + + # Les informations sur les étudiants diplômés + self.etuds: list[Identite] = list(etudiants_diplomes.values()) + """Identités des étudiants diplômés""" + self.add_etuds(self.etuds) + + self.diplomes_ids = set(etudiants_diplomes.keys()) + """Etudids des étudiants diplômés""" + + # Les RCS de l'aggrégat (SemX ou RCSemX) + self.rcss: dict[(str, int), pe_rcs.RCS] = {} + """Ensemble des SemX ou des RCSemX associés à l'aggrégat""" + for (nom, fid), rcs in rcss.items(): + if nom == nom_rcs: + self.rcss[(nom, fid)] = rcss + + # Les données tagguées + self.rcstags: dict[(str, int), pe_tabletags.TableTag] = {} + """Ensemble des SxTag ou des RCSTags associés à l'aggrégat""" + for rcs_id in self.rcss: + self.rcstags[rcs_id] = rcstags[rcs_id] + + # Les RCS (SemX ou RCSemX) suivis par les étudiants du jury, + # en ne gardant que ceux associés aux diplomés + self.suivis: dict[int, pe_rcs.RCS] = {} + """Association entre chaque étudiant et le SxTag ou RCSTag à prendre + pour l'aggrégat""" + for etudid in self.diplomes_ids: + self.suivis[etudid] = suivis[etudid][nom_rcs] + + # Les données sur les tags + self.tags_sorted = self._do_taglist() + """Liste des tags (triés par ordre alphabétique)""" + + # Les données sur les UEs (si SxTag) ou compétences (si RCSTag) + self.champs_sorted = self._do_ues_ou_competences_list() + + # Construit la matrice de notes + etudids_sorted = sorted(list(self.diplomes_ids)) + + self.nom = self.get_repr() + """Représentation textuelle de l'interclassement""" + + # Synthétise les moyennes/classements par tag + self.moyennes_tags: dict[str, pe_moytag.MoyennesTag] = {} + for tag in self.tags_sorted: + notes = self.compute_notes_matrice(tag, etudids_sorted, self.champs_sorted) + coeffs = self.compute_coeffs_matrice( + tag, etudids_sorted, self.champs_sorted + ) + self.moyennes_tags[tag] = pe_moytag.MoyennesTag( + tag, + self.type, + notes, + coeffs, # limite les moyennes aux étudiants de la promo + ) + + def get_repr(self) -> str: + """Une représentation textuelle""" + return f"{self.nom_rcs} par {self.type}" + + def _do_taglist(self): + """Synthétise les tags à partir des TableTags (SXTag ou RCSTag) + + Returns: + Une liste de tags triés par ordre alphabétique + """ + tags = [] + for rcstag in self.rcstags.values(): + tags.extend(rcstag.tags_sorted) + return sorted(set(tags)) + + def compute_notes_matrice( + self, tag, etudids_sorted: list[int], champs_sorted: list[str] + ) -> pd.DataFrame: + """Construit la matrice de notes (etudids x champs) en + reportant les moyennes obtenues par les étudiants + aux semestres de l'aggrégat pour le tag visé. + + Les champs peuvent être des acronymes d'UEs ou des compétences. + + Args: + etudids_sorted: Les etudids des étudiants (diplômés) triés + champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice + Return: + Le dataFrame (etudids x champs) + reportant les moyennes des étudiants aux champs + """ + + # Partant d'un dataframe vierge + df = pd.DataFrame(np.nan, index=etudids_sorted, columns=champs_sorted) + + for rcstag in self.rcstags.values(): + # Charge les moyennes au tag d'un RCStag + if tag in rcstag.moyennes_tags: + moytag: pd.DataFrame = rcstag.moyennes_tags[tag].matrice_notes + + # Etudiants/Champs communs entre le RCSTag et les données interclassées + ( + etudids_communs, + champs_communs, + ) = pe_comp.find_index_and_columns_communs(df, moytag) + + # Injecte les notes par tag + df.loc[etudids_communs, champs_communs] = moytag.loc[ + etudids_communs, champs_communs + ] + + return df + + def compute_coeffs_matrice( + self, tag, etudids_sorted: list[int], champs_sorted: list[str] + ) -> pd.DataFrame: + """Idem que compute_notes_matrices mais pour les coeffs + + Args: + etudids_sorted: Les etudids des étudiants (diplômés) triés + champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice + Return: + Le dataFrame (etudids x champs) + reportant les moyennes des étudiants aux champs + """ + + # Partant d'un dataframe vierge + df = pd.DataFrame(np.nan, index=etudids_sorted, columns=champs_sorted) + + for rcstag in self.rcstags.values(): + if tag in rcstag.moyennes_tags: + # Charge les coeffs au tag d'un RCStag + coeffs: pd.DataFrame = rcstag.moyennes_tags[tag].matrice_notes + + # Etudiants/Champs communs entre le RCSTag et les données interclassées + ( + etudids_communs, + champs_communs, + ) = pe_comp.find_index_and_columns_communs(df, coeffs) + + # Injecte les coeffs par tag + df.loc[etudids_communs, champs_communs] = coeffs.loc[ + etudids_communs, champs_communs + ] + + return df + + def _do_ues_ou_competences_list(self) -> list[str]: + """Synthétise les champs (UEs ou compétences) sur lesquels + sont calculés les moyennes. + + Returns: + Un dictionnaire {'acronyme_ue' : 'compétences'} + """ + dict_champs = [] + for rcstag in self.rcstags.values(): + if isinstance(rcstag, pe_sxtag.SxTag): + champs = rcstag.acronymes_sorted + else: # pe_rcstag.RCSTag + champs = rcstag.competences_sorted + dict_champs.extend(champs) + return sorted(set(dict_champs)) + + def has_tags(self): + """Indique si l'interclassement a des tags (cas d'un + interclassement sur un S5 qui n'a pas eu lieu) + """ + return len(self.tags_sorted) > 0 + + def _un_rcstag_significatif(self, rcsstags: dict[(str, int):pe_tabletags]): + """Renvoie un rcstag significatif (ayant des tags et des notes aux tags) + parmi le dictionnaire de rcsstags""" + for rcstag_id, rcstag in rcsstags.items(): + moystags: pe_moytag.MoyennesTag = rcstag.moyennes_tags + for tag, moystag in moystags.items(): + tags_tries = moystag.get_all_significant_tags() + if tags_tries: + return moystag + return None + + def compute_df_synthese_moyennes_tag( + self, tag, aggregat=None, type_colonnes=False + ) -> pd.DataFrame: + """Construit le dataframe retraçant pour les données des moyennes + pour affichage dans la synthèse du jury PE. (cf. to_df()) + + Args: + etudids_sorted: Les etudids des étudiants (diplômés) triés + champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice + Return: + Le dataFrame (etudids x champs) + reportant les moyennes des étudiants aux champs + """ + if aggregat: + assert ( + aggregat == self.nom_rcs + ), "L'interclassement ciblé ne correspond pas à l'aggrégat visé" + + etudids_sorted = sorted(list(self.diplomes_ids)) + + if self.rcstags: + return None + + # Un rcstag significatif pour initier les colonnes + moytag = self._un_rcstag_significatif(self.rcstags) + if not moytag: + return None + df_moytag = moytag.to_df( + aggregat=aggregat, + cohorte="Groupe", + ) + colonnes = list(df_moytag.columns) + + # Partant d'un dataframe vierge + df = pd.DataFrame(index=etudids_sorted, columns=colonnes) # colonnes) + for col in colonnes: + if "rang" in col: + df[col] = df[col].astype(str) + df.columns = list(range(len(colonnes))) + + for rcstag in self.rcstags.values(): + # Charge les moyennes au tag d'un RCStag (SemX ou RCSXTag) + if tag in rcstag.moyennes_tags: + # Les infos sur les moyennes du tag + moytag: pe_moytag.MoyennesTag = rcstag.moyennes_tags[tag] + df_moytag = moytag.to_df( + aggregat=aggregat, + cohorte="Groupe", + ) + df_moytag.columns = list(range(len(colonnes))) + + # Etudiants/Champs communs entre le df et les données interclassées + ( + etudids_communs, + champs_communs, # les colonnes de synthèse + ) = pe_comp.find_index_and_columns_communs(df, df_moytag) + + # Injecte les données par tag + df.loc[etudids_communs, champs_communs] = df_moytag.loc[ + etudids_communs, champs_communs + ] + + # Refixe les colonnes + df.columns = colonnes + return df diff --git a/app/pe/pe_moytag.py b/app/pe/moys/pe_moy.py similarity index 58% rename from app/pe/pe_moytag.py rename to app/pe/moys/pe_moy.py index 1029c66cf..2342a7a13 100644 --- a/app/pe/pe_moytag.py +++ b/app/pe/moys/pe_moy.py @@ -1,13 +1,12 @@ import numpy as np import pandas as pd -from app import comp from app.comp.moy_sem import comp_ranks_series from app.pe import pe_affichage class Moyenne: - CRITERES = [ + COLONNES = [ "note", "classement", "rang", @@ -17,6 +16,9 @@ class Moyenne: "nb_etuds", "nb_inscrits", ] + """Colonnes du df""" + + COLONNES_SYNTHESE = ["note", "rang", "min", "max", "moy"] def __init__(self, notes: pd.Series): """Classe centralisant la synthèse des moyennes/classements d'une série @@ -58,7 +60,7 @@ class Moyenne: df = pd.DataFrame( np.nan, index=self.etudids, - columns=Moyenne.CRITERES, + columns=Moyenne.COLONNES, ) # Supprime d'éventuelles chaines de caractères dans les notes @@ -67,14 +69,18 @@ class Moyenne: # Les nb d'étudiants & nb d'inscrits df["nb_etuds"] = len(self.etudids) + df["nb_etuds"] = df["nb_etuds"].astype(int) + # Les étudiants dont la note n'est pas nulle inscrits_ids = notes[notes.notnull()].index.to_list() df.loc[inscrits_ids, "nb_inscrits"] = len(inscrits_ids) + # df["nb_inscrits"] = df["nb_inscrits"].astype(int) # Le classement des inscrits notes_non_nulles = notes[inscrits_ids] (class_str, class_int) = comp_ranks_series(notes_non_nulles) df.loc[inscrits_ids, "classement"] = class_int + # df["classement"] = df["classement"].astype(int) # Le rang (classement/nb_inscrit) df["rang"] = df["rang"].astype(str) @@ -91,6 +97,10 @@ class Moyenne: return df + def get_df_synthese(self): + """Renvoie le df de synthese limité aux colonnes de synthese""" + return self.df[self.COLONNES_SYNTHESE] + def to_dict(self) -> dict: """Renvoie un dictionnaire de synthèse des moyennes/classements/statistiques générale (but)""" synthese = { @@ -103,10 +113,6 @@ class Moyenne: } return synthese - def get_notes(self): - """Série des notes, arrondies à 2 chiffres après la virgule""" - return self.df_gen["note"].round(2) - def get_rangs_inscrits(self) -> pd.Series: """Série des rangs classement/nbre_inscrit""" return self.df_gen["rang"] @@ -151,91 +157,3 @@ class Moyenne: def is_significatif(self) -> bool: """Indique si la moyenne est significative (c'est-à-dire à des notes)""" return self.synthese["nb_inscrits"] > 0 - - -class MoyennesTag: - def __init__( - self, - tag: str, - matrice_notes: pd.DataFrame, # etudids x colonnes - matrice_coeffs: pd.DataFrame, # etudids x colonnes - ): - """Classe centralisant la synthèse des moyennes/classements d'une série - d'étudiants à un tag donné, en différenciant les notes - obtenues aux UE et au général (toutes UEs confondues) - - - Args: - tag: Un tag - matrice_notes: Les moyennes (etudid x acronymes_ues ou etudid x compétences) aux différentes UEs ou compétences - matrice_coeffs: Les coeff à appliquer pour le calcul de la moyenne générale - # notes_gen: Une série de notes (moyenne) sous forme d'un ``pd.Series`` (toutes UEs confondues) - """ - self.tag = tag - """Le tag associé aux moyennes""" - - # Les moyennes par UE - self.matrice_notes: pd.DataFrame = matrice_notes - """Les notes aux UEs ou aux compétences (DataFrame)""" - - self.matrice_coeffs_moy_gen: pd.DataFrame = matrice_coeffs - """Les coeffs à appliquer pour le calcul des moyennes générales - (toutes UE ou compétences confondues). NaN si étudiant non inscrit""" - - self.moyennes: dict[int, pd.DataFrame] = {} - """Les dataframes retraçant les moyennes/classements/statistiques des étudiants aux UEs""" - colonnes = self.matrice_notes.columns - for col in colonnes: # if ue.type != UE_SPORT: - notes = matrice_notes[col] - self.moyennes[col] = Moyenne(notes) - - # Les moyennes générales - notes_gen = pd.Series(np.nan, index=self.matrice_notes.index) - """Les notes générales (moyenne toutes UEs confonudes)""" - if self.has_notes(): - notes_gen = self.compute_moy_gen( - self.matrice_notes, self.matrice_coeffs_moy_gen - ) - self.notes_gen = notes_gen - self.moyenne_gen = Moyenne(notes_gen) - """Le dataframe retraçant les moyennes/classements/statistiques général""" - - def has_notes(self): - """Détermine si les moyennes (aux UEs ou aux compétences) - ont des notes - - Returns: - True si la moytag a des notes, False sinon - """ - notes = self.matrice_notes - nbre_nan = notes.isna().sum().sum() - nbre_notes_potentielles = len(notes.index) * len(notes.columns) - if nbre_nan == nbre_notes_potentielles: - return False - else: - return True - - def compute_moy_gen(self, moys: pd.DataFrame, coeffs: pd.DataFrame) -> pd.Series: - """Calcule la moyenne générale (toutes UE/compétences confondus) - pour le tag considéré, en pondérant les notes obtenues au UE - par les coeff (généralement les crédits ECTS). - - Args: - moys: Les moyennes etudids x acronymes_ues/compétences - coeff: Les coeff etudids x ueids/compétences - """ - - # Calcule la moyenne générale dans le semestre (pondérée par le ECTS) - try: - moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects( - moys, - coeffs.fillna(0.0), - # formation_id=self.formsemestre.formation_id, - skip_empty_ues=True, - ) - except TypeError as e: - raise TypeError( - "Pb dans le calcul de la moyenne toutes UEs/compétences confondues" - ) - - return moy_gen_tag diff --git a/app/pe/moys/pe_moytag.py b/app/pe/moys/pe_moytag.py new file mode 100644 index 000000000..2870be551 --- /dev/null +++ b/app/pe/moys/pe_moytag.py @@ -0,0 +1,155 @@ +import numpy as np +import pandas as pd + +from app import comp +from app.comp.moy_sem import comp_ranks_series +from app.pe.moys import pe_moy + + +CODE_MOY_UE = "UEs" +CODE_MOY_COMPETENCES = "Compétences" +CHAMP_GENERAL = "Général" # Nom du champ de la moyenne générale + + +class MoyennesTag: + def __init__( + self, + tag: str, + type_moyenne: str, + matrice_notes: pd.DataFrame, # etudids x colonnes + matrice_coeffs: pd.DataFrame, # etudids x colonnes + ): + """Classe centralisant la synthèse des moyennes/classements d'une série + d'étudiants à un tag donné, en différenciant les notes + obtenues aux UE et au général (toutes UEs confondues) + + + Args: + tag: Un tag + matrice_notes: Les moyennes (etudid x acronymes_ues ou etudid x compétences) aux différentes UEs ou compétences + matrice_coeffs: Les coeff à appliquer pour le calcul de la moyenne générale + # notes_gen: Une série de notes (moyenne) sous forme d'un ``pd.Series`` (toutes UEs confondues) + """ + self.tag = tag + """Le tag associé aux moyennes""" + + self.type = type_moyenne + """Le type de moyennes (par UEs ou par compétences)""" + + # Les moyennes par UE + self.matrice_notes: pd.DataFrame = matrice_notes + """Les notes aux UEs ou aux compétences (DataFrame)""" + + self.matrice_coeffs_moy_gen: pd.DataFrame = matrice_coeffs + """Les coeffs à appliquer pour le calcul des moyennes générales + (toutes UE ou compétences confondues). NaN si étudiant non inscrit""" + + self.moyennes: dict[int, pd.DataFrame] = {} + """Les dataframes retraçant les moyennes/classements/statistiques des étudiants aux UEs""" + + self.etudids = self.matrice_notes.index + """Les étudids renseignés dans les moyennes""" + + self.champs = self.matrice_notes.columns + """Les champs (acronymes d'UE ou compétences) renseignés dans les moyennes""" + for col in self.champs: # if ue.type != UE_SPORT: + notes = matrice_notes[col] + self.moyennes[col] = pe_moy.Moyenne(notes) + + # Les moyennes générales + notes_gen = pd.Series(np.nan, index=self.matrice_notes.index) + """Les notes générales (moyenne toutes UEs confonudes)""" + if self.has_notes(): + notes_gen = self.compute_moy_gen( + self.matrice_notes, self.matrice_coeffs_moy_gen + ) + self.notes_gen = notes_gen + self.moyenne_gen = pe_moy.Moyenne(notes_gen) + """Le dataframe retraçant les moyennes/classements/statistiques général""" + + def has_notes(self): + """Détermine si les moyennes (aux UEs ou aux compétences) + ont des notes + + Returns: + True si la moytag a des notes, False sinon + """ + notes = self.matrice_notes + nbre_nan = notes.isna().sum().sum() + nbre_notes_potentielles = len(notes.index) * len(notes.columns) + if nbre_nan == nbre_notes_potentielles: + return False + else: + return True + + def compute_moy_gen(self, moys: pd.DataFrame, coeffs: pd.DataFrame) -> pd.Series: + """Calcule la moyenne générale (toutes UE/compétences confondus) + pour le tag considéré, en pondérant les notes obtenues au UE + par les coeff (généralement les crédits ECTS). + + Args: + moys: Les moyennes etudids x acronymes_ues/compétences + coeff: Les coeff etudids x ueids/compétences + """ + + # Calcule la moyenne générale dans le semestre (pondérée par le ECTS) + try: + moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects( + moys, + coeffs.fillna(0.0), + # formation_id=self.formsemestre.formation_id, + skip_empty_ues=True, + ) + except TypeError as e: + raise TypeError( + "Pb dans le calcul de la moyenne toutes UEs/compétences confondues" + ) + + return moy_gen_tag + + def to_df(self, aggregat=None, cohorte=None) -> pd.DataFrame: + """Renvoie le df synthétisant l'ensemble des données + connues + Adapte les intitulés des colonnes aux données fournies + (nom d'aggrégat, type de cohorte). + """ + + etudids_sorted = sorted(self.etudids) + + df = pd.DataFrame(index=etudids_sorted) + + # Ajout des notes pour tous les champs + champs = list(self.champs) + for champ in champs: + df_champ = self.moyennes[champ].get_df_synthese() # le dataframe + # Renomme les colonnes + cols = [ + get_colonne_df(aggregat, self.tag, champ, cohorte, critere) + for critere in pe_moy.Moyenne.COLONNES_SYNTHESE + ] + df_champ.columns = cols + df = df.join(df_champ) + + # Ajoute la moy générale + df_moy_gen = self.moyenne_gen.get_df_synthese() + cols = [ + get_colonne_df(aggregat, self.tag, CHAMP_GENERAL, cohorte, critere) + for critere in pe_moy.Moyenne.COLONNES_SYNTHESE + ] + df_moy_gen.columns = cols + df = df.join(df_moy_gen) + + return df + + +def get_colonne_df(aggregat, tag, champ, cohorte, critere): + """Renvoie le tuple (aggregat, tag, champ, cohorte, critere) + utilisé pour désigner les colonnes du df""" + liste_champs = [] + if aggregat: + liste_champs += [aggregat] + liste_champs += [tag, champ] + if cohorte: + liste_champs += [cohorte] + liste_champs += [critere] + return tuple(liste_champs) diff --git a/app/pe/pe_rcstag.py b/app/pe/moys/pe_rcstag.py similarity index 97% rename from app/pe/pe_rcstag.py rename to app/pe/moys/pe_rcstag.py index f8f6b9dc7..664ff9888 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/moys/pe_rcstag.py @@ -36,19 +36,17 @@ 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 from app.pe.rcss import pe_rcs, pe_rcsemx -import app.pe.pe_sxtag as pe_sxtag +import app.pe.moys.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 +from app.pe.moys import pe_tabletags, pe_moytag -class RCSTag(TableTag): +class RCSTag(pe_tabletags.TableTag): def __init__( self, rcsemx: pe_rcsemx.RCSemX, sxstags: dict[(str, int) : pe_sxtag.SxTag] ): @@ -62,7 +60,7 @@ class RCSTag(TableTag): rcsemx: Le RCSemX (identifié par un nom et l'id de son semestre terminal) sxstags: Les données sur les SemX taggués """ - TableTag.__init__(self) + 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)""" @@ -121,10 +119,9 @@ class RCSTag(TableTag): pe_affichage.pe_print(f"* Tags : {', '.join(self.tags_sorted)}") # Les moyennes - self.moyennes_tags: dict[str, MoyennesTag] = {} + 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: - print(tag) # 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 @@ -147,8 +144,11 @@ class RCSTag(TableTag): 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 + self.moyennes_tags[tag] = pe_moytag.MoyennesTag( + tag, + pe_moytag.CODE_MOY_COMPETENCES, + moys_competences, + matrice_coeffs_moy_gen, ) def __eq__(self, other): diff --git a/app/pe/pe_ressemtag.py b/app/pe/moys/pe_ressemtag.py similarity index 95% rename from app/pe/pe_ressemtag.py rename to app/pe/moys/pe_ressemtag.py index 09b88ab7f..0732cb8e3 100644 --- a/app/pe/pe_ressemtag.py +++ b/app/pe/moys/pe_ressemtag.py @@ -37,16 +37,13 @@ Created on Fri Sep 9 09:15:05 2016 """ import pandas as pd -from app import db, ScoValueError +from app import ScoValueError from app import comp from app.comp.res_but import ResultatsSemestreBUT -from app.comp.res_sem import load_formsemestre_results from app.models import FormSemestre, UniteEns -from app.models.moduleimpls import ModuleImpl import app.pe.pe_affichage as pe_affichage import app.pe.pe_etudiant as pe_etudiant -import app.pe.pe_tabletags as pe_tabletags -from app.pe.pe_moytag import MoyennesTag +from app.pe.moys import pe_tabletags, pe_moytag from app.scodoc import sco_tag_module from app.scodoc import codes_cursus as sco_codes @@ -126,17 +123,20 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): # pe_affichage.pe_print(f" -> Traitement du tag {tag}") infos_tag = tags_dict["personnalises"][tag] moy_ues_tag = self.compute_moy_ues_tag(infos_tag) - self.moyennes_tags[tag] = MoyennesTag( - tag, moy_ues_tag, self.matrice_coeffs_moy_gen + self.moyennes_tags[tag] = pe_moytag.MoyennesTag( + tag, pe_moytag.CODE_MOY_UE, moy_ues_tag, self.matrice_coeffs_moy_gen ) # Ajoute les moyennes par UEs + la moyenne générale (but) moy_gen = self.compute_moy_gen() - self.moyennes_tags["but"] = MoyennesTag( - "but", moy_gen, self.matrice_coeffs_moy_gen # , moy_gen_but + self.moyennes_tags["but"] = pe_moytag.MoyennesTag( + "but", + pe_moytag.CODE_MOY_UE, + moy_gen, + self.matrice_coeffs_moy_gen, # , moy_gen_but ) - self.tags_sorted = self.get_all_tags() + self.tags_sorted = self.get_all_significant_tags() """Tags (personnalisés+compétences) par ordre alphabétique""" def get_repr(self, verbose=False) -> str: @@ -302,11 +302,12 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): dict_tags["auto"] = {"but": {}} noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp - + aff_tags_auto = ", ".join([f"👜{nom}" for nom in noms_tags_auto]) + aff_tags_perso = ", ".join([f"👜{nom}" for nom in noms_tags_perso]) # Affichage pe_affichage.pe_print( - f"* Tags du programme de formation : {', '.join(noms_tags_perso)} " - + f"Tags automatiques : {', '.join(noms_tags_auto)}" + f"* Tags du programme de formation : {aff_tags_perso} + " + + f"Tags automatiques : {aff_tags_auto}" ) return dict_tags diff --git a/app/pe/pe_sxtag.py b/app/pe/moys/pe_sxtag.py similarity index 97% rename from app/pe/pe_sxtag.py rename to app/pe/moys/pe_sxtag.py index b7600bcac..abdd0bde6 100644 --- a/app/pe/pe_sxtag.py +++ b/app/pe/moys/pe_sxtag.py @@ -37,16 +37,15 @@ Created on Fri Sep 9 09:15:05 2016 """ from app.pe import pe_affichage, pe_comp -import app.pe.pe_ressemtag as pe_ressemtag +import app.pe.moys.pe_ressemtag as pe_ressemtag import pandas as pd import numpy as np -from app.pe.pe_tabletags import TableTag -from app.pe.pe_moytag import MoyennesTag +from app.pe.moys import pe_moytag, pe_tabletags import app.pe.rcss.pe_trajectoires as pe_trajectoires -class SxTag(TableTag): +class SxTag(pe_tabletags.TableTag): def __init__( self, sxtag_id: (str, int), @@ -80,7 +79,7 @@ class SxTag(TableTag): les semestres à regrouper et les résultats/moyennes par tag des semestres """ - TableTag.__init__(self) + pe_tabletags.TableTag.__init__(self) assert sxtag_id and len(sxtag_id) == 2 and sxtag_id[1] in ressembuttags @@ -186,8 +185,11 @@ class SxTag(TableTag): self.__aff_profil_coeff_ects(tag) # Mémorise les infos pour la moyennes au tag - self.moyennes_tags[tag] = MoyennesTag( - tag, matrice_moys_ues, self.matrice_coeffs_moy_gen + self.moyennes_tags[tag] = pe_moytag.MoyennesTag( + tag, + pe_moytag.CODE_MOY_UE, + matrice_moys_ues, + self.matrice_coeffs_moy_gen, ) def __aff_profil_coeff_ects(self, tag): diff --git a/app/pe/moys/pe_tabletags.py b/app/pe/moys/pe_tabletags.py new file mode 100644 index 000000000..67564736d --- /dev/null +++ b/app/pe/moys/pe_tabletags.py @@ -0,0 +1,204 @@ +# -*- 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 Thu Sep 8 09:36:33 2016 + +@author: barasc +""" + +import pandas as pd + +from app.models import Identite +from app.pe.moys import pe_moytag + +TAGS_RESERVES = ["but"] + +CHAMPS_ADMINISTRATIFS = ["Civilité", "Nom", "Prénom"] + + +class TableTag(object): + def __init__(self): + """Classe centralisant différentes méthodes communes aux + SemestreTag, TrajectoireTag, AggregatInterclassTag + """ + # Les étudiants + # self.etuds: list[Identite] = None # A venir + """Les étudiants""" + # self.etudids: list[int] = {} + """Les etudids""" + + def add_etuds(self, etuds: list[Identite]): + """Mémorise les informations sur les étudiants + + Args: + etuds: la liste des identités de l'étudiant + """ + # self.etuds = etuds + self.etudids = list({etud.etudid for etud in etuds}) + + def get_all_significant_tags(self): + """Liste des tags de la table, triée par ordre alphabétique, + extraite des clés du dictionnaire ``moyennes_tags``, en ne + considérant que les moyennes ayant des notes. + + Returns: + Liste de tags triés par ordre alphabétique + """ + tags = [] + tag: str = "" + moytag: pe_moytag.MoyennesTag = None + for tag, moytag in self.moyennes_tags.items(): + if moytag.has_notes(): + tags.append(tag) + return sorted(tags) + + def to_df( + self, + administratif=True, + aggregat=None, + tags_cibles=None, + cohorte=None, + type_colonnes=True, + ) -> pd.DataFrame: + """Renvoie un dataframe listant toutes les données + des moyennes/classements/nb_inscrits/min/max/moy + des étudiants aux différents tags. + + tags_cibles limitent le dataframe aux tags indiqués + type_colonnes indiquent si les colonnes doivent être passées en multiindex + + Args: + administratif: Indique si les données administratives sont incluses + aggregat: l'aggrégat représenté + tags_cibles: la liste des tags ciblés + cohorte: la cohorte représentée + Returns: + Le dataframe complet de synthèse + """ + if not self.is_significatif(): + return None + + # Les tags visés + tags_tries = self.get_all_significant_tags() + if not tags_cibles: + tags_cibles = tags_tries + tags_cibles = sorted(tags_cibles) + + # Les tags visés avec des notes + + # Les étudiants visés + if administratif: + df = df_administratif(self.etuds, aggregat, cohorte) + else: + df = pd.DataFrame(index=self.etudids) + + # Ajout des données par tags + for tag in tags_cibles: + moy_tag_df = self.moyennes_tags[tag].to_df(aggregat, cohorte) + df = df.join(moy_tag_df) + + # Tri par nom, prénom + if administratif: + colonnes_tries = [ + _get_champ_administratif(champ, aggregat, cohorte) + for champ in CHAMPS_ADMINISTRATIFS[1:] + ] # Nom + Prénom + df = df.sort_values(by=colonnes_tries) + + # Conversion des colonnes en multiindex + if type_colonnes: + df.columns = pd.MultiIndex.from_tuples(df.columns) + + return df + + def has_etuds(self): + """Indique si un tabletag contient des étudiants""" + return len(self.etuds) > 0 + + def is_significatif(self): + """Indique si une tabletag a des données""" + # A des étudiants + if not self.etuds: + return False + # A des tags avec des notes + tags_tries = self.get_all_significant_tags() + if not tags_tries: + return False + return True + + +def _get_champ_administratif(champ, aggregat=None, cohorte=None): + """Pour un champ donné, renvoie l'index (ou le multindex) + à intégrer au dataframe""" + liste = [] + if aggregat: + liste += [aggregat] + liste += ["Administratif", "Identité"] + if cohorte: + liste += [champ] + liste += [champ] + return tuple(liste) + + +def df_administratif( + etuds: list[Identite], aggregat=None, cohorte=None +) -> pd.DataFrame: + """Renvoie un dataframe donnant les données administratives + des étudiants du TableTag + + Args: + etuds: Identité des étudiants générant les données administratives + """ + identites = {etud.etudid: etud for etud in etuds} + + donnees = {} + etud: Identite = None + for etudid, etud in identites.items(): + data = { + CHAMPS_ADMINISTRATIFS[0]: etud.civilite_str, + CHAMPS_ADMINISTRATIFS[1]: etud.nom, + CHAMPS_ADMINISTRATIFS[2]: etud.prenom_str, + } + donnees[etudid] = { + _get_champ_administratif(champ, aggregat, cohorte): data[champ] + for champ in CHAMPS_ADMINISTRATIFS + } + + colonnes = [ + _get_champ_administratif(champ, aggregat, cohorte) + for champ in CHAMPS_ADMINISTRATIFS + ] + df = pd.DataFrame.from_dict(donnees, orient="index", columns=colonnes) + df = df.sort_values(by=colonnes[1:]) + return df diff --git a/app/pe/pe_interclasstag.py b/app/pe/pe_interclasstag.py deleted file mode 100644 index 3de44d234..000000000 --- a/app/pe/pe_interclasstag.py +++ /dev/null @@ -1,160 +0,0 @@ -############################################################################## -# -# 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 Thu Sep 8 09:36:33 2016 - -@author: barasc -""" - -import pandas as pd -import numpy as np - -from app.pe.pe_tabletags import TableTag -from app.pe.pe_moytag import MoyennesTag -import app.pe.pe_etudiant as pe_etudiant -import app.pe.rcss.pe_rcs as pe_rcs -import app.pe.pe_rcss_jury as pe_rcss_jury -import app.pe.pe_rcstag as pe_rcstag - - -class RCSInterclasseTag(TableTag): - """ - Interclasse l'ensemble des étudiants diplômés à une année - donnée (celle du jury), pour un RCS donné (par ex: 'S2', '3S') - en reportant : - - * les moyennes obtenues sur la trajectoire qu'il ont suivi pour atteindre - le numéro de semestre de fin de l'aggrégat (indépendamment de son - formsemestre) - * calculant le classement sur les étudiants diplômes - """ - - def __init__( - self, - nom_rcs: str, - etudiants: pe_etudiant.EtudiantsJuryPE, - rcss_jury_pe: pe_rcss_jury.RCSsJuryPE, - rcss_tags: dict[tuple, pe_rcstag.RCSTag], - ): - TableTag.__init__(self) - - self.nom_rcs = nom_rcs - """Le nom du RCS interclassé""" - - self.nom = self.get_repr() - """Représentation textuelle de l'interclassement""" - - self.diplomes_ids = etudiants.etudiants_diplomes - """Identité des étudiants diplômés""" - - self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids} - """Etudids des étudiants diplômés""" - - # Les RCSemX et leur versions taggués SxTag, en ne gardant que - # celles associées à l'aggrégat - self.rcss: dict[int, pe_rcs.RCS] = {} - """Ensemble des trajectoires associées à l'aggrégat""" - for trajectoire_id in rcss_jury_pe.trajectoires: - trajectoire = rcss_jury_pe.trajectoires[trajectoire_id] - if trajectoire_id[0] == nom_rcs: - self.rcss[trajectoire_id] = trajectoire - - self.trajectoires_taggues: dict[int, pe_rcs.RCS] = {} - """Ensemble des trajectoires tagguées associées à l'aggrégat""" - for trajectoire_id in self.rcss: - self.trajectoires_taggues[trajectoire_id] = rcss_tags[trajectoire_id] - - # Les trajectoires suivies par les étudiants du jury, en ne gardant que - # celles associées aux diplomés - self.suivi: dict[int, pe_rcs.RCS] = {} - """Association entre chaque étudiant et la trajectoire tagguée à prendre en - compte pour l'aggrégat""" - for etudid in self.diplomes_ids: - self.suivi[etudid] = rcss_jury_pe.trajectoires_suivies[etudid][nom_rcs] - - self.tags_sorted = self.do_taglist() - """Liste des tags (triés par ordre alphabétique)""" - - # Construit la matrice de notes - self.notes = self.compute_notes_matrice() - """Matrice des notes de l'aggrégat""" - - # Synthétise les moyennes/classements par tag - self.moyennes_tags: dict[str, MoyennesTag] = {} - for tag in self.tags_sorted: - moy_gen_tag = self.notes[tag] - self.moyennes_tags[tag] = MoyennesTag(tag, moy_gen_tag) - - # Est significatif ? (aka a-t-il des tags et des notes) - self.significatif = len(self.tags_sorted) > 0 - - def get_repr(self) -> str: - """Une représentation textuelle""" - return f"Aggrégat {self.nom_rcs}" - - def do_taglist(self): - """Synthétise les tags à partir des trajectoires_tagguées - - Returns: - Une liste de tags triés par ordre alphabétique - """ - tags = [] - for trajectoire in self.trajectoires_taggues.values(): - tags.extend(trajectoire.tags_sorted) - return sorted(set(tags)) - - def compute_notes_matrice(self): - """Construit la matrice de notes (etudid x tags) - retraçant les moyennes obtenues par les étudiants dans les semestres associés à - l'aggrégat (une trajectoire ayant pour numéro de semestre final, celui de l'aggrégat). - """ - # nb_tags = len(self.tags_sorted) unused ? - # nb_etudiants = len(self.diplomes_ids) - - # Index de la matrice (etudids -> dim 0, tags -> dim 1) - etudids = list(self.diplomes_ids) - tags = self.tags_sorted - - # Partant d'un dataframe vierge - df = pd.DataFrame(np.nan, index=etudids, columns=tags) - - for trajectoire in self.trajectoires_taggues.values(): - # Charge les moyennes par tag de la trajectoire tagguée - notes = trajectoire.notes - # Etudiants/Tags communs entre la trajectoire_tagguée et les données interclassées - etudids_communs = df.index.intersection(notes.index) - tags_communs = df.columns.intersection(notes.columns) - - # Injecte les notes par tag - df.loc[etudids_communs, tags_communs] = notes.loc[ - etudids_communs, tags_communs - ] - - return df diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 92fb6f95e..aa87cc8e5 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -51,13 +51,18 @@ import numpy as np import pandas as pd from app.pe.rcss import pe_rcs -from app.pe import pe_sxtag +from app.pe.moys import pe_sxtag from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE import app.pe.pe_affichage as pe_affichage import app.pe.pe_etudiant as pe_etudiant -from app.pe.pe_rcstag import RCSTag -from app.pe.pe_ressemtag import ResSemBUTTag -from app.pe.pe_interclasstag import RCSInterclasseTag +from app.pe.moys import ( + pe_tabletags, + pe_ressemtag, + pe_sxtag, + pe_rcstag, + pe_interclasstag, + pe_moytag, +) import app.pe.pe_rcss_jury as pe_rcss_jury @@ -85,8 +90,7 @@ class JuryPE(object): ) # Chargement des étudiants à prendre en compte dans le jury pe_affichage.pe_print( - f"""*** Recherche et chargement des étudiants diplômés en { - self.diplome}""" + f"""*** Recherche des étudiants diplômés 🎓 en {self.diplome}""" ) self.etudiants = pe_etudiant.EtudiantsJuryPE( self.diplome @@ -110,8 +114,8 @@ class JuryPE(object): self._gen_xls_sxtags(zipfile) self._gen_rcsemxs() self._gen_xls_rcstags(zipfile) - # self._gen_xls_interclassements_rcss(zipfile) - # self._gen_xls_synthese_jury_par_tag(zipfile) + self._gen_xls_interclasstags(zipfile) + self._gen_xls_synthese_jury_par_tag(zipfile) # self._gen_xls_synthese_par_etudiant(zipfile) except Exception as e: raise e @@ -159,7 +163,7 @@ class JuryPE(object): self.ressembuttags = {} for frmsem_id, formsemestre in formsemestres.items(): # Crée le semestre_tag et exécute les calculs de moyennes - self.ressembuttags[frmsem_id] = ResSemBUTTag(formsemestre) + self.ressembuttags[frmsem_id] = pe_ressemtag.ResSemBUTTag(formsemestre) # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() @@ -170,7 +174,7 @@ class JuryPE(object): for res_sem_tag in self.ressembuttags.values(): onglet = res_sem_tag.get_repr(verbose=True) onglets += [] - df = res_sem_tag.df_moyennes_et_classements() + df = res_sem_tag.to_df() # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") @@ -219,7 +223,6 @@ class JuryPE(object): for rcf_id, rcf in self.rcss_jury.semXs.items(): # SxTag traduisant le RCF sxtag_id = rcf_id - self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, rcf, self.ressembuttags) # Intègre le bilan des semestres taggués au zip final @@ -230,20 +233,21 @@ class JuryPE(object): onglets = [] for sxtag in self.sxtags.values(): onglet = sxtag.get_repr(verbose=False) - onglets += [onglet] - df = sxtag.df_moyennes_et_classements() - # écriture dans l'onglet - df.to_excel(writer, onglet, index=True, header=True) + if sxtag.is_significatif(): + df = sxtag.to_df() + onglets += [onglet] + # écriture dans l'onglet + df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) - - self.add_file_to_zip( - zipfile, - f"semestres_taggues_{self.diplome}.xlsx", - output.read(), - path="details", - ) + if onglets: + self.add_file_to_zip( + zipfile, + f"semestres_taggues_{self.diplome}.xlsx", + output.read(), + path="details", + ) def _gen_rcsemxs(self): """Génère les regroupements cohérents de RCFs qu'ont suivi chaque étudiant""" @@ -279,7 +283,7 @@ class JuryPE(object): self.rcss_tags = {} for rcs_id, rcsemx in self.rcss_jury.rcsemxs.items(): - self.rcss_tags[rcs_id] = RCSTag(rcsemx, self.sxtags) + self.rcss_tags[rcs_id] = pe_rcstag.RCSTag(rcsemx, self.sxtags) # Intègre le bilan des trajectoires tagguées au zip final output = io.BytesIO() @@ -289,27 +293,67 @@ class JuryPE(object): onglets = [] for rcs_tag in self.rcss_tags.values(): onglet = rcs_tag.get_repr(verbose=False) - onglets += [onglet] - df = rcs_tag.df_moyennes_et_classements() - # écriture dans l'onglet - df.to_excel(writer, onglet, index=True, header=True) + if rcs_tag.is_significatif(): + df = rcs_tag.to_df() + onglets += [onglet] + # écriture dans l'onglet + df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) - self.add_file_to_zip( - zipfile, - f"RCRCFs_{self.diplome}.xlsx", - output.read(), - path="details", - ) + if onglets: + self.add_file_to_zip( + zipfile, + f"RCRCFs_{self.diplome}.xlsx", + output.read(), + path="details", + ) - def _gen_xls_interclassements_rcss(self, zipfile: ZipFile): - """Intègre le bilan des RCS (interclassé par promo) au zip""" - # Génère les interclassements (par promo et) par (nom d') aggrégat - pe_affichage.pe_print("*** Génère les interclassements") - self.interclassements_taggues = compute_interclassements( - self.etudiants, self.rcss_jury, self.rcss_tags + def _gen_xls_interclasstags(self, zipfile: ZipFile): + """Génère les interclassements sur la promo de diplômés + par (nom d') aggrégat + en distinguant les interclassements par accronymes d'UEs (sur les SxTag) + et ceux par compétences (sur les RCSTag). + """ + pe_affichage.pe_print( + "*** Génère les interclassements sur chaque type de RCS/agrgégat" ) + self.interclasstags = { + pe_moytag.CODE_MOY_UE: {}, + pe_moytag.CODE_MOY_COMPETENCES: {}, + } + + etudiants_diplomes = self.etudiants.etudiants_diplomes + + # Les interclassements par UE + for Sx in pe_rcs.TOUS_LES_SEMESTRES: + pe_affichage.pe_print( + f"--> Interclassement par (acronyme d')UEs pour le RCS {Sx}" + ) + interclass = pe_interclasstag.InterClassTag( + Sx, + pe_moytag.CODE_MOY_UE, + etudiants_diplomes, + self.rcss_jury.semXs, + self.sxtags, + self.rcss_jury.semXs_suivis, + ) + self.interclasstags[pe_moytag.CODE_MOY_UE][Sx] = interclass + + # Les interclassements par compétences + for nom_rcs in pe_rcs.TOUS_LES_RCS: + pe_affichage.pe_print( + f"--> Interclassement par compétences pour le RCS {nom_rcs}" + ) + interclass = pe_interclasstag.InterClassTag( + nom_rcs, + pe_moytag.CODE_MOY_COMPETENCES, + etudiants_diplomes, + self.rcss_jury.rcsemxs, + self.rcss_tags, + self.rcss_jury.rcsemxs_suivis, + ) + self.interclasstags[pe_moytag.CODE_MOY_COMPETENCES][nom_rcs] = interclass # Intègre le bilan des aggrégats (interclassé par promo) au zip final output = io.BytesIO() @@ -317,23 +361,29 @@ class JuryPE(object): output, engine="openpyxl" ) as writer: onglets = [] - for interclass_tag in self.interclassements_taggues.values(): - if interclass_tag.significatif: # Avec des notes - onglet = interclass_tag.get_repr() - onglets += [onglet] - df = interclass_tag.df_moyennes_et_classements() - # écriture dans l'onglet - df.to_excel(writer, onglet, index=True, header=True) + for type_interclass in [ + pe_moytag.CODE_MOY_UE, + pe_moytag.CODE_MOY_COMPETENCES, + ]: + interclasstag = self.interclasstags[type_interclass] + for nom_rcs, interclass in interclasstag.items(): + onglet = interclass.get_repr() + if interclass.is_significatif(): + df = interclass.to_df(cohorte="Promo") + onglets += [onglet] + # écriture dans l'onglet + df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) - self.add_file_to_zip( - zipfile, - f"interclassements_taggues_{self.diplome}.xlsx", - output.read(), - path="details", - ) + if onglets: + self.add_file_to_zip( + zipfile, + f"InterClassTags_{self.diplome}.xlsx", + output.read(), + path="details", + ) def _gen_xls_synthese_jury_par_tag(self, zipfile: ZipFile): """Synthèse des éléments du jury PE tag par tag""" @@ -347,7 +397,8 @@ class JuryPE(object): output, engine="openpyxl" ) as writer: onglets = [] - for onglet, df in self.synthese.items(): + for (tag, type_moy), df in self.synthese.items(): + onglet = f"{tag} {type_moy}" onglets += [onglet] # écriture dans l'onglet: df.to_excel(writer, onglet, index=True, header=True) @@ -407,15 +458,18 @@ class JuryPE(object): self.zipdata.seek(0) return self.zipdata - def do_tags_list(self, interclassements: dict[str, RCSInterclasseTag]): + def _do_tags_list(self, interclassements: dict[str, dict]): """La liste des tags extraites des interclassements""" tags = [] - for aggregat in interclassements: - interclass = interclassements[aggregat] - if interclass.tags_sorted: - tags.extend(interclass.tags_sorted) - tags = sorted(set(tags)) - return tags + # Pour chaque type d'interclassements + for type in interclassements: + interclassement = interclassements[type] + for aggregat in interclassement: + interclass = interclassement[aggregat] + if interclass.tags_sorted: + tags.extend(interclass.tags_sorted) + tags = sorted(set(tags)) + return tags # **************************************************************************************************************** # # Méthodes pour la synthèse du juryPE @@ -423,154 +477,79 @@ class JuryPE(object): def synthetise_jury_par_tags(self) -> dict[pd.DataFrame]: """Synthétise tous les résultats du jury PE dans des dataframes, - dont les onglets sont les tags""" + dont les onglets sont les tags et des types de calcul de moyennes + (par UEs ou par compétences)""" - pe_affichage.pe_print("*** Synthèse finale des moyennes par tag***") + pe_affichage.pe_print( + "*** Synthèse finale des moyennes par tag et par type de moyennes (UEs ou Compétences)***" + ) synthese = {} pe_affichage.pe_print(" -> Synthèse des données administratives") synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids) - tags = self.do_tags_list(self.interclassements_taggues) + tags = self._do_tags_list(self.interclasstags) for tag in tags: - pe_affichage.pe_print(f" -> Synthèse du tag {tag}") - synthese[tag] = self.df_tag(tag) + for type_moy in [pe_moytag.CODE_MOY_UE, pe_moytag.CODE_MOY_COMPETENCES]: + pe_affichage.pe_print(f" -> Synthèse du tag {tag} par {type_moy}") + synthese[(tag, type_moy)] = self.df_tag_type(tag, type_moy) return synthese - def df_tag(self, tag): - """Génère le DataFrame synthétisant les moyennes/classements (groupe, - interclassement promo) pour tous les aggrégats prévus, + def df_tag_type(self, tag, type_moy, type_colonnes=True): + """Génère le DataFrame synthétisant les moyennes/classements (groupe + + interclassement promo) pour tous les aggrégats prévus, en fonction + du type (UEs ou Compétences) de données souhaitées, tels que fourni dans l'excel final. + Si type=UEs => tous les sxtag du tag + Si type=Compétences => tous les rcstag du tag + Args: tag: Un des tags (a minima `but`) + type_moy: Un type de moyenne Returns: """ - etudids = list(self.diplomes_ids) - # Les données des étudiants - donnees_etudiants = {} - for etudid in etudids: - etudiant = self.etudiants.identites[etudid] - donnees_etudiants[etudid] = { - ("Identité", "", "Civilite"): etudiant.civilite_str, - ("Identité", "", "Nom"): etudiant.nom, - ("Identité", "", "Prenom"): etudiant.prenom, - } - df_synthese = pd.DataFrame.from_dict(donnees_etudiants, orient="index") + etuds = [etud for etudid, etud in self.etudiants.etudiants_diplomes.items()] + df = pe_tabletags.df_administratif(etuds, aggregat="", cohorte="") - # Ajout des aggrégats - for aggregat in pe_rcs.TOUS_LES_RCS: - descr = app.pe.rcss.pe_rcs.TYPES_RCS[aggregat]["descr"] + if type_moy == pe_moytag.CODE_MOY_UE: + aggregats = pe_rcs.TOUS_LES_SEMESTRES + else: + aggregats = pe_rcs.TOUS_LES_RCS - # Les trajectoires (tagguées) suivies par les étudiants pour l'aggrégat et le tag - # considéré - trajectoires_tagguees = [] - for etudid in etudids: - trajectoire = self.rcss_jury.trajectoires_suivies[etudid][aggregat] - if trajectoire: - tid = trajectoire.sxtag_id - trajectoire_tagguee = self.rcss_tags[tid] - if ( - tag in trajectoire_tagguee.moyennes_tags - and trajectoire_tagguee not in trajectoires_tagguees - ): - trajectoires_tagguees.append(trajectoire_tagguee) + for aggregat in aggregats: + print(aggregat) + # Descr de l'aggrégat + descr = pe_rcs.TYPES_RCS[aggregat]["descr"] - # Combien de notes vont être injectées ? - nbre_notes_injectees = 0 - for traj in trajectoires_tagguees: - moy_traj = traj.moyennes_tags[tag] - inscrits_traj = moy_traj.inscrits_ids - etudids_communs = set(etudids) & set(inscrits_traj) - nbre_notes_injectees += len(etudids_communs) + # L'interclassement associé + interclass = self.interclasstags[type_moy][aggregat] - # Si l'aggrégat est significatif (aka il y a des notes) - if nbre_notes_injectees > 0: - # Ajout des données classements & statistiques - nom_stat_promo = f"{NOM_STAT_PROMO} {self.diplome}" - donnees = pd.DataFrame( - index=etudids, - columns=[ - [descr] * (1 + 4 * 2), - [""] + [NOM_STAT_GROUPE] * 4 + [nom_stat_promo] * 4, - ["note"] + ["class.", "min", "moy", "max"] * 2, - ], + if interclass.is_significatif(): + # Le dataframe du classement sur le groupe + df_groupe = interclass.compute_df_synthese_moyennes_tag( + tag, aggregat=aggregat, type_colonnes=False ) + df = df.join(df_groupe) - for traj in trajectoires_tagguees: - # Les données des trajectoires_tagguees - moy_traj = traj.moyennes_tags[tag] + # Le dataframe du classement sur la promo + df_promo = interclass.to_df( + administratif=False, + aggregat=aggregat, + tags_cibles=[tag], + cohorte="Promo", + type_colonnes=False, + ) + df = df.join(df_promo) - # Les étudiants communs entre tableur de synthèse et trajectoires - inscrits_traj = moy_traj.inscrits_ids - etudids_communs = list(set(etudids) & set(inscrits_traj)) - - # Les notes - champ = (descr, "", "note") - notes_traj = moy_traj.get_notes() - donnees.loc[etudids_communs, champ] = notes_traj.loc[ - etudids_communs - ] - - # Les rangs - champ = (descr, NOM_STAT_GROUPE, "class.") - rangs = moy_traj.get_rangs_inscrits() - donnees.loc[etudids_communs, champ] = rangs.loc[etudids_communs] - - # Les mins - champ = (descr, NOM_STAT_GROUPE, "min") - mins = moy_traj.get_min() - donnees.loc[etudids_communs, champ] = mins.loc[etudids_communs] - - # Les max - champ = (descr, NOM_STAT_GROUPE, "max") - maxs = moy_traj.get_max() - donnees.loc[etudids_communs, champ] = maxs.loc[etudids_communs] - - # Les moys - champ = (descr, NOM_STAT_GROUPE, "moy") - moys = moy_traj.get_moy() - donnees.loc[etudids_communs, champ] = moys.loc[etudids_communs] - - # Ajoute les données d'interclassement - interclass = self.interclassements_taggues[aggregat] - moy_interclass = interclass.moyennes_tags[tag] - - # Les étudiants communs entre tableur de synthèse et l'interclassement - inscrits_interclass = moy_interclass.inscrits_ids - etudids_communs = list(set(etudids) & set(inscrits_interclass)) - - # Les classements d'interclassement - champ = (descr, nom_stat_promo, "class.") - rangs = moy_interclass.get_rangs_inscrits() - donnees.loc[etudids_communs, champ] = rangs.loc[etudids_communs] - - # Les mins - champ = (descr, nom_stat_promo, "min") - mins = moy_interclass.get_min() - donnees.loc[etudids_communs, champ] = mins.loc[etudids_communs] - - # Les max - champ = (descr, nom_stat_promo, "max") - maxs = moy_interclass.get_max() - donnees.loc[etudids_communs, champ] = maxs.loc[etudids_communs] - - # Les moys - champ = (descr, nom_stat_promo, "moy") - moys = moy_interclass.get_moy() - donnees.loc[etudids_communs, champ] = moys.loc[etudids_communs] - - df_synthese = df_synthese.join(donnees) - # Fin de l'aggrégat - - # Tri par nom/prénom - df_synthese.sort_values( - by=[("Identité", "", "Nom"), ("Identité", "", "Prenom")], inplace=True - ) - return df_synthese + # Conversion des colonnes en multiindex + if type_colonnes: + df.columns = pd.MultiIndex.from_tuples(df.columns) + return df + # Fin de l'aggrégat def synthetise_jury_par_etudiants(self) -> dict[pd.DataFrame]: """Synthétise tous les résultats du jury PE dans des dataframes, @@ -600,7 +579,7 @@ class JuryPE(object): def df_synthese_etudiant(self, etudid: int) -> pd.DataFrame: """Créé un DataFrame pour un étudiant donné par son etudid, retraçant toutes ses moyennes aux différents tag et aggrégats""" - tags = self.do_tags_list(self.interclassements_taggues) + tags = self._do_tags_list(self.interclasstags) donnees = {} @@ -620,7 +599,7 @@ class JuryPE(object): trajectoire_tagguee = self.rcss_tags[trajectoire.sxtag_id] if tag in trajectoire_tagguee.moyennes_tags: # L'interclassement - interclass = self.interclassements_taggues[aggregat] + interclass = self.interclasstags[aggregat] # Injection des données dans un dictionnaire donnees[tag] |= get_dict_synthese_aggregat( @@ -662,25 +641,6 @@ def get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict: return semestres -def compute_interclassements( - etudiants: pe_etudiant.EtudiantsJuryPE, - trajectoires_jury_pe: pe_rcss_jury.RCSsJuryPE, - trajectoires_tagguees: dict[tuple, pe_rcs.RCS], -): - """Interclasse les étudiants, (nom d') aggrégat par aggrégat, - pour fournir un classement sur la promo. Le classement est établi au regard du nombre - d'étudiants ayant participé au même aggrégat. - """ - aggregats_interclasses_taggues = {} - for nom_aggregat in pe_rcs.TOUS_LES_RCS: - pe_affichage.pe_print(f" --> Interclassement {nom_aggregat}") - interclass = RCSInterclasseTag( - nom_aggregat, etudiants, trajectoires_jury_pe, trajectoires_tagguees - ) - aggregats_interclasses_taggues[nom_aggregat] = interclass - return aggregats_interclasses_taggues - - def get_defaut_dict_synthese_aggregat(nom_rcs: str, diplome: int) -> dict: """Renvoie le dictionnaire de synthèse (à intégrer dans un tableur excel) pour décrire les résultats d'un aggrégat @@ -723,8 +683,8 @@ def get_defaut_dict_synthese_aggregat(nom_rcs: str, diplome: int) -> dict: def get_dict_synthese_aggregat( aggregat: str, - trajectoire_tagguee: RCSTag, - interclassement_taggue: RCSInterclasseTag, + trajectoire_tagguee: pe_rcstag.RCSTag, + interclassement_taggue: pe_interclasstag.InterClassTag, etudid: int, tag: str, diplome: int, diff --git a/app/pe/pe_tabletags.py b/app/pe/pe_tabletags.py deleted file mode 100644 index a14f593b4..000000000 --- a/app/pe/pe_tabletags.py +++ /dev/null @@ -1,122 +0,0 @@ -# -*- 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 Thu Sep 8 09:36:33 2016 - -@author: barasc -""" - -import pandas as pd - -from app.models import Identite - -TAGS_RESERVES = ["but"] - - -class TableTag(object): - def __init__(self): - """Classe centralisant différentes méthodes communes aux - SemestreTag, TrajectoireTag, AggregatInterclassTag - """ - # Les étudiants - # self.etuds: list[Identite] = None # A venir - """Les étudiants""" - # self.etudids: list[int] = {} - """Les etudids""" - - def add_etuds(self, etuds: list[Identite]): - """Mémorise les informations sur les étudiants - - Args: - etuds: la liste des identités de l'étudiant - """ - # self.etuds = etuds - self.etudids = list({etud.etudid for etud in etuds}) - - def get_all_tags(self): - """Liste des tags de la table, triée par ordre alphabétique, - extraite des clés du dictionnaire ``moyennes_tags`` connues (tags en doublon - possible). - - Returns: - Liste de tags triés par ordre alphabétique - """ - return sorted(list(self.moyennes_tags.keys())) - - def df_moyennes_et_classements(self) -> pd.DataFrame: - """Renvoie un dataframe listant toutes les moyennes, - et les classements des étudiants pour tous les tags. - - Est utilisé pour afficher le détail d'un tableau taggué - (semestres, trajectoires ou aggrégat) - - Returns: - Le dataframe des notes et des classements - """ - - etudiants = {etud.etudid: [etud.nom, etud.prenom] for etud in self.etuds} - df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom", "prenom"]) - - tags_tries = self.get_all_tags() - for tag in tags_tries: - moy_tag = self.moyennes_tags[tag] - for acronyme in moy_tag.moyennes: - moy = moy_tag.moyennes[acronyme] # une moyenne - df = df.join(moy.synthese["notes"].rename(f"Moy {tag}-{acronyme}")) - df = df.join( - moy.synthese["classements"].rename(f"Class {tag}-{acronyme}") - ) - moy_gen = moy_tag.moyenne_gen - df = df.join(moy_gen.synthese["notes"].rename(f"Moy {tag} (gen)")) - df = df.join(moy_gen.synthese["classements"].rename(f"Class {tag} (gen)")) - - df.sort_values(by=["nom", "prenom"]) - - return df - - def df_notes(self) -> pd.DataFrame | None: - """Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags - - Returns: - Un dataframe etudids x tag (avec tag par ordre alphabétique) - """ - tags_tries = self.get_all_tags() - if tags_tries: - dict_series = {} - for tag in tags_tries: - # Les moyennes associés au tag - moy_tag = self.moyennes_tags[tag] - dict_series[tag] = moy_tag.synthese["notes"] - df = pd.DataFrame(dict_series) - return df diff --git a/app/pe/rcss/pe_rcsemx.py b/app/pe/rcss/pe_rcsemx.py index 8c6bb0b0d..94d3db5d4 100644 --- a/app/pe/rcss/pe_rcsemx.py +++ b/app/pe/rcss/pe_rcsemx.py @@ -10,7 +10,7 @@ Created on 01-2024 """ from app.models import FormSemestre -from app.pe import pe_sxtag, pe_affichage +from app.pe.moys import pe_sxtag from app.pe.rcss import pe_rcs, pe_trajectoires