This commit is contained in:
Emmanuel Viennet 2024-02-04 00:09:11 +01:00
commit 9e13b51669
7 changed files with 392 additions and 301 deletions

View File

@ -349,8 +349,8 @@ class EtudiantsJuryPE:
trajectoire = trajectoire_aggr[aggregat]
if trajectoire:
# Le semestre terminal de l'étudiant de l'aggrégat
fid = trajectoire.semestre_final.formsemestre_id
formsemestres_terminaux[fid] = trajectoire.semestre_final
fid = trajectoire.formsemestre_final.formsemestre_id
formsemestres_terminaux[fid] = trajectoire.formsemestre_final
return formsemestres_terminaux
def get_formsemestres(self, semestres_recherches=None):

View File

@ -1,5 +1,5 @@
from app.comp import moy_sem
from app.pe.pe_tabletags import TableTag
from app.pe.pe_tabletags import TableTag, MoyenneTag
from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_trajectoire import Trajectoire, TrajectoiresJuryPE
from app.pe.pe_trajectoiretag import TrajectoireTag
@ -10,7 +10,6 @@ import numpy as np
class AggregatInterclasseTag(TableTag):
# -------------------------------------------------------------------------------------------------------------------
def __init__(
self,
@ -30,24 +29,30 @@ class AggregatInterclasseTag(TableTag):
"""
TableTag.__init__(self)
# Le nom
self.aggregat = nom_aggregat
"""Aggrégat de l'interclassement"""
self.nom = self.get_repr()
"""Les étudiants diplômés et leurs trajectoires (cf. trajectoires.suivis)""" # TODO
self.diplomes_ids = etudiants.etudiants_diplomes
self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids}
# pour les exports sous forme de dataFrame
self.etudiants = {etudid: etudiants.identites[etudid].etat_civil for etudid in self.diplomes_ids}
self.etudiants = {
etudid: etudiants.identites[etudid].etat_civil
for etudid in self.diplomes_ids
}
# Les trajectoires (et leur version tagguées), en ne gardant que celles associées à l'aggrégat
self.trajectoires: dict[int, Trajectoire] = {}
"""Ensemble des trajectoires associées à l'aggrégat"""
for trajectoire_id in trajectoires_jury_pe.trajectoires:
trajectoire = trajectoires_jury_pe.trajectoires[trajectoire_id]
if trajectoire_id[0] == nom_aggregat:
self.trajectoires[trajectoire_id] = trajectoire
self.trajectoires_taggues: dict[int, Trajectoire] = {}
"""Ensemble des trajectoires tagguées associées à l'aggrégat"""
for trajectoire_id in self.trajectoires:
self.trajectoires_taggues[trajectoire_id] = trajectoires_taggues[
trajectoire_id
@ -56,26 +61,27 @@ class AggregatInterclasseTag(TableTag):
# Les trajectoires suivies par les étudiants du jury, en ne gardant que
# celles associées aux diplomés
self.suivi: dict[int, Trajectoire] = {}
"""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] = trajectoires_jury_pe.suivi[etudid][nom_aggregat]
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 = {}
self.moyennes_tags: dict[str, MoyenneTag] = {}
for tag in self.tags_sorted:
moy_gen_tag = self.notes[tag]
self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag)
self.moyennes_tags[tag] = MoyenneTag(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.aggregat}"
@ -118,7 +124,4 @@ class AggregatInterclasseTag(TableTag):
etudids_communs, tags_communs
]
# Force les nan
df.fillna(np.nan)
return df

View File

@ -65,35 +65,15 @@ import pandas as pd
class JuryPE(object):
"""Classe mémorisant toutes les informations nécessaires pour établir un jury de PE.
Modèle basé sur NotesTable.
Attributs :
* diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX)
* juryEtudDict : dictionnaire récapitulant les étudiants participant au jury PE (données administratives +
celles des semestres valides à prendre en compte permettant le calcul des moyennes ...
``{'etudid : { 'nom', 'prenom', 'civilite', 'diplome', '', }}``
a
Rq: il contient à la fois les étudiants qui vont être diplomés à la date prévue
et ceux qui sont éliminés (abandon, redoublement, ...) pour affichage alternatif
"""
# Variables de classe décrivant les aggrégats, leur ordre d'apparition temporelle et
# leur affichage dans les avis latex
# ------------------------------------------------------------------------------------------------------------------
def __init__(self, diplome):
"""
Création d'une table PE sur la base d'un semestre selectionné. De ce semestre est déduit :
Classe mémorisant toutes les informations nécessaires pour établir un jury de PE, sur la base
d'une année de diplôme. De ce semestre est déduit :
1. l'année d'obtention du DUT,
2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés.
Args:
sem_base: le FormSemestre donnant le semestre à la base du jury PE
semBase: le dictionnaire sem donnant la base du jury (CB: TODO: A supprimer à long term)
meme_programme: si True, impose un même programme pour tous les étudiants participant au jury,
si False, permet des programmes differents
diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX)
"""
self.diplome = diplome
"L'année du diplome"
@ -101,7 +81,7 @@ class JuryPE(object):
self.nom_export_zip = f"Jury_PE_{self.diplome}"
"Nom du zip où ranger les fichiers générés"
# Chargement des étudiants à prendre en compte dans le jury
# Chargement des étudiants à prendre en compte Sydans le jury
pe_affichage.pe_print(
f"""*** Recherche et chargement des étudiants diplômés en {
self.diplome}"""
@ -122,7 +102,6 @@ class JuryPE(object):
self._gen_xls_synthese_jury_par_tag(zipfile)
self._gen_xls_synthese_par_etudiant(zipfile)
# Fin !!!! Tada :)
def _gen_xls_diplomes(self, zipfile: ZipFile):
@ -342,48 +321,131 @@ class JuryPE(object):
"""
etudids = list(self.diplomes_ids)
aggregats = pe_comp.TOUS_LES_PARCOURS
donnees = {}
# Les données des étudiants
donnees_etudiants = {}
for etudid in etudids:
etudiant = self.etudiants.identites[etudid]
donnees[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")
for aggregat in aggregats:
# Le dictionnaire par défaut des moyennes
donnees[etudid] |= get_defaut_dict_synthese_aggregat(aggregat, self.diplome)
# Ajout des aggrégats
aggregats = pe_comp.TOUS_LES_PARCOURS
# La trajectoire de l'étudiant sur l'aggrégat
for aggregat in aggregats:
descr = pe_comp.PARCOURS[aggregat]["descr"]
# 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.trajectoires.suivi[etudid][aggregat]
if trajectoire:
trajectoire_tagguee = self.trajectoires_tagguees[
trajectoire.trajectoire_id
]
else:
trajectoire_tagguee = None
tid = trajectoire.trajectoire_id
trajectoire_tagguee = self.trajectoires_tagguees[tid]
if (
tag in trajectoire_tagguee.moyennes_tags
and trajectoire_tagguee not in trajectoires_tagguees
):
trajectoires_tagguees.append(trajectoire_tagguee)
# L'interclassement
# Ajout des notes
notes = pd.DataFrame(index=etudids, columns=[ [descr], [""], ["note"] ])
nbre_notes_injectees = 0
for traj in trajectoires_tagguees:
moy_traj = traj.moyennes_tags[tag]
notes_traj = moy_traj.get_df_notes(arrondi=True)
etudids_communs = notes_traj.index.intersection(etudids)
nbre_notes_injectees += len(etudids_communs)
notes.loc[etudids_communs, (descr, "", "note")] = notes_traj.loc[etudids_communs, "notes"]
# Si l'aggrégat est significatif (aka il y a des notes)
if nbre_notes_injectees > 0:
df_synthese = df_synthese.join(notes)
# Ajout des classements & statistiques
donnees = pd.DataFrame(
index=etudids,
columns=[ [descr]*4, [NOM_STAT_GROUPE]*4, ["class.", "min", "moy", "max"] ],
)
# donnees[(descr, NOM_STAT_GROUPE, "class.")] = donnees[
# (descr, NOM_STAT_GROUPE, "class.")
# ].astype(str)
# donnees[(descr, NOM_STAT_GROUPE, "class.")] = np.nan
for traj in trajectoires_tagguees:
moy_traj = traj.moyennes_tags[tag]
# Les classements
rangs = moy_traj.get_df_rangs_pertinents()
# Les etudids communs pour la trajectoire
etudids_communs = rangs.index.intersection(etudids)
donnees.loc[
etudids_communs, (descr, NOM_STAT_GROUPE, "class.")
] = rangs.loc[etudids_communs, "rangs"]
# Le min
donnees.loc[
etudids_communs, (descr, NOM_STAT_GROUPE, "min")
] = moy_traj.get_min_for_df()
# Le max
donnees.loc[
etudids_communs, (descr, NOM_STAT_GROUPE, "max")
] = moy_traj.get_max_for_df()
# La moyenne
donnees.loc[
etudids_communs, (descr, NOM_STAT_GROUPE, "moy")
] = moy_traj.get_moy_for_df()
df_synthese = df_synthese.join(donnees)
# Ajoute les données d'interclassement
interclass = self.interclassements_taggues[aggregat]
moy_traj = interclass.moyennes_tags[tag]
# Injection des données dans un dictionnaire
donnees[etudid] |= get_dict_synthese_aggregat(aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome)
nom_stat_promo = f"{NOM_STAT_PROMO} {self.diplome}"
donnees = pd.DataFrame(
index=etudids,
columns=[ [descr]*4, [nom_stat_promo]*4, ["class.", "min", "moy", "max"] ],
)
# Les classements
rangs = moy_traj.get_df_rangs_pertinents()
etudids_communs = rangs.index.intersection(etudids)
donnees.loc[
etudids_communs, (descr, nom_stat_promo, "class.")
] = rangs.loc[etudids_communs, "rangs"]
# Le min
donnees.loc[
etudids_communs, (descr, nom_stat_promo, "min")
] = moy_traj.get_min_for_df()
# Le max
donnees.loc[
etudids_communs, (descr, nom_stat_promo, "max")
] = moy_traj.get_max_for_df()
# La moyenne
donnees.loc[
etudids_communs, (descr, nom_stat_promo, "moy")
] = moy_traj.get_moy_for_df()
df_synthese = df_synthese.join(donnees)
# Fin de l'aggrégat
# Construction du dataFrame
df = pd.DataFrame.from_dict(donnees, orient="index")
# Tri par nom/prénom
df.sort_values(
df_synthese.sort_values(
by=[("Identité", "", "Nom"), ("Identité", "", "Prenom")], inplace=True
)
return df
return df_synthese
def synthetise_jury_par_etudiants(self) -> dict[pd.DataFrame]:
"""Synthétise tous les résultats du jury PE dans des dataframes,
@ -424,7 +486,9 @@ class JuryPE(object):
for aggregat in aggregats:
# Le dictionnaire par défaut des moyennes
donnees[tag] |= get_defaut_dict_synthese_aggregat(aggregat, self.diplome)
donnees[tag] |= get_defaut_dict_synthese_aggregat(
aggregat, self.diplome
)
# La trajectoire de l'étudiant sur l'aggrégat
trajectoire = self.trajectoires.suivi[etudid][aggregat]
@ -432,26 +496,29 @@ class JuryPE(object):
trajectoire_tagguee = self.trajectoires_tagguees[
trajectoire.trajectoire_id
]
else:
trajectoire_tagguee = None
# L'interclassement
interclass = self.interclassements_taggues[aggregat]
# Injection des données dans un dictionnaire
donnees[tag] |= get_dict_synthese_aggregat(aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome)
if tag in trajectoire_tagguee.moyennes_tags:
# L'interclassement
interclass = self.interclassements_taggues[aggregat]
# Injection des données dans un dictionnaire
donnees[tag] |= get_dict_synthese_aggregat(
aggregat,
trajectoire_tagguee,
interclass,
etudid,
tag,
self.diplome,
)
# Fin de l'aggrégat
# Construction du dataFrame
df = pd.DataFrame.from_dict(donnees, orient="index")
# Tri par nom/prénom
df.sort_values(
by=[("", "", "tag")], inplace=True
)
df.sort_values(by=[("", "", "tag")], inplace=True)
return df
def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
"""Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés.
Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire
@ -587,7 +654,7 @@ def get_dict_synthese_aggregat(
interclassement_taggue: AggregatInterclasseTag,
etudid: int,
tag: str,
diplome: int
diplome: int,
):
"""Renvoie le dictionnaire (à intégrer au tableur excel de synthese)
traduisant les résultats (moy/class) d'un étudiant à une trajectoire tagguée associée
@ -600,66 +667,43 @@ def get_dict_synthese_aggregat(
note = np.nan
# Les données de la trajectoire tagguée pour le tag considéré
if trajectoire_tagguee and tag in trajectoire_tagguee.moyennes_tags:
bilan = trajectoire_tagguee.moyennes_tags[tag]
moy_tag = trajectoire_tagguee.moyennes_tags[tag]
# La moyenne de l'étudiant
note = TableTag.get_note_for_df(bilan, etudid)
# Les données de l'étudiant
note = moy_tag.get_note_for_df(etudid)
# Statistiques sur le groupe
if not pd.isna(note) and note != np.nan:
# Les moyennes de cette trajectoire
donnees |= {
(descr, "", "note"): note,
(
descr,
NOM_STAT_GROUPE,
"class.",
): TableTag.get_class_for_df(bilan, etudid),
(
descr,
NOM_STAT_GROUPE,
"min",
): TableTag.get_min_for_df(bilan),
(
descr,
NOM_STAT_GROUPE,
"moy",
): TableTag.get_moy_for_df(bilan),
(
descr,
NOM_STAT_GROUPE,
"max",
): TableTag.get_max_for_df(bilan),
}
classement = moy_tag.get_class_for_df(etudid)
nmin = moy_tag.get_min_for_df()
nmax = moy_tag.get_max_for_df()
nmoy = moy_tag.get_moy_for_df()
# Statistiques sur le groupe
if not pd.isna(note) and note != np.nan:
# Les moyennes de cette trajectoire
donnees |= {
(descr, "", "note"): note,
(descr, NOM_STAT_GROUPE, "class."): classement,
(descr, NOM_STAT_GROUPE, "min"): nmin,
(descr, NOM_STAT_GROUPE, "moy"): nmoy,
(descr, NOM_STAT_GROUPE, "max"): nmax,
}
# L'interclassement
if tag in interclassement_taggue.moyennes_tags:
bilan = interclassement_taggue.moyennes_tags[tag]
moy_tag = interclassement_taggue.moyennes_tags[tag]
if not pd.isna(note) and note != np.nan:
nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}"
classement = moy_tag.get_class_for_df(etudid)
nmin = moy_tag.get_min_for_df()
nmax = moy_tag.get_max_for_df()
nmoy = moy_tag.get_moy_for_df()
if not pd.isna(note) and note != np.nan:
nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}"
donnees |= {
(descr, nom_stat_promo, "class."): classement,
(descr, nom_stat_promo, "min"): nmin,
(descr, nom_stat_promo, "moy"): nmoy,
(descr, nom_stat_promo, "max"): nmax,
}
donnees |= {
(
descr,
nom_stat_promo,
"class.",
): TableTag.get_class_for_df(bilan, etudid),
(
descr,
nom_stat_promo,
"min",
): TableTag.get_min_for_df(bilan),
(
descr,
nom_stat_promo,
"moy",
): TableTag.get_moy_for_df(bilan),
(
descr,
nom_stat_promo,
"max",
): TableTag.get_max_for_df(bilan),
}
return donnees

View File

@ -38,7 +38,7 @@ Created on Fri Sep 9 09:15:05 2016
import numpy as np
import app.pe.pe_etudiant
from app import db, log
from app import db, log, ScoValueError
from app.comp import res_sem, moy_ue, moy_sem
from app.comp.moy_sem import comp_ranks_series
from app.comp.res_compat import NotesTableCompat
@ -49,7 +49,7 @@ from app.models.moduleimpls import ModuleImpl
from app.scodoc import sco_tag_module
from app.scodoc.codes_cursus import UE_SPORT
import app.pe.pe_affichage as pe_affichage
from app.pe.pe_tabletags import TableTag, TAGS_RESERVES
from app.pe.pe_tabletags import TableTag, TAGS_RESERVES, MoyenneTag
import pandas as pd
@ -94,44 +94,51 @@ class SemestreTag(TableTag):
# Les tags :
## Saisis par l'utilisateur
self.tags_personnalises = get_synthese_tags_personnalises_semestre(
tags_personnalises = get_synthese_tags_personnalises_semestre(
self.nt.formsemestre
)
## Déduit des compétences
self.tags_competences = get_noms_competences_from_ues(self.nt.formsemestre)
dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre)
# Supprime les doublons dans les tags
tags_reserves = TAGS_RESERVES + list(self.tags_competences.values())
for tag in self.tags_personnalises:
if tag in tags_reserves:
del self.tags_personnalises[tag]
pe_affichage.pe_print(f"Supprime le tag {tag}")
self.tags = (
list(tags_personnalises.keys())
+ list(dict_ues_competences.values())
+ ["but"]
)
"""Tags du semestre taggué"""
## Vérifie l'unicité des tags
if len(set(self.tags)) != len(self.tags):
raise ScoValueError(
f"""Erreur dans le module PE : L'un des tags saisis dans le programme
fait parti des tags réservés (par ex. "comp. <titre_compétence>"). Modifiez les
tags de votre programme"""
)
# Calcul des moyennes & les classements de chaque étudiant à chaque tag
self.moyennes_tags = {}
for tag in self.tags_personnalises:
for tag in tags_personnalises:
# pe_affichage.pe_print(f" -> Traitement du tag {tag}")
moy_gen_tag = self.compute_moyenne_tag(tag)
self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag)
moy_gen_tag = self.compute_moyenne_tag(tag, tags_personnalises)
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
# Ajoute les moyennes générales de BUT pour le semestre considéré
moy_gen_but = self.nt.etud_moy_gen
moy_gen_but = pd.to_numeric(moy_gen_but, errors="coerce")
self.moyennes_tags["but"] = self.comp_moy_et_stat(moy_gen_but)
self.moyennes_tags["but"] = MoyenneTag("but", moy_gen_but)
# Ajoute les moyennes par compétence
for ue_id, competence in self.tags_competences.items():
for ue_id, competence in dict_ues_competences.items():
moy_ue = self.nt.etud_moy_ue[ue_id]
self.moyennes_tags[competence] = self.comp_moy_et_stat(moy_ue)
self.moyennes_tags[competence] = MoyenneTag(competence, moy_ue)
self.tags_sorted = self.get_all_tags()
"""Tags (personnalisés+compétences) par ordre alphabétique"""
# Synthétise l'ensemble des moyennes dans un dataframe
self.tags_sorted = sorted(
self.moyennes_tags
) # les tags (personnalisés+compétences) par ordre alphabétique
self.notes = (
self.df_notes()
) # Le dataframe synthétique des notes (=moyennes par tag)
self.notes = self.df_notes()
"""Dataframe synthétique des notes par tag"""
pe_affichage.pe_print(
f" => Traitement des tags {', '.join(self.tags_sorted)}"
@ -141,9 +148,10 @@ class SemestreTag(TableTag):
"""Nom affiché pour le semestre taggué"""
return app.pe.pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True)
def compute_moyenne_tag(self, tag: str) -> list:
def compute_moyenne_tag(self, tag: str, tags_infos: dict) -> pd.Series:
"""Calcule la moyenne des étudiants pour le tag indiqué,
pour ce SemestreTag.
pour ce SemestreTag, en ayant connaissance des informations sur
les tags (dictionnaire donnant les coeff de repondération)
Sont pris en compte les modules implémentés associés au tag,
avec leur éventuel coefficient de **repondération**, en utilisant les notes
@ -151,8 +159,8 @@ class SemestreTag(TableTag):
Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
Renvoie les informations sous la forme d'une liste
[ (moy, somme_coeff_normalise, etudid), ...]
Returns:
La série des moyennes
"""
"""Adaptation du mask de calcul des moyennes au tag visé"""
@ -163,13 +171,13 @@ class SemestreTag(TableTag):
"""Désactive tous les modules qui ne sont pas pris en compte pour ce tag"""
for i, modimpl in enumerate(self.formsemestre.modimpls_sorted):
if modimpl.moduleimpl_id not in self.tags_personnalises[tag]:
if modimpl.moduleimpl_id not in tags_infos[tag]:
modimpls_mask[i] = False
"""Applique la pondération des coefficients"""
modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy()
for modimpl_id in self.tags_personnalises[tag]:
ponderation = self.tags_personnalises[tag][modimpl_id]["ponderation"]
for modimpl_id in tags_infos[tag]:
ponderation = tags_infos[tag][modimpl_id]["ponderation"]
modimpl_coefs_ponderes_df[modimpl_id] *= ponderation
"""Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)"""

View File

@ -40,8 +40,10 @@ Created on Thu Sep 8 09:36:33 2016
import datetime
import numpy as np
from app import ScoValueError
from app.comp.moy_sem import comp_ranks_series
from app.pe import pe_affichage
from app.pe.pe_affichage import SANS_NOTE
from app.scodoc import sco_utils as scu
import pandas as pd
@ -49,62 +51,34 @@ import pandas as pd
TAGS_RESERVES = ["but"]
class TableTag(object):
def __init__(self):
"""Classe centralisant différentes méthodes communes aux
SemestreTag, TrajectoireTag, AggregatInterclassTag
class MoyenneTag:
def __init__(self, tag: str, notes: pd.Series):
"""Classe centralisant la synthèse des moyennes/classements d'une série
d'étudiants à un tag donné, en stockant un dictionnaire :
``
{
"notes": la Serie pandas des notes (float),
"classements": la Serie pandas des classements (float),
"min": la note minimum,
"max": la note maximum,
"moy": la moyenne,
"nb_inscrits": le nombre d'étudiants ayant une note,
}
``
Args:
tag: Un tag
note: Une série de notes (moyenne) sous forme d'un pd.Series()
"""
pass
self.tag = tag
"""Le tag associé à la moyenne"""
self.synthese = self.comp_moy_et_stat(notes)
"""La synthèse des notes/classements/statistiques"""
# -----------------------------------------------------------------------------------------------------------
def get_all_tags(self):
"""Liste des tags de la table, triée par ordre alphabétique
Returns:
Liste de tags triés par ordre alphabétique
"""
return sorted(self.moyennes_tags.keys())
def df_moyennes_et_classements(self):
"""Renvoie un dataframe listant toutes les moyennes,
et les classements des étudiants pour tous les tags
"""
etudiants = self.etudiants
df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"])
for tag in self.get_all_tags():
df = df.join(self.moyennes_tags[tag]["notes"].rename(f"Moy {tag}"))
df = df.join(self.moyennes_tags[tag]["classements"].rename(f"Class {tag}"))
return df
def df_notes(self):
"""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 = self.get_all_tags()
if tags:
dict_series = {tag: self.moyennes_tags[tag]["notes"] for tag in tags}
df = pd.DataFrame(dict_series)
return df
else:
return None
def str_tagtable(self):
"""Renvoie une chaine de caractère listant toutes les moyennes,
les rangs des étudiants pour tous les tags."""
etudiants = self.etudiants
df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"])
for tag in self.get_all_tags():
df = df.join(self.moyennes_tags[tag]["notes"].rename(f"moy {tag}"))
df = df.join(self.moyennes_tags[tag]["classements"].rename(f"class {tag}"))
return df.to_csv(sep=";")
def __eq__(self, other):
"""Egalité de deux MoyenneTag lorsque leur tag sont identiques"""
return self.tag == other.tag
def comp_moy_et_stat(self, notes: pd.Series) -> dict:
"""Calcule et structure les données nécessaires au PE pour une série
@ -131,7 +105,7 @@ class TableTag(object):
(_, class_gen_ue_non_nul) = comp_ranks_series(notes_non_nulles)
# Les classements (toutes notes confondues, avec NaN si pas de notes)
class_gen_ue = pd.Series(np.nan, index=notes.index, dtype="Int64")
class_gen_ue = pd.Series(np.nan, index=notes.index) # , dtype="Int64")
class_gen_ue[indices] = class_gen_ue_non_nul[indices]
synthese = {
@ -140,42 +114,115 @@ class TableTag(object):
"min": notes.min(),
"max": notes.max(),
"moy": notes.mean(),
"nb_inscrits": len(indices),
"nb_inscrits": sum(indices),
}
return synthese
@classmethod
def get_min_for_df(cls, bilan: dict) -> float:
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
revoie le min renseigné pour affichage dans un df"""
return round(bilan["min"], 2)
def get_df_notes(self, arrondi=False):
"""Série des notes, arrondies à 2 chiffres après la virgule"""
if arrondi:
serie = self.synthese["notes"].round(2)
else:
serie = self.synthese["notes"]
df = serie.to_frame("notes")
return df
@classmethod
def get_max_for_df(cls, bilan: dict) -> float:
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
renvoie le max renseigné pour affichage dans un df"""
return round(bilan["max"], 2)
def get_df_rangs_pertinents(self) -> pd.Series:
"""Série des rangs classement/nbre_inscrit"""
classement = self.synthese["classements"]
indices = classement[classement.notnull()].index.to_list()
classement_non_nul = classement.loc[indices].to_frame("classements")
classement_non_nul.insert(1, "rangs", np.nan)
@classmethod
def get_moy_for_df(cls, bilan: dict) -> float:
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
renvoie la moyenne renseignée pour affichage dans un df"""
return round(bilan["moy"], 2)
nb_inscrit = self.synthese["nb_inscrits"]
@classmethod
def get_class_for_df(cls, bilan: dict, etudid: int) -> str:
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
renvoie le classement ramené au nombre d'inscrits,
classement_non_nul["rangs"] = classement_non_nul["classements"].astype(int).astype(str) + "/" + str(nb_inscrit)
return classement_non_nul["rangs"].to_frame("rangs")
def get_note_for_df(self, etudid: int):
"""Note d'un étudiant donné par son etudid"""
return round(self.synthese["notes"].loc[etudid], 2)
def get_min_for_df(self) -> float:
"""Min renseigné pour affichage dans un df"""
return round(self.synthese["min"], 2)
def get_max_for_df(self) -> float:
"""Max renseigné pour affichage dans un df"""
return round(self.synthese["max"], 2)
def get_moy_for_df(self) -> float:
"""Moyenne renseignée pour affichage dans un df"""
return round(self.synthese["moy"], 2)
def get_class_for_df(self, etudid: int) -> str:
"""Classement ramené au nombre d'inscrits,
pour un étudiant donné par son etudid"""
classement = bilan['classements'].loc[etudid]
classement = self.synthese["classements"].loc[etudid]
nb_inscrit = self.synthese["nb_inscrits"]
if not pd.isna(classement):
return f"{classement}/{bilan['nb_inscrits']}"
classement = int(classement)
return f"{classement}/{nb_inscrit}"
else:
return pe_affichage.SANS_NOTE
@classmethod
def get_note_for_df(cls, bilan: dict, etudid: int):
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
renvoie la note (moyenne)
pour un étudiant donné par son etudid"""
return round(bilan["notes"].loc[etudid], 2)
def is_significatif(self) -> bool:
"""Indique si la moyenne est significative (c'est-à-dire à des notes)"""
return self.synthese["nb_inscrits"] > 0
class TableTag(object):
def __init__(self):
"""Classe centralisant différentes méthodes communes aux
SemestreTag, TrajectoireTag, AggregatInterclassTag
"""
pass
# -----------------------------------------------------------------------------------------------------------
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 = self.etudiants
df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"])
tags_tries = self.get_all_tags()
for tag in tags_tries:
moy_tag = self.moyennes_tags[tag]
df = df.join(moy_tag.synthese["notes"].rename(f"Moy {tag}"))
df = df.join(moy_tag.synthese["classements"].rename(f"Class {tag}"))
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

View File

@ -6,43 +6,44 @@ from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date
class Trajectoire:
"""Modélise, pour un aggrégat visé (par ex: 'S2', '3S', '2A')
et un ensemble d'étudiants donnés,
la combinaison des formsemestres des étudiants amenant à un semestre
terminal visé.
Si l'aggrégat est un semestre de type Si, elle stocke le (ou les)
formsemestres de numéro i qu'ont suivis l'étudiant pour atteindre le Si
(en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants)
Pour des aggrégats de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie
les semestres que les étudiants ont suivis pour les amener jusqu'au semestre
terminal de la trajectoire (par ex: ici un S3).
Ces semestres peuvent être :
* des S1+S2+S1+S2+S3 si redoublement de la 1ère année
* des S1+S2+(année de césure)+S3 si césure, ...
"""
def __init__(self, nom_aggregat: str, semestre_final: FormSemestre):
"""Modélise un ensemble de formsemestres d'étudiants
amenant à un semestre terminal
amenant à un semestre terminal, au sens d'un aggrégat (par ex: 'S2', '3S', '2A').
Si l'aggrégat est un semestre de type Si, elle stocke le (ou les)
formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si
(en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants)
Pour des aggrégats de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie
les semestres que les étudiants ont suivis pour les amener jusqu'au semestre
terminal de la trajectoire (par ex: ici un S3).
Ces semestres peuvent être :
* des S1+S2+S1+S2+S3 si redoublement de la 1ère année
* des S1+S2+(année de césure)+S3 si césure, ...
Args:
nom_aggregat: Un nom d'aggrégat (par ex: '5S')
semestre_final: Le semestre final de l'aggrégat
"""
self.nom = nom_aggregat
self.semestre_final = semestre_final
self.trajectoire_id = (nom_aggregat, semestre_final.formsemestre_id)
"""Nom de l'aggrégat"""
self.formsemestre_final = semestre_final
"""FormSemestre terminal de la trajectoire"""
self.trajectoire_id = (nom_aggregat, semestre_final.formsemestre_id)
"""Identifiant de la trajectoire"""
"""Les semestres à aggréger"""
self.semestres_aggreges = {}
"""Semestres aggrégés"""
def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]):
"""Ajoute des semestres au semestre à aggréger
"""Ajout de semestres aux semestres à aggréger
Args:
semestres: Dictionnaire ``{fid: FormSemestre(fid)} à ajouter``
semestres: Dictionnaire ``{fid: FormSemestre(fid)}`` à ajouter
"""
self.semestres_aggreges = self.semestres_aggreges | semestres
@ -55,27 +56,30 @@ class Trajectoire:
semestre = self.semestres_aggreges[fid]
noms.append(f"S{semestre.semestre_id}({fid})")
noms = sorted(noms)
repr = f"{self.nom} ({self.semestre_final.formsemestre_id}) {self.semestre_final.date_fin.year}"
repr = f"{self.nom} ({self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}"
if verbose and noms:
repr += " - " + "+".join(noms)
return repr
class TrajectoiresJuryPE:
"""Centralise toutes les trajectoires du jury PE"""
def __init__(self, annee_diplome: int):
"""
"""Classe centralisant toutes les trajectoires des étudiants à prendre
en compte dans un jury PE
Args:
annee_diplome: L'année de diplomation
"""
self.annee_diplome = annee_diplome
"""Toutes les trajectoires possibles"""
self.trajectoires: dict[tuple:Trajectoire] = {}
"""Quelle trajectoires pour quel étudiant :
dictionnaire {etudid: {nom_aggregat: Trajectoire}}"""
"""Ensemble des trajectoires recensées : {(aggregat, fid_terminal): Trajectoire}"""
self.suivi: dict[int:str] = {}
"""Dictionnaire associant, pour chaque étudiant et pour chaque aggrégat,
sa trajectoire : {etudid: {nom_aggregat: Trajectoire}}"""
def cree_trajectoires(self, etudiants: EtudiantsJuryPE):
"""Créé toutes les trajectoires, au regard du cursus des étudiants
@ -122,9 +126,6 @@ class TrajectoiresJuryPE:
"""Mémoire la trajectoire suivie par l'étudiant"""
self.suivi[etudid][nom_aggregat] = trajectoire
"""Vérifications"""
# dernier_semestre_aggregat = get_dernier_semestre_en_date(semestres_aggreges)
# assert dernier_semestre_aggregat == formsemestre_terminal
def get_trajectoires_etudid(trajectoires, etudid):
@ -142,26 +143,3 @@ def get_trajectoires_etudid(trajectoires, etudid):
return liste
def get_semestres_a_aggreger(self, aggregat: str, formsemestre_id_terminal: int):
"""Pour un nom d'aggrégat donné (par ex: 'S3') et un semestre terminal cible
identifié par son formsemestre_id (par ex: 'S3 2022-2023'),
renvoie l'ensemble des semestres à prendre en compte dans
l'aggrégat sous la forme d'un dictionnaire {fid: FormSemestre(fid)}.
Fusionne les cursus individuels des étudiants, dont le cursus correspond
à l'aggrégat visé.
Args:
aggregat: Un aggrégat (par ex. 1A, 2A, 3S, 6S)
formsemestre_id_terminal: L'identifiant du formsemestre terminal de l'aggrégat, devant correspondre au
dernier semestre de l'aggrégat
"""
noms_semestres_aggreges = pe_comp.PARCOURS[aggregat]["aggregat"]
formsemestres = {}
for etudid in self.cursus:
cursus_etudiant = self.cursus[etudid][aggregat]
if formsemestre_id_terminal in cursus_etudiant:
formsemestres_etudiant = cursus_etudiant[formsemestre_id_terminal]
formsemestres = formsemestres | formsemestres_etudiant
return formsemestres

View File

@ -43,7 +43,7 @@ import pandas as pd
import numpy as np
from app.pe.pe_trajectoire import Trajectoire
from app.pe.pe_tabletags import TableTag
from app.pe.pe_tabletags import TableTag, MoyenneTag
class TrajectoireTag(TableTag):
@ -58,18 +58,25 @@ class TrajectoireTag(TableTag):
Par ex: fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S'
Args:
trajectoire: Une trajectoire (aggrégat+semestre terminal)
semestres_taggues: Les données sur les semestres taggués
"""
TableTag.__init__(self)
# La trajectoire associée
self.trajectoire_id = trajectoire.trajectoire_id
"""Identifiant de la trajectoire tagguée"""
self.trajectoire = trajectoire
"""Trajectoire associée à la trajectoire tagguée"""
# Le nom de la trajectoire tagguée (identique à la trajectoire)
self.nom = self.get_repr()
"""Représentation textuelle de la trajectoire tagguée"""
self.formsemestre_terminal = trajectoire.semestre_final
self.formsemestre_terminal = trajectoire.formsemestre_final
"""Le formsemestre terminal"""
# Les résultats du formsemestre terminal
nt = load_formsemestre_results(self.formsemestre_terminal)
@ -100,11 +107,15 @@ class TrajectoireTag(TableTag):
self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted)
"""Calcul les moyennes par tag sous forme d'un dataframe"""
self.moyennes_tags = {}
self.moyennes_tags: dict[str, MoyenneTag] = {}
"""Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)"""
for tag in self.tags_sorted:
moy_gen_tag = self.notes[tag]
self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag)
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
def __eq__(self, other):
"""Egalité de 2 trajectoires tagguées sur la base de leur identifiant"""
return self.trajectoire_id == other.trajectoire_id
def get_repr(self, verbose=False) -> str:
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle