diff --git a/app/pe/moys/pe_interclasstag.py b/app/pe/moys/pe_interclasstag.py index 382d88fe..0dfd25f2 100644 --- a/app/pe/moys/pe_interclasstag.py +++ b/app/pe/moys/pe_interclasstag.py @@ -198,7 +198,7 @@ class InterClassTag(pe_tabletags.TableTag): if tag in rcstag.moyennes_tags: moytag = rcstag.moyennes_tags[tag] - notes = moytag.matrice_notes_gen # dataframe etudids x ues + notes = moytag.matrice_notes # dataframe etudids x ues # Etudiants/Champs communs entre le RCSTag et les données interclassées ( @@ -231,7 +231,7 @@ class InterClassTag(pe_tabletags.TableTag): 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_coeffs_moy_gen + coeffs: pd.DataFrame = rcstag.moyennes_tags[tag].matrice_coeffs # Etudiants/Champs communs entre le RCSTag et les données interclassées ( diff --git a/app/pe/moys/pe_moy.py b/app/pe/moys/pe_moy.py index 38883edf..20e1a268 100644 --- a/app/pe/moys/pe_moy.py +++ b/app/pe/moys/pe_moy.py @@ -41,11 +41,11 @@ class Moyenne: self.etudids = list(notes.index) # calcul à venir """Les id des étudiants""" self.inscrits_ids = notes[notes.notnull()].index.to_list() - """Les id des étudiants dont la note est non nulle""" + """Les id des étudiants dont la note est non nan/renseignée""" self.df: pd.DataFrame = self.comp_moy_et_stat(self.notes) """Le dataframe retraçant les moyennes/classements/statistiques""" - self.synthese = self.to_dict() - """La synthèse (dictionnaire) des notes/classements/statistiques""" + # self.synthese = self.to_dict() + # """La synthèse (dictionnaire) des notes/classements/statistiques""" def comp_moy_et_stat(self, notes: pd.Series) -> dict: """Calcule et structure les données nécessaires au PE pour une série @@ -102,8 +102,10 @@ class Moyenne: return df - def get_df_synthese(self, with_min_max_moy=None): - """Renvoie le df de synthese limité aux colonnes de synthese""" + def to_df(self, with_min_max_moy=None): + """Renvoie le df de synthèse, en limitant les colonnes à celles attendues + (dépendantes de l'option `with_min_max_moy`) + """ colonnes_synthese = Moyenne.get_colonnes_synthese( with_min_max_moy=with_min_max_moy ) @@ -111,18 +113,12 @@ class Moyenne: df["rang"] = df["rang"].replace("nan", "") return df - def to_dict(self) -> dict: + def to_json(self) -> dict: """Renvoie un dictionnaire de synthèse des moyennes/classements/statistiques générale (but)""" - synthese = { - "notes": self.df["note"], - "classements": self.df["classement"], - "min": self.df["min"].mean(), - "max": self.df["max"].mean(), - "moy": self.df["moy"].mean(), - "nb_inscrits": self.df["nb_inscrits"].mean(), - } - return synthese + df = self.to_df(with_min_max_moy=True) + resultat = df.to_json(orient="index") + return resultat - def is_significatif(self) -> bool: - """Indique si la moyenne est significative (c'est-à-dire à des notes)""" - return self.synthese["nb_inscrits"] > 0 + def has_notes(self) -> bool: + """Indique si la moyenne est significative (c'est-à-dire à des notes) et/ou des inscrits""" + return len(self.inscrits_ids) > 0 diff --git a/app/pe/moys/pe_moytag.py b/app/pe/moys/pe_moytag.py index 1d8bcc00..34ce2050 100644 --- a/app/pe/moys/pe_moytag.py +++ b/app/pe/moys/pe_moytag.py @@ -16,8 +16,8 @@ class MoyennesTag: self, tag: str, type_moyenne: str, - matrice_notes_gen: pd.DataFrame, # etudids x colonnes - matrice_coeffs: pd.DataFrame, # etudids x colonnes + matrice_notes: pd.DataFrame, # etudids x UEs|comp + matrice_coeffs: pd.DataFrame, # etudids x UEs|comp ): """Classe centralisant la synthèse des moyennes/classements d'une série d'étudiants à un tag donné, en différenciant les notes @@ -26,9 +26,10 @@ class MoyennesTag: Args: tag: Un tag - matrice_notes_gen: Les moyennes (etudid x acronymes_ues ou etudid x compétences) + matrice_notes: Les moyennes (etudid x acronymes_ues|compétences) aux différentes UEs ou compétences - # notes_gen: Une série de notes (moyenne) sous forme d'un ``pd.Series`` (toutes UEs confondues) + matrice_coeffs: Les coeffs (etudid x acronymes_ues|compétences) + aux différentes UEs ou compétences """ self.tag = tag """Le tag associé aux moyennes""" @@ -37,31 +38,35 @@ class MoyennesTag: """Le type de moyennes (par UEs ou par compétences)""" # Les moyennes par UE/compétences (ressources/SAEs confondues) - self.matrice_notes_gen: pd.DataFrame = matrice_notes_gen - """Les notes par UEs ou Compétences (DataFrame)""" + self.matrice_notes: pd.DataFrame = matrice_notes + """Les notes par UEs ou Compétences (DataFrame etudids x UEs|comp)""" - self.matrice_coeffs_moy_gen: pd.DataFrame = matrice_coeffs + self.matrice_coeffs: 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_gen: dict[int, pd.DataFrame] = {} - """Dataframes retraçant les moyennes/classements/statistiques des étudiants aux UEs""" + self.champs: list[str] = list(self.matrice_notes.columns) + """Les champs (acronymes d'UE ou compétences) renseignés dans les moyennes""" + assert len(self.champs) == len( + set(self.champs) + ), "Des champs de moyennes en doublons" - self.etudids = self.matrice_notes_gen.index + self.etudids: list[int] = list(self.matrice_notes.index) """Les étudids renseignés dans les moyennes""" - self.champs = self.matrice_notes_gen.columns - """Les champs (acronymes d'UE ou compétences) renseignés dans les moyennes""" + self.moyennes_dict: dict[str, pe_moy.Moyenne] = {} + """Dictionnaire associant à chaque UE|Compétence ses données moyenne/class/stat""" for col in self.champs: # if ue.type != UE_SPORT: # Les moyennes tous modules confondus - notes = matrice_notes_gen[col] - self.moyennes_gen[col] = pe_moy.Moyenne(notes) + notes = matrice_notes[col] + self.moyennes_dict[col] = pe_moy.Moyenne(notes) # Les moyennes générales (toutes UEs confondues) - self.notes_gen = pd.Series(np.nan, index=self.matrice_notes_gen.index) + self.notes_gen = pd.Series(np.nan, index=self.matrice_notes.index) + """Notes de la moyenne générale (toutes UEs|Comp confondues)""" if self.has_notes(): self.notes_gen = self.compute_moy_gen( - self.matrice_notes_gen, self.matrice_coeffs_moy_gen + self.matrice_notes, self.matrice_coeffs ) self.moyenne_gen = pe_moy.Moyenne(self.notes_gen) """Dataframe retraçant les moyennes/classements/statistiques général (toutes UESs confondues et modules confondus)""" @@ -73,23 +78,26 @@ class MoyennesTag: Returns: True si la moytag a des notes, False sinon """ - notes = self.matrice_notes_gen + for col, moy in self.moyennes_dict.items(): + if not moy.has_notes(): + return False + return True + # 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 + # 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). + """Calcule la moyenne générale (toutes UE/compétences confondus), en pondérant + les notes obtenues aux UEs|Compétences par les coeff (ici les crédits ECTS). Args: - moys: Les moyennes etudids x acronymes_ues/compétences - coeff: Les coeff etudids x ueids/compétences + moys: Les moyennes (etudids x acronymes_ues/compétences) + coeff: Les coeff (etudids x acronymes_ues/compétences) """ # Calcule la moyenne générale dans le semestre (pondérée par le ECTS) @@ -100,36 +108,39 @@ class MoyennesTag: # formation_id=self.formsemestre.formation_id, skip_empty_ues=True, ) + return moy_gen_tag 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, options={"min_max_moy": True} ) -> pd.DataFrame: - """Renvoie le df synthétisant l'ensemble des données - connues + """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). + + Args: + aggregat: Le nom de l'aggrégat (éventuellement `None` si non connu) + cohorte: La cohorte Groupe ou Promo (éventuellement `None` si non connue) """ if "min_max_moy" not in options or options["min_max_moy"]: with_min_max_moy = True else: with_min_max_moy = False + # Les étudiants triés par etudid etudids_sorted = sorted(self.etudids) + # Le dataFrame à générer 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_gen[champ].get_df_synthese( - with_min_max_moy=with_min_max_moy - ) # le dataframe + moy: pe_moy.Moyenne = self.moyennes_dict[champ] + df_champ = moy.to_df(with_min_max_moy=with_min_max_moy) # le dataframe # Renomme les colonnes cols = [ @@ -142,7 +153,7 @@ class MoyennesTag: df = df.join(df_champ) # Ajoute la moy générale - df_moy_gen = self.moyenne_gen.get_df_synthese(with_min_max_moy=with_min_max_moy) + df_moy_gen = self.moyenne_gen.to_df(with_min_max_moy=with_min_max_moy) cols = [ get_colonne_df(aggregat, self.tag, CHAMP_GENERAL, cohorte, critere) for critere in pe_moy.Moyenne.get_colonnes_synthese( @@ -157,7 +168,23 @@ class MoyennesTag: 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""" + utilisé pour désigner les colonnes du df. + + Args: + aggregat: Un nom d'aggrégat (généralement "S1" ou "3S") + pouvant être optionnel (si `None`) + tag: Un nom de tags (par ex. "maths") + champ: Un nom d'UE ou de compétences + cohorte: Une cohorte pour les interclassements (généralement + Groupe ou Promo + pouvant être optionnel (si `None`) + critere: Un critère correspondant à l'une des colonnes + d'une pe_moy.Moyenne + Returns: + Une chaine de caractères indiquant les champs séparés par + un ``"|"``, généralement de la forme + "S1|maths|UE|Groupe|note" + """ liste_champs = [] if aggregat != None: liste_champs += [aggregat] diff --git a/app/pe/moys/pe_rcstag.py b/app/pe/moys/pe_rcstag.py index 1ff960cc..aa82392a 100644 --- a/app/pe/moys/pe_rcstag.py +++ b/app/pe/moys/pe_rcstag.py @@ -229,8 +229,8 @@ class RCSemXTag(pe_tabletags.TableTag): # 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 + notes = moys_tag.matrice_notes + coeffs_moy_gen = moys_tag.matrice_coeffs # les coeffs coeffs_rcues = sxtag.coefs_rcue # dictionnaire UE -> coeff # Traduction des acronymes d'UE en compétences diff --git a/app/pe/moys/pe_sxtag.py b/app/pe/moys/pe_sxtag.py index bb13a0f7..6c11ea1a 100644 --- a/app/pe/moys/pe_sxtag.py +++ b/app/pe/moys/pe_sxtag.py @@ -274,7 +274,7 @@ class SxTag(pe_tabletags.TableTag): # Charge les notes du semestre tag sem_tag = self.ressembuttags[frmsem_id] moys_tag = sem_tag.moyennes_tags[tag] - notes = moys_tag.matrice_notes_gen # dataframe etudids x ues + notes = moys_tag.matrice_notes # dataframe etudids x ues # les étudiants et les acronymes communs etudids_communs, acronymes_communs = pe_comp.find_index_and_columns_communs( diff --git a/app/pe/moys/pe_tabletags.py b/app/pe/moys/pe_tabletags.py index 6c5ecbe4..0ea7747d 100644 --- a/app/pe/moys/pe_tabletags.py +++ b/app/pe/moys/pe_tabletags.py @@ -76,8 +76,6 @@ class TableTag(object): 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) @@ -100,9 +98,9 @@ class TableTag(object): Args: administratif: Indique si les données administratives sont incluses - aggregat: l'aggrégat représenté + aggregat: l'aggrégat représenté (éventuellement `None` si non connu) tags_cibles: la liste des tags ciblés - cohorte: la cohorte représentée + cohorte: la cohorte représentée (éventuellement `None` si non connue) Returns: Le dataframe complet de synthèse """ @@ -112,8 +110,6 @@ class TableTag(object): 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=aggregat, cohorte=cohorte) diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 6e380850..d06df6c4 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -99,9 +99,9 @@ class JuryPE(object): "moyennes_tags": True, "moyennes_ue_res_sae": True, "moyennes_ues_rcues": True, - "min_max_moy": False, - "publipostage": False, - "classeurs_detailles": False, + "min_max_moy": True, # par défaut: False + "publipostage": True, # par défaut: False + "classeurs_detailles": True, # par défaut: False }, ): pe_affichage.pe_start_log() diff --git a/tests/unit/test_pe.py b/tests/unit/test_pe.py index 23c9779f..73e3cf03 100644 --- a/tests/unit/test_pe.py +++ b/tests/unit/test_pe.py @@ -7,8 +7,227 @@ import pytest from tests.unit import setup from app import db - +import pandas as pd +import app.pe.moys.pe_moy as pe_moy import app.pe.moys.pe_rcstag as pe_rcstag +import app.pe.moys.pe_tabletags as pe_tabletags +import app.pe.moys.pe_moytag as pe_moytag + + +def egalite_df(df1, df2): + return ((df1 == df2) | (np.isnan(df1) & np.isnan(df2))).all() + + +# ****************************** +# app.pe.moys.pe_moy +# ****************************** + + +class Test_pe_moy: + def test_init(self): + """Test de pe_moy.Moyenne.__init__""" + notes = pd.Series({1: 10.0, 2: 14.0, 3: np.nan, 4: 0.0}) + moy = pe_moy.Moyenne(notes) + assert moy.etudids == [1, 2, 3, 4], "Etudids incorrect" + assert moy.inscrits_ids == [1, 2, 4], "Inscriptions incorrectes" + # Les notes + notes_attendues = pd.Series([10.0, 14.0, np.nan, 0.0], index=[1, 2, 3, 4]) + assert "note" in moy.df.columns, "Colonne manquante" + assert egalite_df(moy.df["note"], notes_attendues), "Notes incorrecte" + # Les étuds + nb_etuds = pd.Series([4] * 4, index=[1, 2, 3, 4]) + assert "nb_etuds" in moy.df.columns, "Colonne manquante" + assert egalite_df(moy.df["nb_etuds"], nb_etuds) + # Les inscrits + nb_inscrits = pd.Series([3, 3, np.nan, 3], index=[1, 2, 3, 4]) + assert "nb_inscrits" in moy.df.columns, "Colonne manquante" + assert egalite_df(moy.df["nb_inscrits"], nb_inscrits) + # Les classements + classement = pd.Series([2.0, 1.0, np.nan, 3.0], index=[1, 2, 3, 4]) + assert "classement" in moy.df, "Colonne manquante" + assert egalite_df(moy.df["classement"], classement), "Classements incorrects" + # Les rangs + rang = pd.Series(["2/3", "1/3", "nan", "3/3"], index=[1, 2, 3, 4]) + assert "rang" in moy.df, "Colonne manquante" + assert moy.df["rang"].isnull().sum() == 0, "Des Nan dans les rangs interdits" + assert (moy.df["rang"] == rang).any(), "Rangs incorrects" + # Les mins + assert "min" in moy.df, "Colonne manquante" + mina = pd.Series([0.0, 0.0, np.nan, 0.0], index=[1, 2, 3, 4]) + assert egalite_df(moy.df["min"], mina), "Min incorrect" + # Les max + assert "max" in moy.df, "Colonne manquante" + maxa = pd.Series([14.0, 14.0, np.nan, 14.0], index=[1, 2, 3, 4]) + assert egalite_df(moy.df["max"], maxa), "Max incorrect" + # Les moy + assert "moy" in moy.df, "Colonne manquante" + moya = pd.Series([8.0, 8.0, np.nan, 8.0], index=[1, 2, 3, 4]) + assert egalite_df(moy.df["moy"], moya), "Moy incorrect" + + def test_init_ex_aequo(self): + """Test de pe_moy.Moyenne.__init__ pour des ex-aequo""" + notes = pd.Series({1: 10.0, 2: 14.0, 3: 10.0, 4: 0.0}) + moy = pe_moy.Moyenne(notes) + # Les rangs + rang = pd.Series(["2 ex/4", "1/4", "2 ex/4", "3/4"], index=[1, 2, 3, 4]) + assert moy.df["rang"].isnull().sum() == 0, "Des Nan dans les rangs interdits" + assert (moy.df["rang"] == rang).any(), "Rangs incorrects" + + @pytest.mark.parametrize( + "notes, resultat", + [ + pytest.param( + pd.Series({1: 10.0, 2: 14.0, 3: np.nan, 4: 0.0}), True, id="avec_notes" + ), + pytest.param(pd.Series({1: np.nan, 2: np.nan}), False, id="sans_note"), + pytest.param(pd.Series({1: 0.0, 2: np.nan}), True, id="avec 0"), + ], + ) + def test_has_notes(self, notes, resultat): + moy = pe_moy.Moyenne(notes) + assert ( + moy.has_notes() == resultat + ), "Le test sur la présence de notes est incorrect" + + +# ****************************** +# app.pe.moys.pe_moytag +# ****************************** +class Test_pe_moytag: + @pytest.mark.parametrize( + "aggregat, tag, champ, cohorte, critere, attendu", + [ + pytest.param( + "S1", "Math", "UE", "Gr", "note", "S1|Math|UE|Gr|note", id="tous_args" + ), + pytest.param( + None, + "Math", + "UE", + "Gr", + "note", + "Math|UE|Gr|note", + id="aggregat manquant", + ), + pytest.param( + None, + "Math", + "UE", + None, + "note", + "Math|UE|note", + id="aggregat et cohorte manquant", + ), + ], + ) + def test_colonnes_df(self, aggregat, tag, champ, cohorte, critere, attendu): + descr = pe_moytag.get_colonne_df(aggregat, tag, champ, cohorte, critere) + assert descr == attendu, "Nom de colonne incorrect" + + def test_moyennes_tag__init__(self): + matrice_notes = pd.DataFrame.from_dict( + { + 1: [12.0, 14.0, 15.0], + 2: [8.0, np.nan, 12.0], + 3: [0.0, 11.0, 13.0], + 4: [np.nan, np.nan, np.nan], + 5: [np.nan, np.nan, np.nan], + 6: [0.0, 0.0, 0.0], + }, + orient="index", + columns=["UE1.1", "UE1.2", "UE1.3"], + ) + matrice_coeffs = pd.DataFrame.from_dict( + { + 1: [1, 2, 3], + 2: [2, 10, 6], + 3: [1, 2, np.nan], + 4: [5, 4, 3], + 5: [np.nan, np.nan, np.nan], + 6: [1, 1, 1], + }, + orient="index", + columns=["UE1.1", "UE1.2", "UE1.3"], + ) + moy_tag = pe_moytag.MoyennesTag("maths", None, matrice_notes, matrice_coeffs) + attendu = pd.Series( + [ + (12 * 1 + 14 * 2 + 15 * 3) / (1 + 2 + 3), + (8 * 2 + 12 * 6) / (2 + 6), + (0 * 1 + 2 * 11) / (1 + 2), + np.nan, + np.nan, + 0, + ], + index=[1, 2, 3, 4, 5, 6], + ) + assert egalite_df(moy_tag.notes_gen, attendu), "La moyenne n'est pas correcte" + + def test_to_df(self): + matrice_notes = pd.DataFrame.from_dict( + { + 1: [12.0, 14.0, 15.0], + 2: [8.0, np.nan, 12.0], + 3: [0.0, 11.0, 13.0], + 4: [np.nan, np.nan, np.nan], + 5: [np.nan, np.nan, np.nan], + 6: [0.0, 0.0, 0.0], + }, + orient="index", + columns=["UE1.1", "UE1.2", "UE1.3"], + ) + matrice_coeffs = pd.DataFrame.from_dict( + { + 1: [1, 2, 3], + 2: [2, 10, 6], + 3: [1, 2, np.nan], + 4: [5, 4, 3], + 5: [np.nan, np.nan, np.nan], + 6: [1, 1, 1], + }, + orient="index", + columns=["UE1.1", "UE1.2", "UE1.3"], + ) + moy_tag = pe_moytag.MoyennesTag("maths", None, matrice_notes, matrice_coeffs) + + def test_to_df(self): + """Test le dataframe de synthèse""" + matrice_notes = pd.DataFrame.from_dict( + { + 2: [13.0, 13.0, 13], + 1: [12.0, 14.0, 15.0], + }, + orient="index", + columns=["UE1.1", "UE1.2", "UE1.3"], + ) + matrice_coeffs = pd.DataFrame.from_dict( + { + 2: [1, 2, 3], + 1: [1, 2, 3], + }, + orient="index", + columns=["UE1.1", "UE1.2", "UE1.3"], + ) + moy_tag = pe_moytag.MoyennesTag("maths", None, matrice_notes, matrice_coeffs) + synthese = moy_tag.to_df( + aggregat="S1", cohorte="groupe", options={"min_max_moy": True} + ) + colonnes_attendues = [] + for ue in ["UE1.1", "UE1.2", "UE1.3", "Général"]: + for champ in ["note", "rang", "min", "max", "moy"]: + colonnes_attendues += [f"S1|maths|{ue}|groupe|{champ}"] + assert ( + list(synthese.columns) == colonnes_attendues + ), "Les colonnes de synthèse ne sont pas correctes" + assert list(synthese.index) == [ + 1, + 2, + ], "Les lignes ne sont pas triées par id d'étudiants" + + +# ****************************** +# app.pe.moys.pe_rcstag +# ****************************** @pytest.mark.parametrize(