Débute l'aggrégation des moyennes dans des RCS de type Sx (prise en compte de la meilleure des 2 UE en cas de redoublement)

This commit is contained in:
Cléo Baras 2024-02-15 17:05:03 +01:00
parent 267dbb6460
commit 883028216f
10 changed files with 827 additions and 515 deletions

View File

@ -187,7 +187,7 @@ def dept_etudiants(acronym: str):
] ]
""" """
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
return [etud.to_dict_short() for etud in dept.etudiants] return [etud.to_dict_short() for etud in dept.etats_civils]
@bp.route("/departement/id/<int:dept_id>/etudiants") @bp.route("/departement/id/<int:dept_id>/etudiants")
@ -200,7 +200,7 @@ def dept_etudiants_by_id(dept_id: int):
Retourne la liste des étudiants d'un département d'id donné. Retourne la liste des étudiants d'un département d'id donné.
""" """
dept = Departement.query.get_or_404(dept_id) dept = Departement.query.get_or_404(dept_id)
return [etud.to_dict_short() for etud in dept.etudiants] return [etud.to_dict_short() for etud in dept.etats_civils]
@bp.route("/departement/<string:acronym>/formsemestres_ids") @bp.route("/departement/<string:acronym>/formsemestres_ids")

View File

@ -36,7 +36,8 @@ Created on Thu Sep 8 09:36:33 2016
import pandas as pd import pandas as pd
import numpy as np import numpy as np
from app.pe.pe_tabletags import TableTag, MoyenneTag from app.pe.pe_tabletags import TableTag
from app.pe.pe_moytag import MoyennesTag
from app.pe.pe_etudiant import EtudiantsJuryPE from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_rcs import RCS, RCSsJuryPE from app.pe.pe_rcs import RCS, RCSsJuryPE
from app.pe.pe_rcstag import RCSTag from app.pe.pe_rcstag import RCSTag
@ -107,10 +108,10 @@ class RCSInterclasseTag(TableTag):
"""Matrice des notes de l'aggrégat""" """Matrice des notes de l'aggrégat"""
# Synthétise les moyennes/classements par tag # Synthétise les moyennes/classements par tag
self.moyennes_tags: dict[str, MoyenneTag] = {} self.moyennes_tags: dict[str, MoyennesTag] = {}
for tag in self.tags_sorted: for tag in self.tags_sorted:
moy_gen_tag = self.notes[tag] moy_gen_tag = self.notes[tag]
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag) self.moyennes_tags[tag] = MoyennesTag(tag, moy_gen_tag)
# Est significatif ? (aka a-t-il des tags et des notes) # Est significatif ? (aka a-t-il des tags et des notes)
self.significatif = len(self.tags_sorted) > 0 self.significatif = len(self.tags_sorted) > 0

View File

@ -53,9 +53,9 @@ import pandas as pd
from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE 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_affichage as pe_affichage
from app.pe.pe_etudiant import * # TODO A éviter -> pe_etudiant. from app.pe.pe_etudiant import * # TODO A éviter -> pe_etudiant.
from app.pe.pe_rcs import * # TODO A éviter import app.pe.pe_rcs as pe_rcs
from app.pe.pe_rcstag import RCSTag from app.pe.pe_rcstag import RCSTag
from app.pe.pe_semtag import SemestreTag from app.pe.pe_ressemtag import ResSemTag
from app.pe.pe_interclasstag import RCSInterclasseTag from app.pe.pe_interclasstag import RCSInterclasseTag
@ -96,11 +96,11 @@ class JuryPE(object):
pe_affichage.pe_print("*** Aucun étudiant diplômé") pe_affichage.pe_print("*** Aucun étudiant diplômé")
else: else:
self._gen_xls_diplomes(zipfile) self._gen_xls_diplomes(zipfile)
self._gen_xls_semestre_taggues(zipfile) self._gen_xls_resultats_semestres_taggues(zipfile)
self._gen_xls_rcss_tags(zipfile) # self._gen_xls_rcss_tags(zipfile)
self._gen_xls_interclassements_rcss(zipfile) # self._gen_xls_interclassements_rcss(zipfile)
self._gen_xls_synthese_jury_par_tag(zipfile) # self._gen_xls_synthese_jury_par_tag(zipfile)
self._gen_xls_synthese_par_etudiant(zipfile) # self._gen_xls_synthese_par_etudiant(zipfile)
# et le log # et le log
self._add_log_to_zip(zipfile) self._add_log_to_zip(zipfile)
@ -131,19 +131,19 @@ class JuryPE(object):
path="details", path="details",
) )
def _gen_xls_semestre_taggues(self, zipfile: ZipFile): def _gen_xls_resultats_semestres_taggues(self, zipfile: ZipFile):
"Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE" """Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE"""
pe_affichage.pe_print("*** Génère les semestres taggués") pe_affichage.pe_print("*** Génère les résultats des semestres taggués")
self.sems_tags = compute_semestres_tag(self.etudiants) self.res_sems_tags = compute_resultats_semestres_tag(self.etudiants)
# Intègre le bilan des semestres taggués au zip final # Intègre le bilan des semestres taggués au zip final
output = io.BytesIO() output = io.BytesIO()
with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated
output, engine="openpyxl" output, engine="openpyxl"
) as writer: ) as writer:
for formsemestretag in self.sems_tags.values(): for res_sem_tag in self.res_sems_tags.values():
onglet = formsemestretag.nom onglet = res_sem_tag.get_repr()
df = formsemestretag.df_moyennes_et_classements() df = res_sem_tag.df_moyennes_et_classements()
# écriture dans l'onglet # écriture dans l'onglet
df.to_excel(writer, onglet, index=True, header=True) df.to_excel(writer, onglet, index=True, header=True)
output.seek(0) output.seek(0)
@ -156,20 +156,46 @@ class JuryPE(object):
) )
def _gen_xls_rcss_tags(self, zipfile: ZipFile): def _gen_xls_rcss_tags(self, zipfile: ZipFile):
"""Génère les RCS (combinaisons de semestres suivis """Génère :
par un étudiant)
* les RCS (combinaisons de semestres suivis par les étudiants au sens
d'un aggrégat (par ex: '3S'))
* les RCS tagguées des RCS, en calculant les moyennes et les classements par tag
pour chacune.
Stocke le résultat dans self.rccs_tag, un dictionnaire de
la forme ``{nom_aggregat: {fid_terminal: SetTag(fid_terminal)} }``
Pour rappel : Chaque RCS est identifié par un nom d'aggrégat et par un formsemestre terminal.
Par exemple :
* combinaisons '3S' : S1+S2+S3 en prenant en compte tous les S3 qu'ont fréquenté les
étudiants du jury PE. Ces S3 marquent les formsemestre terminal de chaque combinaison.
* combinaisons 'S2' : 1 seul S2 pour des étudiants n'ayant pas redoublé, 2 pour des redoublants (dont les
notes seront moyennées sur leur 2 semestres S2). Ces combinaisons ont pour formsemestre le dernier S2 en
date (le S2 redoublé par les redoublants est forcément antérieur)
Args:
etudiants: Les données des étudiants
semestres_tag: Les semestres tag (pour lesquels des moyennes par tag ont été calculés)
""" """
pe_affichage.pe_print( pe_affichage.pe_print(
"*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants" "*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants"
) )
self.rcss = RCSsJuryPE(self.diplome) self.rcss_jury = pe_rcs.RCSsJuryPE(self.diplome)
self.rcss.cree_rcss(self.etudiants) self.rcss_jury.cree_rcss(self.etudiants)
# Génère les moyennes par tags des trajectoires # Génère les moyennes par tags des trajectoires
pe_affichage.pe_print("*** Calcule les moyennes par tag des RCS possibles") pe_affichage.pe_print("*** Calcule les moyennes par tag des RCS possibles")
self.rcss_tags = compute_trajectoires_tag(
self.rcss, self.etudiants, self.sems_tags self.rcss_tags = {}
) for rcs_id, rcs in self.rcss_jury.rcss.items():
# nom = rcs.get_repr()
self.rcss_tags[rcs_id] = RCSTag(rcs, self.res_sems_tags)
# Intègre le bilan des trajectoires tagguées au zip final # Intègre le bilan des trajectoires tagguées au zip final
output = io.BytesIO() output = io.BytesIO()
@ -177,7 +203,7 @@ class JuryPE(object):
output, engine="openpyxl" output, engine="openpyxl"
) as writer: ) as writer:
for rcs_tag in self.rcss_tags.values(): for rcs_tag in self.rcss_tags.values():
onglet = rcs_tag.get_repr() onglet = rcs_tag.get_repr(mode="short")
df = rcs_tag.df_moyennes_et_classements() df = rcs_tag.df_moyennes_et_classements()
# écriture dans l'onglet # écriture dans l'onglet
df.to_excel(writer, onglet, index=True, header=True) df.to_excel(writer, onglet, index=True, header=True)
@ -195,7 +221,7 @@ class JuryPE(object):
# Génère les interclassements (par promo et) par (nom d') aggrégat # Génère les interclassements (par promo et) par (nom d') aggrégat
pe_affichage.pe_print("*** Génère les interclassements par aggrégat") pe_affichage.pe_print("*** Génère les interclassements par aggrégat")
self.interclassements_taggues = compute_interclassements( self.interclassements_taggues = compute_interclassements(
self.etudiants, self.rcss, self.rcss_tags self.etudiants, self.rcss_jury, self.rcss_tags
) )
# Intègre le bilan des aggrégats (interclassé par promo) au zip final # Intègre le bilan des aggrégats (interclassé par promo) au zip final
@ -339,14 +365,14 @@ class JuryPE(object):
df_synthese = pd.DataFrame.from_dict(donnees_etudiants, orient="index") df_synthese = pd.DataFrame.from_dict(donnees_etudiants, orient="index")
# Ajout des aggrégats # Ajout des aggrégats
for aggregat in TOUS_LES_RCS: for aggregat in pe_rcs.TOUS_LES_RCS:
descr = TYPES_RCS[aggregat]["descr"] descr = pe_rcs.TYPES_RCS[aggregat]["descr"]
# Les trajectoires (tagguées) suivies par les étudiants pour l'aggrégat et le tag # Les trajectoires (tagguées) suivies par les étudiants pour l'aggrégat et le tag
# considéré # considéré
trajectoires_tagguees = [] trajectoires_tagguees = []
for etudid in etudids: for etudid in etudids:
trajectoire = self.rcss.suivi[etudid][aggregat] trajectoire = self.rcss_jury.suivi[etudid][aggregat]
if trajectoire: if trajectoire:
tid = trajectoire.rcs_id tid = trajectoire.rcs_id
trajectoire_tagguee = self.rcss_tags[tid] trajectoire_tagguee = self.rcss_tags[tid]
@ -485,14 +511,14 @@ class JuryPE(object):
# Une ligne pour le tag # Une ligne pour le tag
donnees[tag] = {("", "", "tag"): tag} donnees[tag] = {("", "", "tag"): tag}
for aggregat in TOUS_LES_RCS: for aggregat in pe_rcs.TOUS_LES_RCS:
# Le dictionnaire par défaut des moyennes # Le dictionnaire par défaut des moyennes
donnees[tag] |= get_defaut_dict_synthese_aggregat( donnees[tag] |= get_defaut_dict_synthese_aggregat(
aggregat, self.diplome aggregat, self.diplome
) )
# La trajectoire de l'étudiant sur l'aggrégat # La trajectoire de l'étudiant sur l'aggrégat
trajectoire = self.rcss.suivi[etudid][aggregat] trajectoire = self.rcss_jury.suivi[etudid][aggregat]
if trajectoire: if trajectoire:
trajectoire_tagguee = self.rcss_tags[trajectoire.rcs_id] trajectoire_tagguee = self.rcss_tags[trajectoire.rcs_id]
if tag in trajectoire_tagguee.moyennes_tags: if tag in trajectoire_tagguee.moyennes_tags:
@ -541,7 +567,7 @@ def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict:
return semestres return semestres
def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict: def compute_resultats_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
"""Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés. """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 Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire
des étudiants (cf. attribut etudiants.cursus). des étudiants (cf. attribut etudiants.cursus).
@ -563,68 +589,28 @@ def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
semestres_tags = {} semestres_tags = {}
for frmsem_id, formsemestre in formsemestres.items(): for frmsem_id, formsemestre in formsemestres.items():
# Crée le semestre_tag et exécute les calculs de moyennes # Crée le semestre_tag et exécute les calculs de moyennes
formsemestretag = SemestreTag(frmsem_id) formsemestretag = ResSemTag(frmsem_id)
pe_affichage.pe_print(
f" --> Semestre taggué {formsemestretag.nom} sur la base de {formsemestre}"
)
# Stocke le semestre taggué # Stocke le semestre taggué
semestres_tags[frmsem_id] = formsemestretag semestres_tags[frmsem_id] = formsemestretag
return semestres_tags return semestres_tags
def compute_trajectoires_tag(
trajectoires: RCSsJuryPE,
etudiants: EtudiantsJuryPE,
semestres_taggues: dict[int, SemestreTag],
):
"""Créée les trajectoires tagguées (combinaison aggrégeant plusieurs semestres au sens
d'un aggrégat (par ex: '3S')),
en calculant les moyennes et les classements par tag pour chacune.
Pour rappel : Chaque trajectoire est identifiée un nom d'aggrégat et par un formsemestre terminal.
Par exemple :
* combinaisons '3S' : S1+S2+S3 en prenant en compte tous les S3 qu'ont fréquenté les
étudiants du jury PE. Ces S3 marquent les formsemestre terminal de chaque combinaison.
* combinaisons 'S2' : 1 seul S2 pour des étudiants n'ayant pas redoublé, 2 pour des redoublants (dont les
notes seront moyennées sur leur 2 semestres S2). Ces combinaisons ont pour formsemestre le dernier S2 en
date (le S2 redoublé par les redoublants est forcément antérieur)
Args:
etudiants: Les données des étudiants
semestres_tag: Les semestres tag (pour lesquels des moyennes par tag ont été calculés)
Return:
Un dictionnaire de la forme ``{nom_aggregat: {fid_terminal: SetTag(fid_terminal)} }``
"""
trajectoires_tagguees = {}
for trajectoire_id, trajectoire in trajectoires.rcss.items():
nom = trajectoire.get_repr()
pe_affichage.pe_print(f" --> Aggrégat {nom}")
# Trajectoire_tagguee associée
trajectoire_tagguee = RCSTag(trajectoire, semestres_taggues)
# Mémorise le résultat
trajectoires_tagguees[trajectoire_id] = trajectoire_tagguee
return trajectoires_tagguees
def compute_interclassements( def compute_interclassements(
etudiants: EtudiantsJuryPE, etudiants: EtudiantsJuryPE,
trajectoires_jury_pe: RCSsJuryPE, trajectoires_jury_pe: pe_rcs.RCSsJuryPE,
trajectoires_tagguees: dict[tuple, RCS], trajectoires_tagguees: dict[tuple, pe_rcs.RCS],
): ):
"""Interclasse les étudiants, (nom d') aggrégat par aggrégat, """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 pour fournir un classement sur la promo. Le classement est établi au regard du nombre
d'étudiants ayant participé au même aggrégat. d'étudiants ayant participé au même aggrégat.
""" """
aggregats_interclasses_taggues = {} aggregats_interclasses_taggues = {}
for nom_aggregat in TOUS_LES_RCS: for nom_aggregat in pe_rcs.TOUS_LES_RCS:
pe_affichage.pe_print(f" --> Interclassement {nom_aggregat}") pe_affichage.pe_print(f" --> Interclassement {nom_aggregat}")
interclass = RCSInterclasseTag( interclass = RCSInterclasseTag(
nom_aggregat, etudiants, trajectoires_jury_pe, trajectoires_tagguees nom_aggregat, etudiants, trajectoires_jury_pe, trajectoires_tagguees
@ -642,7 +628,7 @@ def get_defaut_dict_synthese_aggregat(nom_rcs: str, diplome: int) -> dict:
diplôme : l'année du diplôme diplôme : l'année du diplôme
""" """
# L'affichage de l'aggrégat dans le tableur excel # L'affichage de l'aggrégat dans le tableur excel
descr = get_descr_rcs(nom_rcs) descr = pe_rcs.get_descr_rcs(nom_rcs)
nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}" nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}"
donnees = { donnees = {
@ -686,7 +672,7 @@ def get_dict_synthese_aggregat(
à l'aggrégat donné et pour un tag donné""" à l'aggrégat donné et pour un tag donné"""
donnees = {} donnees = {}
# L'affichage de l'aggrégat dans le tableur excel # L'affichage de l'aggrégat dans le tableur excel
descr = get_descr_rcs(aggregat) descr = pe_rcs.get_descr_rcs(aggregat)
# La note de l'étudiant (chargement à venir) # La note de l'étudiant (chargement à venir)
note = np.nan note = np.nan

203
app/pe/pe_moytag.py Normal file
View File

@ -0,0 +1,203 @@
import numpy as np
import pandas as pd
from app.comp.moy_sem import comp_ranks_series
from app.models import UniteEns
from app.pe import pe_affichage
class Moyenne:
CRITERES = [
"note",
"classement",
"rang",
"min",
"max",
"moy",
"nb_etuds",
"nb_inscrits",
]
def __init__(self, notes: pd.Series):
"""Classe centralisant la synthèse des moyennes/classements d'une série
de notes :
* des "notes": la Serie pandas des notes (float),
* des "classements": la Serie pandas des classements (float),
* des "min": la note minimum,
* des "max": la note maximum,
* des "moy": la moyenne,
* des "nb_inscrits": le nombre d'étudiants ayant une note,
"""
self.notes = notes
"""Les notes"""
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"""
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"""
def comp_moy_et_stat(self, notes: pd.Series) -> dict:
"""Calcule et structure les données nécessaires au PE pour une série
de notes (pouvant être une moyenne d'un tag à une UE ou une moyenne générale
d'un tag) dans un dictionnaire spécifique.
Partant des notes, sont calculés les classements (en ne tenant compte
que des notes non nulles).
Args:
notes: Une série de notes (avec des éventuels NaN)
Returns:
Un dictionnaire stockant les notes, les classements, le min,
le max, la moyenne, le nb de notes (donc d'inscrits)
"""
df = pd.DataFrame(
np.nan,
index=self.etudids,
columns=Moyenne.CRITERES,
)
# Supprime d'éventuelles chaines de caractères dans les notes
notes = pd.to_numeric(notes, errors="coerce")
df["note"] = notes
# Les nb d'étudiants & nb d'inscrits
df["nb_etuds"] = len(self.etudids)
# 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)
# 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
# Le rang (classement/nb_inscrit)
df["rang"] = df["rang"].astype(str)
df.loc[inscrits_ids, "rang"] = (
df.loc[inscrits_ids, "classement"].astype(int).astype(str)
+ "/"
+ df.loc[inscrits_ids, "nb_inscrits"].astype(int).astype(str)
)
# Les stat (des inscrits)
df.loc[inscrits_ids, "min"] = notes.min()
df.loc[inscrits_ids, "max"] = notes.max()
df.loc[inscrits_ids, "moy"] = notes.mean()
return df
def to_dict(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
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"]
def get_min(self) -> pd.Series:
"""Série des min"""
return self.df_gen["min"].round(2)
def get_max(self) -> pd.Series:
"""Série des max"""
return self.df_gen["max"].round(2)
def get_moy(self) -> pd.Series:
"""Série des moy"""
return self.df_gen["moy"].round(2)
def get_note_for_df(self, etudid: int):
"""Note d'un étudiant donné par son etudid"""
return round(self.df_gen["note"].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 = self.df_gen["rang"].loc[etudid]
if not pd.isna(classement):
return classement
else:
return pe_affichage.SANS_NOTE
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,
ues: list[UniteEns],
notes_ues: pd.DataFrame,
# notes_gen: pd.Series,
):
"""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
ues: La liste des UEs ayant servie au calcul de la moyenne
notes_ues: Les moyennes (etudid x acronymes_ues) aux différentes UEs et pour le tag
# 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 UE
self.ues: dict[int, UniteEns] = {ue.id: ue for ue in ues}
"""Les UEs sur lesquelles sont calculées les moyennes"""
colonnes = list(notes_ues.columns)
acronymes = [self.ues[ue_id].acronyme for ue_id in colonnes]
assert len(set(acronymes)) == len(colonnes), \
"Deux UEs ne peuvent pas avoir le même acronyme"
# Les moyennes par UE
self.notes_ues = notes_ues
"""Les notes aux UEs (dataframe)"""
self.notes_ues.columns = acronymes # remplace les ue.id par leur acronyme
self.moys_ues: dict[int, pd.DataFrame] = {}
"""Les dataframes retraçant les moyennes/classements/statistiques des étudiants aux UEs"""
for ue in self.ues.values(): # if ue.type != UE_SPORT:
notes = notes_ues[ue.acronyme]
self.moys_ues[ue.acronyme] = Moyenne(notes)
# Les moyennes générales
self.notes_gen = notes_gen
"""Les notes générales (moyenne toutes UEs confonudes)"""
self.moy_gen = Moyenne(notes_gen)
"""Le dataframe retraçant les moyennes/classements/statistiques général"""
def __eq__(self, other):
"""Egalité de deux MoyenneTag lorsque leur tag sont identiques"""
return self.tag == other.tag

View File

@ -116,6 +116,10 @@ class RCS:
self.semestres_aggreges = {} self.semestres_aggreges = {}
"""Semestres regroupés dans le RCS""" """Semestres regroupés dans le RCS"""
def get_formsemestre_id_final(self):
"""Renvoie l'identifiant du formsemestre final du RCS"""
return self.formsemestre_final.formsemestre_id
def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]): def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]):
"""Ajout de semestres aux semestres à regrouper """Ajout de semestres aux semestres à regrouper

View File

@ -37,17 +37,19 @@ Created on Fri Sep 9 09:15:05 2016
""" """
from app.comp.res_sem import load_formsemestre_results from app.comp.res_sem import load_formsemestre_results
from app.pe.pe_semtag import SemestreTag from app.pe import pe_affichage
from app.pe.pe_ressemtag import ResSemTag
import pandas as pd import pandas as pd
import numpy as np import numpy as np
from app.pe.pe_rcs import RCS from app.pe.pe_rcs import RCS
from app.pe.pe_tabletags import TableTag, MoyenneTag from app.pe.pe_tabletags import TableTag
from app.pe.pe_moytag import MoyennesTag
class RCSTag(TableTag): class RCSTag(TableTag):
def __init__( def __init__(
self, rcs: RCS, semestres_taggues: dict[int, SemestreTag] self, rcs: RCS, semestres_taggues: dict[int, ResSemTag]
): ):
"""Calcule les moyennes par tag d'une combinaison de semestres """Calcule les moyennes par tag d'une combinaison de semestres
(RCS), pour extraire les classements par tag pour un (RCS), pour extraire les classements par tag pour un
@ -104,11 +106,11 @@ class RCSTag(TableTag):
self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted) self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted)
"""Calcul les moyennes par tag sous forme d'un dataframe""" """Calcul les moyennes par tag sous forme d'un dataframe"""
self.moyennes_tags: dict[str, MoyenneTag] = {} self.moyennes_tags: dict[str, MoyennesTag] = {}
"""Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)""" """Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)"""
for tag in self.tags_sorted: for tag in self.tags_sorted:
moy_gen_tag = self.notes[tag] moy_gen_tag = self.notes[tag]
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag) self.moyennes_tags[tag] = MoyennesTag(tag, moy_gen_tag)
def __eq__(self, other): def __eq__(self, other):
"""Egalité de 2 RCS taggués sur la base de leur identifiant""" """Egalité de 2 RCS taggués sur la base de leur identifiant"""
@ -172,6 +174,7 @@ class RCSTag(TableTag):
tags = [] tags = []
for frmsem_id in self.semestres_tags_aggreges: for frmsem_id in self.semestres_tags_aggreges:
tags.extend(self.semestres_tags_aggreges[frmsem_id].tags_sorted) tags.extend(self.semestres_tags_aggreges[frmsem_id].tags_sorted)
pe_affichage.pe_print(f"* Tags : {', '.join(tags)}")
return sorted(set(tags)) return sorted(set(tags))

373
app/pe/pe_ressemtag.py Normal file
View File

@ -0,0 +1,373 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
##############################################################################
# Module "Avis de poursuite d'étude"
# conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################
"""
Created on Fri Sep 9 09:15:05 2016
@author: barasc
"""
import pandas as pd
import app.pe.pe_etudiant
from app import db, ScoValueError
from app import comp
from app.comp.res_sem import load_formsemestre_results
from app.models import FormSemestre
from app.models.moduleimpls import ModuleImpl
import app.pe.pe_affichage as pe_affichage
import app.pe.pe_etudiant as pe_etudiant
from app.pe.pe_tabletags import TableTag
from app.pe.pe_moytag import MoyennesTag
from app.scodoc import sco_tag_module
from app.scodoc.codes_cursus import UE_SPORT
class ResSemTag(TableTag):
"""
Un ResSemTag représente les résultats des étudiants à un semestre, en donnant
accès aux moyennes par tag.
Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT.
"""
def __init__(self, formsemestre_id: int):
"""
Args:
formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base
"""
TableTag.__init__(self)
# Le semestre
self.formsemestre_id = formsemestre_id
self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# Le nom du res_semestre taggué
self.nom = self.get_repr(mode="long")
pe_affichage.pe_print(
f"--> Résultats de Semestre taggués {self.nom}"
)
# Les résultats du semestre
self.nt = load_formsemestre_results(self.formsemestre)
# Les étudiants
self.etuds = self.nt.etuds
self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds}
self.etudids = list(self.etudiants.keys())
# Les notes, les modules implémentés triés, les étudiants, les coeffs,
# récupérés notamment de py:mod:`res_but`
self.sem_cube = self.nt.sem_cube
self.modimpls_sorted = self.nt.formsemestre.modimpls_sorted
self.modimpl_coefs_df = self.nt.modimpl_coefs_df
# Les inscriptions aux modules
self.modimpl_inscr_df = self.nt.modimpl_inscr_df
# Les UEs (et les dispenses d'UE)
self.ues = self.nt.ues
ues_hors_sport = [ue for ue in self.ues if ue.type != UE_SPORT]
self.ues_inscr_parcours_df = self.nt.load_ues_inscr_parcours()
self.dispense_ues = self.nt.dispense_ues
# Les tags personnalisés et auto:
tags_dict = self._get_tags_dict()
self._check_tags(tags_dict)
# self.tags = [tag for cat in dict_tags for tag in dict_tags[cat]]
# Calcul des moyennes & les classements de chaque étudiant à chaque tag
self.moyennes_tags = {}
for tag in tags_dict["personnalises"]:
# 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)
# moy_gen_tag = self.compute_moy_gen_tag(moy_ues_tag)
self.moyennes_tags[tag] = MoyennesTag(
tag, ues_hors_sport, moy_ues_tag # moy_gen_tag
)
# Ajoute les d'UE moyennes générales de BUT pour le semestre considéré
# moy_gen_but = self.nt.etud_moy_gen
# self.moyennes_tags["but"] = MoyenneTag("but", [], None, moy_gen_but, )
# Ajoute les moyennes par UEs (et donc par compétence) + la moyenne générale (but)
df_ues = pd.DataFrame(
{ue.id: self.nt.etud_moy_ue[ue.id] for ue in ues_hors_sport},
index=self.etudids,
)
# moy_ues = self.nt.etud_moy_ue[ue_id]
# moy_gen_but = self.nt.etud_moy_gen
self.moyennes_tags["but"] = MoyennesTag(
"but", ues_hors_sport, df_ues #, moy_gen_but
)
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.notes = self.df_notes()
# """Dataframe synthétique des notes par tag"""
# pe_affichage.pe_print(
# f" => Traitement des tags {', '.join(self.tags_sorted)}"
# )
def get_repr(self, mode="long"):
"""Nom affiché pour le semestre taggué"""
if mode == "short":
return f"{self.formsemestre} ({self.formsemestre_id})"
else: # mode == "long"
return pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True)
def compute_moy_ues_tag(self, info_tag: dict[int, dict]) -> pd.DataFrame:
"""Calcule la moyenne par UE des étudiants pour un tag,
en ayant connaissance des informations sur le tag.
Les informations sur le tag sont un dictionnaire listant les modimpl_id rattachés au tag,
et pour chacun leur éventuel coefficient de **repondération**.
Returns:
Le dataframe des moyennes du tag par UE
"""
# Adaptation du mask de calcul des moyennes au tag visé
modimpls_mask = [
modimpl.module.ue.type != UE_SPORT
for modimpl in self.formsemestre.modimpls_sorted
]
# 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 info_tag:
modimpls_mask[i] = False
# Applique la pondération des coefficients
modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy()
for modimpl_id in info_tag:
ponderation = info_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)
moyennes_ues_tag = comp.moy_ue.compute_ue_moys_apc(
self.sem_cube,
self.etuds,
self.formsemestre.modimpls_sorted,
self.modimpl_inscr_df,
modimpl_coefs_ponderes_df,
modimpls_mask,
self.dispense_ues,
block=self.formsemestre.block_moyennes,
)
return moyennes_ues_tag
def compute_moy_gen_tag(self, moy_ues_tag: pd.DataFrame) -> pd.Series:
"""Calcule la moyenne générale (toutes UE confondus)
pour le tag considéré, en les pondérant par les crédits ECTS.
"""
# Les ects
ects = self.ues_inscr_parcours_df.fillna(0.0) * [
ue.ects for ue in self.ues if ue.type != UE_SPORT
]
# Calcule la moyenne générale dans le semestre (pondérée par le ECTS)
moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects(
moy_ues_tag,
ects,
formation_id=self.formsemestre.formation_id,
skip_empty_ues=True,
)
return moy_gen_tag
def _get_tags_dict(self):
"""Renvoie les tags personnalisés (déduits des modules du semestre)
et les tags automatiques ('but'), et toutes leurs informations,
dans un dictionnaire de la forme :
``{"personnalises": {tag: info_sur_le_tag},
"auto": {tag: {}}``
Returns:
Le dictionnaire structuré des tags ("personnalises" vs. "auto")
"""
dict_tags = {"personnalises": dict(), "auto": dict()}
# Les tags perso
dict_tags["personnalises"] = get_synthese_tags_personnalises_semestre(
self.nt.formsemestre
)
noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys())))
pe_affichage.pe_print(
f"* Tags personnalisés (extraits du programme de formation) : {', '.join(noms_tags_perso)}"
)
# Les tags automatiques
# Déduit des compétences
# dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre)
# noms_tags_comp = list(set(dict_ues_competences.values()))
# BUT
dict_tags["auto"] = {"but": {}}
noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp
pe_affichage.pe_print(
f"* Tags automatiquement ajoutés : {', '.join(noms_tags_auto)}"
)
return dict_tags
def _check_tags(self, dict_tags):
"""Vérifie l'unicité des tags"""
noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys())))
noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp
noms_tags = noms_tags_perso + noms_tags_auto
intersection = list(set(noms_tags_perso) & set(noms_tags_auto))
if intersection:
liste_intersection = "\n".join(
[f"<li><code>{tag}</code></li>" for tag in intersection]
)
s = "s" if len(intersection) > 1 else ""
message = f"""Erreur dans le module PE : Un des tags saisis dans votre
programme de formation fait parti des tags réservés. En particulier,
votre semestre <em>{self.formsemestre.titre_annee()}</em>
contient le{s} tag{s} réservé{s} suivant :
<ul>
{liste_intersection}
</ul>
Modifiez votre programme de formation pour le{s} supprimer.
Il{s} ser{'ont' if s else 'a'} automatiquement à vos documents de poursuites d'études.
"""
raise ScoValueError(message)
def get_moduleimpl(modimpl_id) -> dict:
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
modimpl = db.session.get(ModuleImpl, modimpl_id)
if modimpl:
return modimpl
return None
def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float:
"""Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve
le module de modimpl_id
"""
# ré-écrit
modimpl = get_moduleimpl(modimpl_id) # le module
ue_status = nt.get_etud_ue_status(etudid, modimpl.module.ue.id)
if ue_status is None:
return None
return ue_status["moy"]
def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre):
"""Etant données les implémentations des modules du semestre (modimpls),
synthétise les tags renseignés dans le programme pédagogique &
associés aux modules du semestre,
en les associant aux modimpls qui les concernent (modimpl_id) et
au coeff de repondération fournie avec le tag (par défaut 1 si non indiquée)).
Le dictionnaire fournit est de la forme :
``{ tag : { modimplid: {"modimpl": ModImpl,
"ponderation": coeff_de_reponderation}
} }``
Args:
formsemestre: Le formsemestre à la base de la recherche des tags
Return:
Un dictionnaire décrivant les tags
"""
synthese_tags = {}
# Instance des modules du semestre
modimpls = formsemestre.modimpls_sorted
for modimpl in modimpls:
modimpl_id = modimpl.id
# Liste des tags pour le module concerné
tags = sco_tag_module.module_tag_list(modimpl.module.id)
# Traitement des tags recensés, chacun pouvant étant de la forme
# "mathématiques", "théorie", "pe:0", "maths:2"
for tag in tags:
# Extraction du nom du tag et du coeff de pondération
(tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag)
# Ajout d'une clé pour le tag
if tagname not in synthese_tags:
synthese_tags[tagname] = {}
# Ajout du module (modimpl) au tagname considéré
synthese_tags[tagname][modimpl_id] = {
"modimpl": modimpl, # les données sur le module
# "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre
"ponderation": ponderation, # la pondération demandée pour le tag sur le module
# "module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee
# "ue_id": modimpl.module.ue.id, # les données sur l'ue
# "ue_code": modimpl.module.ue.ue_code,
# "ue_acronyme": modimpl.module.ue.acronyme,
}
return synthese_tags
def get_noms_competences_from_ues(formsemestre: FormSemestre) -> dict[int, str]:
"""Partant d'un formsemestre, extrait le nom des compétences associés
à (ou aux) parcours des étudiants du formsemestre.
Ignore les UEs non associées à un niveau de compétence.
Args:
formsemestre: Un FormSemestre
Returns:
Dictionnaire {ue_id: nom_competence} lisant tous les noms des compétences
en les raccrochant à leur ue
"""
# Les résultats du semestre
nt = load_formsemestre_results(formsemestre)
noms_competences = {}
for ue in nt.ues:
if ue.niveau_competence and ue.type != UE_SPORT:
# ?? inutilisé ordre = ue.niveau_competence.ordre
nom = ue.niveau_competence.competence.titre
noms_competences[ue.ue_id] = f"comp. {nom}"
return noms_competences

View File

@ -35,290 +35,216 @@ Created on Fri Sep 9 09:15:05 2016
@author: barasc @author: barasc
""" """
import pandas as pd
import app.pe.pe_etudiant
from app import db, ScoValueError
from app import comp
from app.comp.res_sem import load_formsemestre_results from app.comp.res_sem import load_formsemestre_results
from app.models import FormSemestre from app.models import UniteEns
from app.models.moduleimpls import ModuleImpl from app.pe import pe_affichage
import app.pe.pe_affichage as pe_affichage from app.pe.pe_ressemtag import ResSemTag
from app.pe.pe_tabletags import TableTag, MoyenneTag import pandas as pd
from app.scodoc import sco_tag_module import numpy as np
from app.scodoc.codes_cursus import UE_SPORT from app.pe.pe_rcs import RCS
from app.pe.pe_tabletags import TableTag
from app.pe.pe_moytag import MoyennesTag
class SemestreTag(TableTag): class SemTag(TableTag):
""" def __init__(self, rcs: RCS, semestres_taggues: dict[int, ResSemTag]):
Un SemestreTag représente les résultats des étudiants à un semestre, en donnant """Calcule les moyennes/classements par tag à un RCS d'un seul semestre
accès aux moyennes par tag. (ici semestre) de type 'Sx' (par ex. 'S1', 'S2', ...) :
Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT.
""" * pour les étudiants non redoublants, ce sont les moyennes/classements
du semestre suivi
* pour les étudiants redoublants, c'est une fusion des moyennes/classements
suivis les différents 'Sx' (donné par dans le rcs)
Les **tags considérés** sont uniquement ceux du dernier semestre du RCS
def __init__(self, formsemestre_id: int):
"""
Args: Args:
formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base rcs: Un RCS (identifié par un nom et l'id de son semestre terminal)
semestres_taggues: Les données sur les semestres taggués
""" """
TableTag.__init__(self) TableTag.__init__(self)
# Le semestre self.rcs_id = rcs.rcs_id
self.formsemestre_id = formsemestre_id """Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)"""
self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# Le nom du semestre taggué self.rcs = rcs
"""RCS associé au RCS taggué"""
assert self.rcs.nom.startswith(
"S"
), "Un SemTag ne peut être utilisé que pour un RCS de la forme Sx"
self.nom = self.get_repr() self.nom = self.get_repr()
"""Représentation textuelle du RCS taggué"""
# Les résultats du semestre # Les données du formsemestre_terminal
self.nt = load_formsemestre_results(self.formsemestre) self.formsemestre_terminal = rcs.formsemestre_final
"""Le formsemestre terminal"""
# Les étudiants # Les résultats du formsemestre terminal
self.etuds = self.nt.etuds nt = load_formsemestre_results(self.formsemestre_terminal)
self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds}
self.etudids = list(self.etudiants.keys())
# Les notes, les modules implémentés triés, les étudiants, les coeffs, self.semestres_aggreges = rcs.semestres_aggreges
# récupérés notamment de py:mod:`res_but` """Les semestres aggrégés"""
self.sem_cube = self.nt.sem_cube
self.modimpls_sorted = self.nt.formsemestre.modimpls_sorted
self.modimpl_coefs_df = self.nt.modimpl_coefs_df
# Les inscriptions au module et les dispenses d'UE self.semestres_tags_aggreges = {}
self.modimpl_inscr_df = self.nt.modimpl_inscr_df """Les semestres tags associés aux semestres aggrégés"""
self.ues = self.nt.ues try:
self.ues_inscr_parcours_df = self.nt.load_ues_inscr_parcours() for frmsem_id in self.semestres_aggreges:
self.dispense_ues = self.nt.dispense_ues self.semestres_tags_aggreges[frmsem_id] = semestres_taggues[frmsem_id]
except:
raise ValueError("Semestres taggués manquants")
# Les tags : # Les données des étudiants
## Saisis par l'utilisateur self.etuds = nt.etuds
tags_personnalises = get_synthese_tags_personnalises_semestre( """Les étudiants"""
self.nt.formsemestre self.etudids = [etud.etudid for etud in self.etuds]
) """Les etudids"""
noms_tags_perso = sorted(list(set(tags_personnalises.keys()))) self.etats_civils = {
etudid: self.etuds[etudid].etat_civil for etudid in self.etudids
}
"""Les états civils"""
## Déduit des compétences # Les tags
# dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre) self.tags_sorted = self.comp_tags_list()
# noms_tags_comp = list(set(dict_ues_competences.values())) """Tags extraits du semestre terminal de l'aggrégat"""
noms_tags_auto = ["but"] # + noms_tags_comp
self.tags = noms_tags_perso + noms_tags_auto
"""Tags du semestre taggué"""
## Vérifie l'unicité des tags # Les UEs
if len(set(self.tags)) != len(self.tags): self.ues = self.comp_ues(tag="but")
intersection = list(set(noms_tags_perso) & set(noms_tags_auto)) self.acronymes_ues_sorted = sorted([ue.acronyme for ue in self.ues.values()])
liste_intersection = "\n".join( """UEs extraites du semestre terminal de l'aggrégat (avec
[f"<li><code>{tag}</code></li>" for tag in intersection] check de concordance sur les UE des semestres_aggrégés)"""
)
s = "s" if len(intersection) > 0 else ""
message = f"""Erreur dans le module PE : Un des tags saisis dans votre
programme de formation fait parti des tags réservés. En particulier,
votre semestre <em>{self.formsemestre.titre_annee()}</em>
contient le{s} tag{s} réservé{s} suivant :
<ul>
{liste_intersection}
</ul>
Modifiez votre programme de formation pour le{s} supprimer.
Il{s} ser{'ont' if s else 'a'} automatiquement à vos documents de poursuites d'études.
"""
raise ScoValueError(message)
ues_hors_sport = [ue for ue in self.ues if ue.type != UE_SPORT] self.moyennes_tags: dict[str, MoyennesTag] = {}
"""Moyennes/classements par tag (qu'ils soient personnalisés ou automatiques)"""
# Calcul des moyennes & les classements de chaque étudiant à chaque tag self.notes: dict[str, pd.DataFrame] = {}
self.moyennes_tags = {} """Les notes aux différents tags"""
for tag in self.tags_sorted:
# Cube de note
notes_cube = self.compute_notes_ues_cube(tag, self.acronymes_ues)
for tag in tags_personnalises: # Calcule des moyennes sous forme d'un dataframe"""
# pe_affichage.pe_print(f" -> Traitement du tag {tag}") self.notes[tag] = compute_notes_ues(notes_cube, self.etudids, self.acronymes_ues)
moy_ues_tag = self.compute_moy_ues_tag(tag, tags_personnalises)
moy_gen_tag = self.compute_moy_gen_tag(moy_ues_tag)
self.moyennes_tags[tag] = MoyenneTag( # Les moyennes
tag, ues_hors_sport, moy_ues_tag, moy_gen_tag self.moyennes_tags[tag] = MoyennesTag(tag, self.notes[tag])
)
# Ajoute les d'UE moyennes générales de BUT pour le semestre considéré def __eq__(self, other):
# moy_gen_but = self.nt.etud_moy_gen """Egalité de 2 RCS taggués sur la base de leur identifiant"""
# self.moyennes_tags["but"] = MoyenneTag("but", [], None, moy_gen_but, ) return self.rcs_id == other.rcs_id
# Ajoute les moyennes par UEs (et donc par compétence) + la moyenne générale (but) def get_repr(self, verbose=False) -> str:
df_ues = pd.DataFrame({ue.id: self.nt.etud_moy_ue[ue.id] for ue in ues_hors_sport}, """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
index = self.etudids) est basée)"""
# moy_ues = self.nt.etud_moy_ue[ue_id] return self.rcs.get_repr(verbose=verbose)
moy_gen_but = self.nt.etud_moy_gen
self.moyennes_tags["but"] = MoyenneTag("but", ues_hors_sport, df_ues, moy_gen_but)
def compute_notes_ues_cube(self, tag, acronymes_ues_sorted):
"""Construit le cube de notes des UEs (etudid x accronyme_ue x semestre_aggregé)
nécessaire au calcul des moyennes du tag pour le RCS Sx
"""
# Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2)
etudids = [etud.etudid for etud in self.etuds]
# acronymes_ues = sorted([ue.acronyme for ue in self.ues.values()])
semestres_id = list(self.semestres_tags_aggreges.keys())
self.tags_sorted = self.get_all_tags() dfs = {}
"""Tags (personnalisés+compétences) par ordre alphabétique"""
# Synthétise l'ensemble des moyennes dans un dataframe for frmsem_id in semestres_id:
# Partant d'un dataframe vierge
df = pd.DataFrame(np.nan, index=etudids, columns=acronymes_ues_sorted)
self.notes = self.df_notes() # Charge les notes du semestre tag
"""Dataframe synthétique des notes par tag""" sem_tag = self.semestres_tags_aggreges[frmsem_id]
moys_tag = sem_tag.moyennes_tags[tag]
notes = moys_tag.notes_ues # dataframe etudids x ues
acronymes_ues_sem = list(notes.columns) # les acronymes des UEs du semestre tag
pe_affichage.pe_print( # UEs communes à celles du SemTag (celles du dernier semestre du RCS)
f" => Traitement des tags {', '.join(self.tags_sorted)}" ues_communes = list(set(acronymes_ues_sorted) & set(acronymes_ues_sem))
)
def get_repr(self): # Etudiants communs
"""Nom affiché pour le semestre taggué""" etudids_communs = df.index.intersection(notes.index)
return app.pe.pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True)
def compute_moy_ues_tag(self, tag: str, tags_infos: dict) -> pd.Series: # Recopie
"""Calcule la moyenne par UE des étudiants pour le tag indiqué, df.loc[etudids_communs, ues_communes] = notes.loc[etudids_communs, ues_communes]
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, # Supprime tout ce qui n'est pas numérique
avec leur éventuel coefficient de **repondération**, en utilisant les notes for col in df.columns:
chargées pour ce SemestreTag. df[col] = pd.to_numeric(df[col], errors="coerce")
Force ou non le calcul de la moyenne lorsque des notes sont manquantes. # Stocke le df
dfs[frmsem_id] = df
"""Réunit les notes sous forme d'un cube semestres x etdids x ues"""
semestres_x_etudids_x_ues = [dfs[fid].values for fid in dfs]
etudids_x_ues_x_semestres = np.stack(semestres_x_etudids_x_ues, axis=-1)
return etudids_x_ues_x_semestres
def comp_tags_list(self):
"""Récupère les tag du semestre taggué associé au semestre final du RCS
Returns: Returns:
La série des moyennes Une liste de tags triés par ordre alphabétique
""" """
tags = []
dernier_frmid = self.formsemestre_terminal.formsemestre_id
dernier_semestre_tag = self.semestres_tags_aggreges[dernier_frmid]
tags = dernier_semestre_tag.tags_sorted
pe_affichage.pe_print(f"* Tags : {', '.join(tags)}")
return tags
# Adaptation du mask de calcul des moyennes au tag visé def comp_ues(self, tag="but") -> dict[int, UniteEns]:
modimpls_mask = [ """Récupère les UEs à aggréger, en s'appuyant sur la moyenne générale
modimpl.module.ue.type != UE_SPORT (tag but) du semestre final du RCS
for modimpl in self.formsemestre.modimpls_sorted
]
# Désactive tous les modules qui ne sont pas pris en compte pour ce tag Returns:
for i, modimpl in enumerate(self.formsemestre.modimpls_sorted): Un dictionnaire donnant les UEs
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 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)#
moyennes_ues_tag = comp.moy_ue.compute_ue_moys_apc(
self.sem_cube,
self.etuds,
self.formsemestre.modimpls_sorted,
self.modimpl_inscr_df,
modimpl_coefs_ponderes_df,
modimpls_mask,
self.dispense_ues,
block=self.formsemestre.block_moyennes,
)
return moyennes_ues_tag
def compute_moy_gen_tag(self, moy_ues_tag: pd.DataFrame) -> pd.Series:
"""Calcule la moyenne générale (toutes UE confondus)
pour le tag considéré, en les pondérant par les crédits ECTS.
""" """
# Les ects dernier_frmid = self.formsemestre_terminal.formsemestre_id
ects = self.ues_inscr_parcours_df.fillna(0.0) * [ dernier_semestre_tag = self.semestres_tags_aggreges[dernier_frmid]
ue.ects for ue in self.ues if ue.type != UE_SPORT moy_tag = dernier_semestre_tag.moyennes_tags[tag]
] return moy_tag.ues # les UEs
# Calcule la moyenne générale dans le semestre (pondérée par le ECTS)
moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects(
moy_ues_tag,
ects,
formation_id=self.formsemestre.formation_id,
skip_empty_ues=True,
)
return moy_gen_tag
def get_moduleimpl(modimpl_id) -> dict: def compute_notes_ues(set_cube: np.array, etudids: list, acronymes_ues: list):
"""Renvoie l'objet modimpl dont l'id est modimpl_id""" """Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE
modimpl = db.session.get(ModuleImpl, modimpl_id) par UE) obtenue par un étudiant à un semestre.
if modimpl:
return modimpl
return None
def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float:
"""Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve
le module de modimpl_id
"""
# ré-écrit
modimpl = get_moduleimpl(modimpl_id) # le module
ue_status = nt.get_etud_ue_status(etudid, modimpl.module.ue.id)
if ue_status is None:
return None
return ue_status["moy"]
def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre):
"""Etant données les implémentations des modules du semestre (modimpls),
synthétise les tags renseignés dans le programme pédagogique &
associés aux modules du semestre,
en les associant aux modimpls qui les concernent (modimpl_id) et
aucoeff et pondération fournie avec le tag (par défaut 1 si non indiquée)).
Args: Args:
formsemestre: Le formsemestre à la base de la recherche des tags set_cube: notes moyennes aux modules ndarray
(semestre_ids x etudids x UEs), des floats avec des NaN
Return: etudids: liste des étudiants (dim. 0 du cube)
Un dictionnaire de tags acronymes_ues: liste des acronymes des ues (dim. 1 du cube)
"""
synthese_tags = {}
# Instance des modules du semestre
modimpls = formsemestre.modimpls_sorted
for modimpl in modimpls:
modimpl_id = modimpl.id
# Liste des tags pour le module concerné
tags = sco_tag_module.module_tag_list(modimpl.module.id)
# Traitement des tags recensés, chacun pouvant étant de la forme
# "mathématiques", "théorie", "pe:0", "maths:2"
for tag in tags:
# Extraction du nom du tag et du coeff de pondération
(tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag)
# Ajout d'une clé pour le tag
if tagname not in synthese_tags:
synthese_tags[tagname] = {}
# Ajout du module (modimpl) au tagname considéré
synthese_tags[tagname][modimpl_id] = {
"modimpl": modimpl, # les données sur le module
# "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre
"ponderation": ponderation, # la pondération demandée pour le tag sur le module
# "module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee
# "ue_id": modimpl.module.ue.id, # les données sur l'ue
# "ue_code": modimpl.module.ue.ue_code,
# "ue_acronyme": modimpl.module.ue.acronyme,
}
return synthese_tags
def get_noms_competences_from_ues(formsemestre: FormSemestre) -> dict[int, str]:
"""Partant d'un formsemestre, extrait le nom des compétences associés
à (ou aux) parcours des étudiants du formsemestre.
Ignore les UEs non associées à un niveau de compétence.
Args:
formsemestre: Un FormSemestre
Returns: Returns:
Dictionnaire {ue_id: nom_competence} lisant tous les noms des compétences Un DataFrame avec pour columns les moyennes par ues,
en les raccrochant à leur ue et pour rows les etudid
""" """
# Les résultats du semestre nb_etuds, nb_ues, nb_semestres = set_cube.shape
nt = load_formsemestre_results(formsemestre) assert nb_etuds == len(etudids)
assert nb_ues == len(acronymes_ues)
noms_competences = {} # Quelles entrées du cube contiennent des notes ?
for ue in nt.ues: mask = ~np.isnan(set_cube)
if ue.niveau_competence and ue.type != UE_SPORT:
# ?? inutilisé ordre = ue.niveau_competence.ordre # Enlève les NaN du cube pour les entrées manquantes
nom = ue.niveau_competence.competence.titre set_cube_no_nan = np.nan_to_num(set_cube, nan=-1.0)
noms_competences[ue.ue_id] = f"comp. {nom}"
return noms_competences # Les moyennes par ues
# TODO: Pour l'instant un max sans prise en compte des UE capitalisées
etud_moy = np.max(set_cube_no_nan, axis=2)
# Fix les max non calculé -1 -> NaN
etud_moy[etud_moy < 0] = np.NaN
# Le dataFrame
etud_moy_tag_df = pd.DataFrame(
etud_moy,
index=etudids, # les etudids
columns=acronymes_ues, # les tags
)
etud_moy_tag_df.fillna(np.nan)
return etud_moy_tag_df

View File

@ -37,195 +37,11 @@ Created on Thu Sep 8 09:36:33 2016
@author: barasc @author: barasc
""" """
import datetime
import numpy as np
from app import ScoValueError
from app.comp.moy_sem import comp_ranks_series
from app.models import UniteEns
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 import pandas as pd
from app.scodoc.codes_cursus import UE_SPORT
TAGS_RESERVES = ["but"] TAGS_RESERVES = ["but"]
class MoyenneTag:
def __init__(
self,
tag: str,
ues: list[UniteEns],
notes_ues: pd.DataFrame,
notes_gen: pd.Series,
):
"""Classe centralisant la synthèse des moyennes/classements d'une série
d'étudiants à un tag donné, en stockant :
``
{
"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
ues: La liste des UEs ayant servie au calcul de la moyenne
notes_ues: Les moyennes (etudid x ues) aux différentes UEs et pour le tag
notes_gen: Une série de notes (moyenne) sous forme d'un pd.Series() (toutes UEs confondues)
"""
self.tag = tag
"""Le tag associé à la moyenne"""
self.etudids = list(notes_gen.index) # calcul à venir
"""Les id des étudiants"""
self.ues: list[UniteEns] = ues
"""Les UEs sur lesquelles sont calculées les moyennes"""
self.df_ues: dict[int, pd.DataFrame] = {}
"""Les dataframes retraçant les moyennes/classements/statistiques des étudiants aux UEs"""
for ue in self.ues: # if ue.type != UE_SPORT:
notes = notes_ues[ue.id]
self.df_ues[ue.id] = self.comp_moy_et_stat(notes)
self.inscrits_ids = notes_gen[notes_gen.notnull()].index.to_list()
"""Les id des étudiants dont la moyenne générale est non nulle"""
self.df_gen: pd.DataFrame = self.comp_moy_et_stat(notes_gen)
"""Le dataframe retraçant les moyennes/classements/statistiques général"""
self.synthese = self.to_dict()
"""La synthèse (dictionnaire) des notes/classements/statistiques"""
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
de notes (pouvant être une moyenne d'un tag à une UE ou une moyenne générale
d'un tag) dans un dictionnaire spécifique.
Partant des notes, sont calculés les classements (en ne tenant compte
que des notes non nulles).
Args:
notes: Une série de notes (avec des éventuels NaN)
Returns:
Un dictionnaire stockant les notes, les classements, le min,
le max, la moyenne, le nb de notes (donc d'inscrits)
"""
df = pd.DataFrame(
np.nan,
index=self.etudids,
columns=[
"note",
"classement",
"rang",
"min",
"max",
"moy",
"nb_etuds",
"nb_inscrits",
],
)
# Supprime d'éventuelles chaines de caractères dans les notes
notes = pd.to_numeric(notes, errors="coerce")
df["note"] = notes
# Les nb d'étudiants & nb d'inscrits
df["nb_etuds"] = len(self.etudids)
# 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)
# 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
# Le rang (classement/nb_inscrit)
df["rang"] = df["rang"].astype(str)
df.loc[inscrits_ids, "rang"] = (
df.loc[inscrits_ids, "classement"].astype(int).astype(str)
+ "/"
+ df.loc[inscrits_ids, "nb_inscrits"].astype(int).astype(str)
)
# Les stat (des inscrits)
df.loc[inscrits_ids, "min"] = notes.min()
df.loc[inscrits_ids, "max"] = notes.max()
df.loc[inscrits_ids, "moy"] = notes.mean()
return df
def to_dict(self) -> dict:
"""Renvoie un dictionnaire de synthèse des moyennes/classements/statistiques"""
synthese = {
"notes": self.df_gen["note"],
"classements": self.df_gen["classement"],
"min": self.df_gen["min"].mean(),
"max": self.df_gen["max"].mean(),
"moy": self.df_gen["moy"].mean(),
"nb_inscrits": self.df_gen["nb_inscrits"].mean(),
}
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"]
def get_min(self) -> pd.Series:
"""Série des min"""
return self.df_gen["min"].round(2)
def get_max(self) -> pd.Series:
"""Série des max"""
return self.df_gen["max"].round(2)
def get_moy(self) -> pd.Series:
"""Série des moy"""
return self.df_gen["moy"].round(2)
def get_note_for_df(self, etudid: int):
"""Note d'un étudiant donné par son etudid"""
return round(self.df_gen["note"].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 = self.df_gen["rang"].loc[etudid]
if not pd.isna(classement):
return classement
else:
return pe_affichage.SANS_NOTE
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): class TableTag(object):
def __init__(self): def __init__(self):
"""Classe centralisant différentes méthodes communes aux """Classe centralisant différentes méthodes communes aux
@ -233,7 +49,6 @@ class TableTag(object):
""" """
pass pass
# -----------------------------------------------------------------------------------------------------------
def get_all_tags(self): def get_all_tags(self):
"""Liste des tags de la table, triée par ordre alphabétique, """Liste des tags de la table, triée par ordre alphabétique,
extraite des clés du dictionnaire ``moyennes_tags`` connues (tags en doublon extraite des clés du dictionnaire ``moyennes_tags`` connues (tags en doublon
@ -261,8 +76,9 @@ class TableTag(object):
tags_tries = self.get_all_tags() tags_tries = self.get_all_tags()
for tag in tags_tries: for tag in tags_tries:
moy_tag = self.moyennes_tags[tag] moy_tag = self.moyennes_tags[tag]
df = df.join(moy_tag.synthese["notes"].rename(f"Moy {tag}")) moy_gen = moy_tag.moy_gen
df = df.join(moy_tag.synthese["classements"].rename(f"Class {tag}")) df = df.join(moy_gen.synthese["notes"].rename(f"Moy {tag}"))
df = df.join(moy_gen.synthese["classements"].rename(f"Class {tag}"))
return df return df

View File

@ -65,7 +65,7 @@ class DevConfig(Config):
) )
SECRET_KEY = os.environ.get("DEV_SECRET_KEY") or "bb3faec7d9a34eb68a8e3e710087d87a" SECRET_KEY = os.environ.get("DEV_SECRET_KEY") or "bb3faec7d9a34eb68a8e3e710087d87a"
# pour le avoir url_for dans le shell: # pour le avoir url_for dans le shell:
# SERVER_NAME = "http://localhost:8080" SERVER_NAME = "http://localhost:8080"
class TestConfig(DevConfig): class TestConfig(DevConfig):