Compare commits

...

6 Commits

10 changed files with 442 additions and 744 deletions

View File

@ -1,5 +1,16 @@
from app.models import Formation, FormSemestre
from app.scodoc import codes_cursus
from app import log
PE_DEBUG = 0
if not PE_DEBUG:
# log to notes.log
def pe_print(*a, **kw):
# kw is ignored. log always add a newline
log(" ".join(a))
else:
pe_print = print # print function
def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str:
@ -29,7 +40,7 @@ def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str:
f"{semestre.date_debut.year}-{semestre.date_fin.year}",
]
if avec_fid:
description.append(f"({semestre.forsemestre_id})")
description.append(f"({semestre.formsemestre_id})")
return " ".join(description)

View File

@ -45,21 +45,11 @@ import unicodedata
from flask import g
import app.scodoc.sco_utils as scu
from app import log
from app.models import FormSemestre
from app.scodoc import sco_formsemestre
from app.scodoc.sco_logos import find_logo
PE_DEBUG = 0
if not PE_DEBUG:
# log to notes.log
def pe_print(*a, **kw):
# kw is ignored. log always add a newline
log(" ".join(a))
else:
pe_print = print # print function
# Generated LaTeX files are encoded as:
@ -382,3 +372,5 @@ def get_cosemestres_diplomants(
cosemestres[fid] = cosem
return cosemestres

View File

@ -35,42 +35,56 @@ Created on 17/01/2024
@author: barasc
"""
import pandas as pd
import app.pe.pe_comp as pe_comp
from app.models import FormSemestre, Identite
from app.pe.pe_comp import pe_print
import app.pe.pe_affichage as pe_affichage
import app.pe.pe_comp as pe_comp
from app.pe import pe_comp, pe_affichage
class EtudiantsJuryPE:
"""Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE"""
def __init__(self, annee_diplome: int):
"""
Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE
Args:
annee_diplome: L'année de diplomation
"""
self.annee_diplome = annee_diplome
"""L'année du diplôme"""
"Les identités des étudiants traités pour le jury"
self.identites = {} # ex. ETUDINFO_DICT
"Les cursus (semestres suivis, abandons) des étudiants"
"Les identités des étudiants traités pour le jury"
self.cursus = {}
"""Les aggrégats des semestres suivis (par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements) des étudiants"""
"Les cursus (semestres suivis, abandons) des étudiants"
self.trajectoires = {}
"""Les trajectoires/chemins de semestres suivis par les étudiants
pour atteindre un aggrégat donné
(par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements)"""
"Les etudids des étudiants à considérer au jury (ceux qui seront effectivement diplômés)"
self.etudiants_diplomes = {}
self.diplomes_ids = {}
"""Les identités des étudiants à considérer au jury (ceux qui seront effectivement diplômés)"""
self.diplomes_ids = {}
"""Les etudids des étudiants diplômés"""
"Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons)"
self.etudiants_ids = {}
"""Les étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant été réorientés ou ayant abandonnés)"""
"""Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons).
Il s'agit des étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant
été réorientés ou ayant abandonnés)"""
self.cosemestres: dict[int, FormSemestre] = None
"Les cosemestres donnant lieu à même année de diplome"
self.abandons = {}
"""Les étudiants qui ne seront pas diplômés à ce jury (redoublants/réorientés)"""
self.abandons_ids = {}
"""Les etudids des étudiants redoublants/réorientés"""
def find_etudiants(self, formation_id: int):
"""Liste des étudiants à prendre en compte dans le jury PE, en les recherchant
de manière automatique par rapport à leur année de diplomation ``annee_diplome``
@ -86,17 +100,19 @@ class EtudiantsJuryPE:
cosemestres = pe_comp.get_cosemestres_diplomants(self.annee_diplome, None)
self.cosemestres = cosemestres
pe_comp.pe_print(f"1) Recherche des coSemestres -> {len(cosemestres)} trouvés")
pe_affichage.pe_print(
f"1) Recherche des coSemestres -> {len(cosemestres)} trouvés"
)
pe_comp.pe_print("2) Liste des étudiants dans les différents co-semestres")
pe_affichage.pe_print("2) Liste des étudiants dans les différents co-semestres")
self.etudiants_ids = get_etudiants_dans_semestres(cosemestres)
pe_comp.pe_print(
pe_affichage.pe_print(
" => %d étudiants trouvés dans les cosemestres" % len(self.etudiants_ids)
)
# Analyse des parcours étudiants pour déterminer leur année effective de diplome
# avec prise en compte des redoublements, des abandons, ....
pe_comp.pe_print("3) Analyse des parcours individuels des étudiants")
pe_affichage.pe_print("3) Analyse des parcours individuels des étudiants")
no_etud = 0
for no_etud, etudid in enumerate(self.etudiants_ids):
@ -110,46 +126,43 @@ class EtudiantsJuryPE:
# Analyse son parcours pour atteindre chaque semestre de la formation
self.structure_cursus_etudiant(etudid)
if (no_etud + 1) % 10 == 0:
pe_comp.pe_print(f"{no_etud + 1}")
no_etud += 1
pe_comp.pe_print()
# Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris
self.etudiants_diplomes = self.get_etudiants_diplomes()
self.diplomes_ids = set(self.etudiants_diplomes.keys())
self.etudiants_ids = set(self.identites)
self.etudiants_ids = set(self.identites.keys())
"""Les étudiants dont il faut calculer les moyennes"""
self.formsemestres_jury_ids = self.get_formsemestres()
"""Les formsemestres (des étudiants) dont il faut calculer les moyennes"""
# Les abandons (pour debug)
self.abandons = self.get_etudiants_redoublants_ou_reorientes()
self.abandons_ids = set(self.abandons)
# Synthèse
pe_comp.pe_print(
pe_affichage.pe_print(
f" => {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}"
)
nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes)
pe_comp.pe_print(f" => {nbre_abandons} étudiants éliminer pour abandon")
pe_comp.pe_print(
assert nbre_abandons == len(self.abandons_ids)
pe_affichage.pe_print(
f" => {nbre_abandons} étudiants non considérés (redoublement, réorientation, abandon"
)
pe_affichage.pe_print(
f" => {len(self.formsemestres_jury_ids)} semestres dont il faut calculer la moyenne"
)
pe_comp.pe_print(
" => quelques étudiants futurs diplômés : "
+ ", ".join([str(etudid) for etudid in list(self.etudiants_diplomes)[:10]])
)
pe_comp.pe_print(
" => semestres dont il faut calculer les moyennes : "
+ ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)])
)
# Les abandons :
self.abandons = sorted(
[
cursus["nom"]
for etudid, cursus in self.cursus.items()
if etudid not in self.diplomes_ids
]
)
# pe_affichage.pe_print(
# " => quelques étudiants futurs diplômés : "
# + ", ".join([str(etudid) for etudid in list(self.etudiants_diplomes)[:10]])
# )
# pe_affichage.pe_print(
# " => semestres dont il faut calculer les moyennes : "
# + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)])
# )
def get_etudiants_diplomes(self) -> dict[int, Identite]:
"""Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}`
@ -169,6 +182,23 @@ class EtudiantsJuryPE:
etudiants = {etudid: self.identites[etudid] for etudid in etudids}
return etudiants
def get_etudiants_redoublants_ou_reorientes(self) -> dict[int, Identite]:
"""Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}`
dont les notes seront prises en compte (pour les classements) mais qui n'apparaitront
pas dans le jury car diplômé une autre année (redoublants) ou réorienté ou démissionnaire.
Returns:
Un dictionnaire `{etudid: Identite(etudid)}`
"""
etudids = [
etudid
for etudid in self.cursus
if self.cursus[etudid]["diplome"] != self.annee_diplome
or self.cursus[etudid]["abandon"] is True
]
etudiants = {etudid: self.identites[etudid] for etudid in etudids}
return etudiants
def analyse_etat_etudiant(self, etudid: int, cosemestres: dict[int, FormSemestre]):
"""Analyse le cursus d'un étudiant pouvant être :
@ -259,10 +289,19 @@ class EtudiantsJuryPE:
} # les semestres de n°i de l'étudiant
self.cursus[etudid][nom_sem] = semestres_i
def get_trajectoire(self, etudid: int, formsemestre_final: FormSemestre):
def get_trajectoire(
self, etudid: int, formsemestre_final: FormSemestre, nom_aggregat: str
):
"""Ensemble des semestres parcourus par
un étudiant pour l'amener à un semestre terminal.
Si nom_aggregat est de type "Si", limite les semestres à ceux de numéro i.
Par ex: si formsemestre_terminal est un S3 et nom_agrregat "S3", ne prend en compte que les
semestres 3.
Si nom_aggregat est de type "iA" ou "iS" (incluant plusieurs numéros de semestres), prend en
compte les dit numéros de semestres.
Par ex: si formsemestre_terminal est un S3, ensemble des S1,
S2, S3 suivi pour l'amener au S3 (il peut y avoir plusieurs S1,
ou S2, ou S3 s'il a redoublé).
@ -277,12 +316,21 @@ class EtudiantsJuryPE:
numero_semestre_terminal = formsemestre_final.semestre_id
semestres_significatifs = self.get_semestres_significatifs(etudid)
# Semestres de n° inférieur (pax ex: des S1, S2, S3 pour un S3 terminal)
# et qui lui sont antérieurs
if nom_aggregat.startswith("S"): # les semestres
numero_semestres_possibles = [numero_semestre_terminal]
elif nom_aggregat.endswith("A"): # les années
numero_semestres_possibles = [
int(sem[-1]) for sem in pe_comp.PARCOURS[nom_aggregat]["aggregat"]
]
assert numero_semestre_terminal in numero_semestres_possibles
else: # les xS = tous les semestres jusqu'à Sx (pax ex: des S1, S2, S3 pour un S3 terminal)
numero_semestres_possibles = list(range(1, numero_semestre_terminal + 1))
semestres_aggreges = {}
for fid, semestre in semestres_significatifs.items():
# Semestres parmi ceux de n° possibles & qui lui sont antérieurs
if (
semestre.semestre_id <= numero_semestre_terminal
semestre.semestre_id in numero_semestres_possibles
and semestre.date_fin <= formsemestre_final.date_fin
):
semestres_aggreges[fid] = semestre
@ -373,14 +421,65 @@ class EtudiantsJuryPE:
else:
raise ValueError("Probleme de paramètres d'appel dans get_formsemestreids")
def nbre_etapes_max_diplomes(self):
"""Connaissant les étudiants diplomes du jury PE,
def nbre_etapes_max_diplomes(self, etudids: list[int]) -> int:
"""Partant d'un ensemble d'étudiants,
nombre de semestres (étapes) maximum suivis par les étudiants du jury.
Args:
etudids: Liste d'étudid d'étudiants
"""
nbres_semestres = []
for etudid in self.diplomes_ids:
for etudid in etudids:
nbres_semestres.append(self.cursus[etudid]["nb_semestres"])
return max(nbres_semestres)
if not nbres_semestres:
return 0
else:
return max(nbres_semestres)
def df_administratif(self, etudids: list[int]) -> pd.DataFrame:
"""Synthétise toutes les données administratives d'un groupe
d'étudiants fournis par les etudid dans un dataFrame
Args:
etudids: La liste des étudiants à prendre en compte
"""
etudids = list(etudids)
# Récupération des données des étudiants
administratif = {}
nbre_semestres_max = self.nbre_etapes_max_diplomes(etudids)
for etudid in etudids:
etudiant = self.identites[etudid]
cursus = self.cursus[etudid]
formsemestres = cursus["formsemestres"]
if cursus["diplome"]:
diplome = cursus["diplome"]
else:
diplome = "indéterminé"
administratif[etudid] = {
"Nom": etudiant.nom,
"Prenom": etudiant.prenom,
"Civilite": etudiant.civilite_str,
"Age": pe_comp.calcul_age(etudiant.date_naissance),
"Date d'entree": cursus["entree"],
"Date de diplome": diplome,
"Nbre de semestres": len(formsemestres),
}
# Ajout des noms de semestres parcourus
etapes = pe_affichage.etapes_du_cursus(formsemestres, nbre_semestres_max)
administratif[etudid] |= etapes
# Construction du dataframe
df = pd.DataFrame.from_dict(administratif, orient="index")
# Tri par nom/prénom
df.sort_values(by=["Nom", "Prenom"], inplace=True)
return df
def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set:
@ -402,7 +501,7 @@ def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set:
for sem in semestres.values(): # pour chacun des semestres de la liste
etudiants_du_sem = {ins.etudid for ins in sem.inscriptions}
pe_print(f" --> {sem} : {len(etudiants_du_sem)} etudiants")
pe_affichage.pe_print(f" --> {sem} : {len(etudiants_du_sem)} etudiants")
etudiants_ids = (
etudiants_ids | etudiants_du_sem
) # incluant la suppression des doublons

View File

@ -1,22 +1,15 @@
from app.comp import moy_sem
from app.pe.pe_tabletags import TableTag
from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_trajectoire import Trajectoire, TrajectoiresJuryPE
from app.pe.pe_trajectoiretag import TrajectoireTag
from app.comp import moy_sem
import pandas as pd
import numpy as np
class AggregatInterclasseTag(TableTag):
"""Interclasse l'ensemble des étudiants diplômés à une année
donnée (celle du jury), pour un aggrégat donné (par ex: 'S2', '3S')
en reportant :
* les moyennes obtenues sur la trajectoire qu'il ont suivi pour atteindre le numéro de semestre de fin de l'aggrégat (indépendamment de son
formsemestres)
* calculant le classement sur les étudiants diplômes
"""
# -------------------------------------------------------------------------------------------------------------------
def __init__(
@ -26,12 +19,26 @@ class AggregatInterclasseTag(TableTag):
trajectoires_jury_pe: TrajectoiresJuryPE,
trajectoires_taggues: dict[tuple, TrajectoireTag],
):
# Table nommée au nom de l'aggrégat (par ex: 3S)
TableTag.__init__(self, nom_aggregat)
"""
Interclasse l'ensemble des étudiants diplômés à une année
donnée (celle du jury), pour un aggrégat donné (par ex: 'S2', '3S')
en reportant :
* les moyennes obtenues sur la trajectoire qu'il ont suivi pour atteindre le numéro de semestre de fin de l'aggrégat (indépendamment de son
formsemestres)
* calculant le classement sur les étudiants diplômes
"""
TableTag.__init__(self)
# Le nom
self.aggregat = nom_aggregat
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}
# Les trajectoires (et leur version tagguées), en ne gardant que celles associées à l'aggrégat
self.trajectoires: dict[int, Trajectoire] = {}
@ -72,9 +79,13 @@ class AggregatInterclasseTag(TableTag):
"nb_inscrits": len(moy_gen_tag),
}
# Est significatif ? (aka a-t-il des tags et des notes)
self.significatif = len(self.tags_sorted) > 0
def get_repr(self) -> str:
"""Une représentation textuelle"""
return f"Aggrégat {self.nom}"
return f"Aggrégat {self.aggregat}"
def do_taglist(self):
"""Synthétise les tags à partir des trajectoires_tagguées

View File

@ -46,14 +46,7 @@ import io
import os
from zipfile import ZipFile
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
from app.models.etudiants import Identite
from app.scodoc.gen_tables import GenTable, SeqGenTable
import app.scodoc.sco_utils as scu
from app.scodoc.gen_tables import SeqGenTable
from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_trajectoire import TrajectoiresJuryPE, Trajectoire
import app.pe.pe_comp as pe_comp
@ -63,12 +56,8 @@ from app.pe.pe_trajectoiretag import TrajectoireTag
import app.pe.pe_affichage as pe_affichage
import pandas as pd
import numpy as np
# ----------------------------------------------------------------------------------------
# ----------------------------------------------------------------------------------------
class JuryPE(object):
"""Classe mémorisant toutes les informations nécessaires pour établir un jury de PE.
Modèle basé sur NotesTable.
@ -110,9 +99,10 @@ class JuryPE(object):
"Nom du zip où ranger les fichiers générés"
self.zipdata = io.BytesIO()
with ZipFile(self.zipdata, "w") as zipfile:
# Chargement des étudiants à prendre en compte dans le jury
pe_comp.pe_print(
pe_affichage.pe_print(
f"""*** Recherche et chargement des étudiants diplômés en {
self.diplome} pour la formation {self.formation_id}"""
)
@ -122,85 +112,126 @@ class JuryPE(object):
self.etudiants.find_etudiants(self.formation_id)
self.diplomes_ids = self.etudiants.diplomes_ids
# Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE
pe_comp.pe_print("*** Génère les semestres taggués")
self.semestres_taggues = compute_semestres_tag(self.etudiants)
if pe_comp.PE_DEBUG:
# Intègre le bilan des semestres taggués au zip final
for formsemestretag in self.semestres_taggues.values():
filename = formsemestretag.nom.replace(" ", "_") + ".csv"
pe_comp.pe_print(f" - Export csv de {filename} ")
self.add_file_to_zip(
zipfile,
filename,
formsemestretag.str_tagtable(),
path="details_semestres",
)
# Génère les trajectoires (combinaison de semestres suivis
# par un étudiant pour atteindre le semestre final d'un aggrégat)
pe_comp.pe_print(
"*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants"
)
self.trajectoires = TrajectoiresJuryPE(self.diplome)
self.trajectoires.cree_trajectoires(self.etudiants)
# Génère les moyennes par tags des trajectoires
pe_comp.pe_print(
"*** Calcule les moyennes par tag des trajectoires possibles"
)
self.trajectoires_tagguees = compute_trajectoires_tag(
self.trajectoires, self.etudiants, self.semestres_taggues
)
if pe_comp.PE_DEBUG:
# Intègre le bilan des trajectoires tagguées au zip final
for trajectoire_tagguee in self.trajectoires_tagguees.values():
filename = trajectoire_tagguee.get_repr().replace(" ", "_") + ".csv"
pe_comp.pe_print(f" - Export csv de {filename} ")
self.add_file_to_zip(
zipfile,
filename,
trajectoire_tagguee.str_tagtable(),
path="details_semestres",
)
# Génère les interclassements (par promo et) par (nom d') aggrégat
pe_comp.pe_print("*** Génère les interclassements par aggrégat")
self.interclassements_taggues = compute_interclassements(
self.etudiants, self.trajectoires, self.trajectoires_tagguees
)
if pe_comp.PE_DEBUG:
# Intègre le bilan des aggrégats (par promo) au zip final
for interclass_tag in self.interclassements_taggues.values():
filename = interclass_tag.get_repr().replace(" ", "_") + ".csv"
pe_comp.pe_print(f" - Export csv de {filename} ")
self.add_file_to_zip(
zipfile,
filename,
interclass_tag.str_tagtable(),
path="details_semestres",
)
# Synthèse des éléments du jury PE
self.synthese = self.synthetise_juryPE()
# Export des données => mode 1 seule feuille -> supprimé
pe_comp.pe_print("*** Export du jury de synthese")
# Intègre le bilan des semestres taggués au zip final
output = io.BytesIO()
with pd.ExcelWriter(output, engine="openpyxl") as writer:
for onglet, df in self.synthese.items():
# écriture dans l'onglet:
df.to_excel(writer, onglet, index=True, header=True)
if self.diplomes_ids:
onglet = "diplômés"
df_diplome = self.etudiants.df_administratif(self.diplomes_ids)
df_diplome.to_excel(writer, onglet, index=True, header=True)
if self.etudiants.abandons_ids:
onglet = "redoublants-réorientés"
df_abandon = self.etudiants.df_administratif(
self.etudiants.abandons_ids
)
df_abandon.to_excel(writer, onglet, index=True, header=True)
output.seek(0)
self.add_file_to_zip(
zipfile, f"synthese_jury_{self.diplome}.xlsx", output.read()
zipfile,
f"etudiants_{self.diplome}.xlsx",
output.read(),
path="details",
)
if not self.diplomes_ids:
pe_affichage.pe_tools("*** Aucun étudiant diplômé")
else:
# 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")
self.semestres_taggues = compute_semestres_tag(self.etudiants)
# Intègre le bilan des semestres taggués au zip final
output = io.BytesIO()
with pd.ExcelWriter(output, engine="openpyxl") as writer:
for formsemestretag in self.semestres_taggues.values():
onglet = formsemestretag.nom
df = formsemestretag.df_moyennes_et_classements()
# écriture dans l'onglet
df.to_excel(writer, onglet, index=True, header=True)
output.seek(0)
self.add_file_to_zip(
zipfile,
f"semestres_taggues_{self.diplome}.xlsx",
output.read(),
path="details",
)
# Génère les trajectoires (combinaison de semestres suivis
# par un étudiant pour atteindre le semestre final d'un aggrégat)
pe_affichage.pe_print(
"*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants"
)
self.trajectoires = TrajectoiresJuryPE(self.diplome)
self.trajectoires.cree_trajectoires(self.etudiants)
# Génère les moyennes par tags des trajectoires
pe_affichage.pe_print(
"*** Calcule les moyennes par tag des trajectoires possibles"
)
self.trajectoires_tagguees = compute_trajectoires_tag(
self.trajectoires, self.etudiants, self.semestres_taggues
)
# Intègre le bilan des trajectoires tagguées au zip final
output = io.BytesIO()
with pd.ExcelWriter(output, engine="openpyxl") as writer:
for trajectoire_tagguee in self.trajectoires_tagguees.values():
onglet = trajectoire_tagguee.get_repr()
df = trajectoire_tagguee.df_moyennes_et_classements()
# écriture dans l'onglet
df.to_excel(writer, onglet, index=True, header=True)
output.seek(0)
self.add_file_to_zip(
zipfile,
f"trajectoires_taggues_{self.diplome}.xlsx",
output.read(),
path="details",
)
# 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")
self.interclassements_taggues = compute_interclassements(
self.etudiants, self.trajectoires, self.trajectoires_tagguees
)
# Intègre le bilan des aggrégats (interclassé par promo) au zip final
output = io.BytesIO()
with pd.ExcelWriter(output, engine="openpyxl") as writer:
for interclass_tag in self.interclassements_taggues.values():
if interclass_tag.significatif: # Avec des notes
onglet = interclass_tag.get_repr()
df = interclass_tag.df_moyennes_et_classements()
# écriture dans l'onglet
df.to_excel(writer, onglet, index=True, header=True)
output.seek(0)
self.add_file_to_zip(
zipfile,
f"interclassements_taggues_{self.diplome}.xlsx",
output.read(),
path="details",
)
# Synthèse des éléments du jury PE
self.synthese = self.synthetise_juryPE()
# Export des données => mode 1 seule feuille -> supprimé
pe_affichage.pe_print("*** Export du jury de synthese")
output = io.BytesIO()
with pd.ExcelWriter(output, engine="openpyxl") as writer:
for onglet, df in self.synthese.items():
# écriture dans l'onglet:
df.to_excel(writer, onglet, index=True, header=True)
output.seek(0)
self.add_file_to_zip(
zipfile, f"synthese_jury_{self.diplome}.xlsx", output.read()
)
# Fin !!!! Tada :)
def add_file_to_zip(self, zipfile: ZipFile, filename: str, data, path=""):
@ -241,58 +272,18 @@ class JuryPE(object):
def synthetise_juryPE(self):
"""Synthétise tous les résultats du jury PE dans des dataframes"""
pe_comp.pe_print("*** Synthèse finale des moyennes ***")
pe_affichage.pe_print("*** Synthèse finale des moyennes ***")
synthese = {}
pe_comp.pe_print(" -> Synthèse des données administratives")
synthese["administratif"] = self.df_administratif()
pe_affichage.pe_print(" -> Synthèse des données administratives")
synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids)
tags = self.do_tags_list(self.interclassements_taggues)
for tag in tags:
pe_comp.pe_print(f" -> Synthèse du tag {tag}")
pe_affichage.pe_print(f" -> Synthèse du tag {tag}")
synthese[tag] = self.df_tag(tag)
return synthese
def df_administratif(self):
"""Synthétise toutes les données administratives des étudiants"""
etudids = list(self.diplomes_ids)
# Récupération des données des étudiants
administratif = {}
nbre_semestres_max = self.etudiants.nbre_etapes_max_diplomes()
for etudid in etudids:
etudiant = self.etudiants.identites[etudid]
cursus = self.etudiants.cursus[etudid]
formsemestres = cursus["formsemestres"]
if cursus["diplome"]:
diplome = cursus["diplome"]
else:
diplome = "indéterminé"
administratif[etudid] = {
"Nom": etudiant.nom,
"Prenom": etudiant.prenom,
"Civilite": etudiant.civilite_str,
"Age": pe_comp.calcul_age(etudiant.date_naissance),
"Date d'entree": cursus["entree"],
"Date de diplome": diplome,
"Nbre de semestres": len(formsemestres),
}
# Ajout des noms de semestres parcourus
etapes = pe_affichage.etapes_du_cursus(formsemestres, nbre_semestres_max)
administratif[etudid] |= etapes
# Construction du dataframe
df = pd.DataFrame.from_dict(administratif, orient="index")
# Tri par nom/prénom
df.sort_values(by=["Nom", "Prenom"], inplace=True)
return df
def df_tag(self, tag):
"""Génère le DataFrame synthétisant les moyennes/classements (groupe,
interclassement promo) pour tous les aggrégats prévus,
@ -318,40 +309,40 @@ class JuryPE(object):
}
for aggregat in aggregats:
"""La trajectoire de l'étudiant sur l'aggrégat"""
# La trajectoire de l'étudiant sur l'aggrégat
trajectoire = self.trajectoires.suivi[etudid][aggregat]
"""Les moyennes par tag de cette trajectoire"""
# Les moyennes par tag de cette trajectoire
donnees[etudid] |= {
f"{aggregat} notes ": "-",
f"{aggregat} class. (groupe)": "-",
f"{aggregat} min | moy | max (groupe)": "-",
}
if trajectoire:
trajectoire_tagguee = self.trajectoires_tagguees[
trajectoire.trajectoire_id
]
bilan = trajectoire_tagguee.moyennes_tags[tag]
if tag in trajectoire_tagguee.moyennes_tags:
bilan = trajectoire_tagguee.moyennes_tags[tag]
donnees[etudid] |= {
f"{aggregat} notes ": f"{bilan['notes'].loc[etudid]:.1f}",
f"{aggregat} class. (groupe)": f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}",
f"{aggregat} min/moy/max (groupe)": f"{bilan['min']:.1f}/{bilan['moy']:.1f}/{bilan['max']:.1f}",
}
else:
donnees[etudid] |= {
f"{aggregat} notes ": "-",
f"{aggregat} class. (groupe)": "-",
f"{aggregat} min/moy/max (groupe)": "-",
}
donnees[etudid] |= {
f"{aggregat} notes ": round(bilan["notes"].loc[etudid], 2),
f"{aggregat} class. (groupe)": f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}",
f"{aggregat} min | moy | max (groupe)": f"{bilan['min']:.1f} | {bilan['moy']:.1f} | {bilan['max']:.1f}",
}
"""L'interclassement"""
interclass = self.interclassements_taggues[aggregat]
donnees[etudid] |= {
f"{aggregat} class. (promo)": "-",
f"{aggregat} min | moy | max (promo)": "-",
}
if tag in interclass.moyennes_tags:
bilan = interclass.moyennes_tags[tag]
donnees[etudid] |= {
f"{aggregat} class. (promo)": f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}",
f"{aggregat} min/moy/max (promo)": f"{bilan['min']:.1f}/{bilan['moy']:.1f}/{bilan['max']:.1f}",
}
else:
donnees[etudid] |= {
f"{aggregat} class. (promo)": "-",
f"{aggregat} min/moy/max (promo)": "-",
f"{aggregat} min | moy | max (promo)": f"{bilan['min']:.1f} | {bilan['moy']:.1f} | {bilan['max']:.1f}",
}
# Fin de l'aggrégat
@ -362,17 +353,6 @@ class JuryPE(object):
df.sort_values(by=["Nom", "Prenom"], inplace=True)
return df
def table_syntheseJury(self, mode="singlesheet"): # was str_syntheseJury
"""Table(s) du jury
mode: singlesheet ou multiplesheet pour export excel
"""
sT = SeqGenTable() # le fichier excel à générer
if mode == "singlesheet":
return sT.get_genTable("singlesheet")
else:
return sT
def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
"""Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés.
@ -389,7 +369,7 @@ def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
"""
"""Création des semestres taggués, de type 'S1', 'S2', ..."""
pe_comp.pe_print("*** Création des semestres taggués")
pe_affichage.pe_print("*** Création des semestres taggués")
formsemestres = etudiants.get_formsemestres(
semestres_recherches=pe_comp.TOUS_LES_SEMESTRES
@ -397,19 +377,11 @@ def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
semestres_tags = {}
for frmsem_id, formsemestre in formsemestres.items():
"""Choix d'un nom pour le semestretag"""
nom = "S%d %d %d-%d" % (
formsemestre.semestre_id,
frmsem_id,
formsemestre.date_debut.year,
formsemestre.date_fin.year,
)
pe_comp.pe_print(f" --> Semestre taggué {nom} sur la base de {formsemestre}")
# Crée le semestre_tag et exécute les calculs de moyennes
formsemestretag = SemestreTag(nom, frmsem_id)
formsemestretag = SemestreTag(frmsem_id)
pe_affichage.pe_print(
f" --> Semestre taggué {formsemestretag.nom} sur la base de {formsemestre}"
)
# Stocke le semestre taggué
semestres_tags[frmsem_id] = formsemestretag
@ -442,23 +414,16 @@ def compute_trajectoires_tag(
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)} }
Un dictionnaire de la forme ``{nom_aggregat: {fid_terminal: SetTag(fid_terminal)} }``
"""
pe_comp.pe_print(" *** Création des aggrégats ")
trajectoires_tagguees = {}
for trajectoire_id in trajectoires.trajectoires:
trajectoire = trajectoires.trajectoires[trajectoire_id]
for trajectoire_id, trajectoire in trajectoires.trajectoires.items():
nom = trajectoire.get_repr()
pe_comp.pe_print(f" --> Fusion {nom}")
trajectoire_tagguee = TrajectoireTag(nom, trajectoire, semestres_taggues)
"""Trajectoire_tagguee associée"""
"""Mémorise le résultat"""
pe_affichage.pe_print(f" --> Aggrégat {nom}")
# Trajectoire_tagguee associée
trajectoire_tagguee = TrajectoireTag(trajectoire, semestres_taggues)
# Mémorise le résultat
trajectoires_tagguees[trajectoire_id] = trajectoire_tagguee
return trajectoires_tagguees
@ -473,11 +438,9 @@ def compute_interclassements(
pour fournir un classement sur la promo. Le classement est établi au regard du nombre
d'étudiants ayant participé au même aggrégat.
"""
pe_comp.pe_print(" Interclassement sur la promo")
aggregats_interclasses_taggues = {}
for nom_aggregat in pe_comp.TOUS_LES_SEMESTRES + pe_comp.TOUS_LES_AGGREGATS:
pe_comp.pe_print(f" --> {nom_aggregat}")
pe_affichage.pe_print(f" --> Interclassement {nom_aggregat}")
interclass = AggregatInterclasseTag(
nom_aggregat, etudiants, trajectoires_jury_pe, trajectoires_tagguees
)

View File

@ -45,63 +45,63 @@ 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_comp as pe_comp
from app.pe.pe_tabletags import (TableTag, TAGS_RESERVES)
import app.pe.pe_affichage as pe_affichage
from app.pe.pe_tabletags import TableTag, TAGS_RESERVES
import pandas as pd
class SemestreTag(TableTag):
"""Un SemestreTag 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.
"""
# -----------------------------------------------------------------------------
# Fonctions d'initialisation
# -----------------------------------------------------------------------------
def __init__(self, nom: str, formsemestre_id: int):
def __init__(self, formsemestre_id: int):
"""
Un SemestreTag 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.
Args:
nom: Nom à donner au SemestreTag
formsemestre_id: Identifiant du FormSemestre sur lequel il se base
formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base
"""
TableTag.__init__(self, nom=nom)
TableTag.__init__(self)
"""Le semestre"""
# Le semestre
self.formsemestre_id = formsemestre_id
self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
"""Les résultats du semestre"""
# Le nom du semestre taggué
self.nom = self.get_repr()
# Les résultats du semestre
self.nt = load_formsemestre_results(self.formsemestre)
"""Les étudiants"""
# Les étudiants
self.etuds = self.nt.etuds
self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds}
"""Les notes, les modules implémentés triés, les étudiants, les coeffs,
récupérés notamment de py:mod:`res_but`
"""
# 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 au module et les dispenses d'UE"""
# Les inscriptions au module et les dispenses d'UE
self.modimpl_inscr_df = self.nt.modimpl_inscr_df
self.ues = self.nt.ues
self.ues_inscr_parcours_df = self.nt.load_ues_inscr_parcours()
self.dispense_ues = self.nt.dispense_ues
"""Les tags (en supprimant les tags réservés)"""
# Les tags (en supprimant les tags réservés)
self.tags = get_synthese_tags_semestre(self.nt.formsemestre)
for tag in TAGS_RESERVES:
if tag in self.tags:
del self.tags[tag]
"""Calcul des moyennes & les classements de chaque étudiant à chaque tag"""
# Calcul des moyennes & les classements de chaque étudiant à chaque tag
self.moyennes_tags = {}
for tag in self.tags:
pe_comp.pe_print(f" -> Traitement du tag {tag}")
# pe_affichage.pe_print(f" -> Traitement du tag {tag}")
moy_gen_tag = self.compute_moyenne_tag(tag)
class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int
class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int
self.moyennes_tags[tag] = {
"notes": moy_gen_tag,
"classements": class_gen_tag,
@ -111,8 +111,7 @@ class SemestreTag(TableTag):
"nb_inscrits": len(moy_gen_tag),
}
"""Ajoute les moyennes générales de BUT pour le semestre considéré"""
pe_comp.pe_print(f" -> Traitement du tag but")
# Ajoute les moyennes générales de BUT pour le semestre considéré
moy_gen_but = self.nt.etud_moy_gen
class_gen_but = self.nt.etud_moy_gen_ranks_int
self.moyennes_tags["but"] = {
@ -124,16 +123,18 @@ class SemestreTag(TableTag):
"nb_inscrits": len(moy_gen_but),
}
"""Synthétise l'ensemble des moyennes dans un dataframe"""
self.tags_sorted = sorted(self.moyennes_tags) # les tags par ordre alphabétique
self.notes = self.df_tagtable() # Le dataframe synthétique des notes (=moyennes par tag)
# Synthétise l'ensemble des moyennes dans un dataframe
self.tags_sorted = sorted(self.moyennes_tags) # les tags par ordre alphabétique
self.notes = (
self.df_notes()
) # Le dataframe synthétique des notes (=moyennes par tag)
# -----------------------------------------------------------------------------
def get_etudids(self):
"""Renvoie la liste des etud_id des étudiants inscrits au semestre"""
return [etud["etudid"] for etud in self.inscrlist]
pe_affichage.pe_print(f" => Traitement des tags {', '.join(self.tags_sorted)}")
def get_repr(self):
"""Nom affiché pour le semestre taggué"""
return pe_affichage.nom_semestre_etape(self.formsemestre, avec_fid=True)
# -----------------------------------------------------------------------------
def compute_moyenne_tag(self, tag: str) -> list:
"""Calcule la moyenne des étudiants pour le tag indiqué,
pour ce SemestreTag.
@ -192,14 +193,22 @@ class SemestreTag(TableTag):
return moy_gen_tag
# -----------------------------------------------------------------------------
def get_noteEtCoeff_modimpl(self, modimpl_id, etudid, profondeur=2):
"""Renvoie un couple donnant la note et le coeff normalisé d'un étudiant à un module d'id modimpl_id.
La note et le coeff sont extraits :
1) soit des données du semestre en normalisant le coefficient par rapport à la somme des coefficients des modules du semestre,
2) soit des données des UE précédemment capitalisées, en recherchant un module de même CODE que le modimpl_id proposé,
le coefficient normalisé l'étant alors par rapport au total des coefficients du semestre auquel appartient l'ue capitalisée
TODO:: A rependre si nécessaire
"""
def get_ue_capitalisees(etudid) -> list[dict]:
"""Renvoie la liste des capitalisation effectivement capitalisées par un étudiant"""
if etudid in self.nt.validations.ue_capitalisees.index:
return self.nt.validations.ue_capitalisees.loc[[etudid]].to_dict("records")
return []
(note, coeff_norm) = (None, None)
modimpl = get_moduleimpl(modimpl_id) # Le module considéré
@ -207,7 +216,7 @@ class SemestreTag(TableTag):
return (None, None)
# Y-a-t-il eu capitalisation d'UE ?
ue_capitalisees = self.get_ue_capitalisees(
ue_capitalisees = get_ue_capitalisees(
etudid
) # les ue capitalisées des étudiants
ue_capitalisees_id = {
@ -273,140 +282,7 @@ class SemestreTag(TableTag):
# Sinon - pas de notes à prendre en compte
return (note, coeff_norm)
# -----------------------------------------------------------------------------
def get_ue_capitalisees(self, etudid) -> list[dict]:
"""Renvoie la liste des capitalisation effectivement capitalisées par un étudiant"""
if etudid in self.nt.validations.ue_capitalisees.index:
return self.nt.validations.ue_capitalisees.loc[[etudid]].to_dict("records")
return []
# -----------------------------------------------------------------------------
# Fonctions d'affichage (et d'export csv) des données du semestre en mode debug
# -----------------------------------------------------------------------------
def str_detail_resultat_d_un_tag(self, tag, etudid=None, delim=";"):
"""Renvoie une chaine de caractère décrivant les résultats d'étudiants à un tag :
rappelle les notes obtenues dans les modules à prendre en compte, les moyennes et les rangs calculés.
Si etudid=None, tous les étudiants inscrits dans le semestre sont pris en compte. Sinon seuls les étudiants indiqués sont affichés.
"""
# Entete
chaine = delim.join(["%15s" % "nom", "etudid"]) + delim
taglist = self.get_all_tags()
if tag in taglist:
for mod in self.tagdict[tag].values():
chaine += mod["module_code"] + delim
chaine += ("%1.1f" % mod["ponderation"]) + delim
chaine += "coeff" + delim
chaine += delim.join(
["moyenne", "rang", "nbinscrit", "somme_coeff", "somme_coeff"]
) # ligne 1
chaine += "\n"
# Différents cas de boucles sur les étudiants (de 1 à plusieurs)
if etudid == None:
lesEtuds = self.get_etudids()
elif isinstance(etudid, str) and etudid in self.get_etudids():
lesEtuds = [etudid]
elif isinstance(etudid, list):
lesEtuds = [eid for eid in self.get_etudids() if eid in etudid]
else:
lesEtuds = []
for etudid in lesEtuds:
descr = (
"%15s" % self.nt.get_nom_short(etudid)[:15]
+ delim
+ str(etudid)
+ delim
)
if tag in taglist:
for modimpl_id in self.tagdict[tag]:
(note, coeff) = self.get_noteEtCoeff_modimpl(modimpl_id, etudid)
descr += (
(
"%2.2f" % note
if note != None and isinstance(note, float)
else str(note)
)
+ delim
+ (
"%1.5f" % coeff
if coeff != None and isinstance(coeff, float)
else str(coeff)
)
+ delim
+ (
"%1.5f" % (coeff * self.somme_coeffs)
if coeff != None and isinstance(coeff, float)
else "???" # str(coeff * self._sum_coeff_semestre) # voir avec Cléo
)
+ delim
)
moy = self.get_moy_from_resultats(tag, etudid)
rang = self.get_rang_from_resultats(tag, etudid)
coeff = self.get_coeff_from_resultats(tag, etudid)
tot = (
coeff * self.somme_coeffs
if coeff != None
and self.somme_coeffs != None
and isinstance(coeff, float)
else None
)
descr += (
pe_tagtable.TableTag.str_moytag(
moy, rang, len(self.get_etudids()), delim=delim
)
+ delim
)
descr += (
(
"%1.5f" % coeff
if coeff != None and isinstance(coeff, float)
else str(coeff)
)
+ delim
+ (
"%.2f" % (tot)
if tot != None
else str(coeff) + "*" + str(self.somme_coeffs)
)
)
chaine += descr
chaine += "\n"
return chaine
def str_tagsModulesEtCoeffs(self):
"""Renvoie une chaine affichant la liste des tags associés au semestre,
les modules qui les concernent et les coeffs de pondération.
Plus concrètement permet d'afficher le contenu de self._tagdict"""
chaine = "Semestre %s d'id %d" % (self.nom, id(self)) + "\n"
chaine += " -> somme de coeffs: " + str(self.somme_coeffs) + "\n"
taglist = self.get_all_tags()
for tag in taglist:
chaine += " > " + tag + ": "
for modid, mod in self.tagdict[tag].items():
chaine += (
mod["module_code"]
+ " ("
+ str(mod["coeff"])
+ "*"
+ str(mod["ponderation"])
+ ") "
+ str(modid)
+ ", "
)
chaine += "\n"
return chaine
# ************************************************************************
# Fonctions diverses
# ************************************************************************
# *********************************************
# -----------------------------------------------------------------------------
def get_moduleimpl(modimpl_id) -> dict:
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
modimpl = db.session.get(ModuleImpl, modimpl_id)
@ -420,7 +296,6 @@ def get_moduleimpl(modimpl_id) -> dict:
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
@ -437,8 +312,8 @@ def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float:
def get_synthese_tags_semestre(formsemestre: FormSemestre):
"""Etant données les implémentations des modules du semestre (modimpls),
synthétise les tags les concernant (tags saisis dans le programme pédagogique)
en les associant aux modimpls qui les concernent (modimpl_id, module_id,
le code du module, coeff et pondération fournie avec le tag (par défaut 1 si non indiquée)).
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)).
{ tagname1: { modimpl_id1: { 'module_id': ...,
'coeff': ...,
@ -451,6 +326,9 @@ def get_synthese_tags_semestre(formsemestre: FormSemestre):
Args:
formsemestre: Le formsemestre à la base de la recherche des tags
Return:
Un dictionnaire de tags
"""
synthese_tags = {}

View File

@ -48,38 +48,13 @@ TAGS_RESERVES = ["but"]
class TableTag(object):
"""
Classe mémorisant les moyennes des étudiants à différents tags et permettant de
calculer des rangs et des statistiques.
Ses attributs sont:
* nom : Nom représentatif des données de la Table
* inscrlist : Les étudiants inscrits dans le TagTag avec leur information de la forme :
{ etudid : dictionnaire d'info extrait de Scodoc, ...}
* taglist : Liste triée des noms des tags
* resultats : Dictionnaire donnant les notes-moyennes de chaque étudiant par tag et la somme commulée
des coeff utilisées dans le calcul de la moyenne pondérée, sous la forme :
{ tag : { etudid: (note_moy, somme_coeff_norm),
...}
* rangs : Dictionnaire donnant les rang par tag de chaque étudiant de la forme :
{ tag : {etudid: rang, ...} }
* nbinscrits : Nombre d'inscrits dans le semestre (pas de distinction entre les tags)
* statistiques : Dictionnaire donnant les statistiques (moyenne, min, max) des résultats par tag de la forme :
{ tag : (moy, min, max), ...}
"""
def __init__(self, nom: str):
"""Les attributs basiques des TagTable, qui seront initialisés
dans les classes dérivées
def __init__(self):
"""Classe centralisant différentes méthodes communes aux
SemestreTag, TrajectoireTag, AggregatInterclassTag
"""
self.nom = nom
"""Les étudiants"""
self.etudiants = {}
"""Les moyennes par tag"""
self.moyennes_tags = {}
pass
# -----------------------------------------------------------------------------------------------------------
def get_all_tags(self):
@ -90,190 +65,21 @@ class TableTag(object):
"""
return sorted(self.moyennes_tags.keys())
# *****************************************************************************************************************
# Accesseurs
# *****************************************************************************************************************
# -----------------------------------------------------------------------------------------------------------
def get_moy_from_resultats(self, tag, etudid):
"""Renvoie la moyenne obtenue par un étudiant à un tag donné au regard du format de self.resultats"""
return (
self.moyennes_tags[tag][etudid][0]
if tag in self.moyennes_tags and etudid in self.moyennes_tags[tag]
else None
)
# -----------------------------------------------------------------------------------------------------------
def get_rang_from_resultats(self, tag, etudid):
"""Renvoie le rang à un tag d'un étudiant au regard du format de self.resultats"""
return (
self.rangs[tag][etudid]
if tag in self.moyennes_tags and etudid in self.moyennes_tags[tag]
else None
)
# -----------------------------------------------------------------------------------------------------------
def get_coeff_from_resultats(self, tag, etudid):
"""Renvoie la somme des coeffs de pondération normalisée utilisés dans le calcul de la moyenne à un tag d'un étudiant
au regard du format de self.resultats.
def df_moyennes_et_classements(self):
"""Renvoie un dataframe listant toutes les moyennes,
et les classements des étudiants pour tous les tags
"""
return (
self.moyennes_tags[tag][etudid][1]
if tag in self.moyennes_tags and etudid in self.moyennes_tags[tag]
else None
)
etudiants = self.etudiants
df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"])
# -----------------------------------------------------------------------------------------------------------
def get_nbinscrits(self):
"""Renvoie le nombre d'inscrits"""
return len(self.inscrlist)
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}"))
# -----------------------------------------------------------------------------------------------------------
def get_moy_from_stats(self, tag):
"""Renvoie la moyenne des notes calculées pour d'un tag donné"""
return self.statistiques[tag][0] if tag in self.statistiques else None
return df
def get_min_from_stats(self, tag):
"""Renvoie la plus basse des notes calculées pour d'un tag donné"""
return self.statistiques[tag][1] if tag in self.statistiques else None
def get_max_from_stats(self, tag):
"""Renvoie la plus haute des notes calculées pour d'un tag donné"""
return self.statistiques[tag][2] if tag in self.statistiques else None
# -----------------------------------------------------------------------------------------------------------
# La structure des données mémorisées pour chaque tag dans le dictionnaire de synthèse
# d'un jury PE
FORMAT_DONNEES_ETUDIANTS = (
"note",
"coeff",
"rang",
"nbinscrits",
"moy",
"max",
"min",
)
def get_resultatsEtud(self, tag, etudid):
"""Renvoie un tuple (note, coeff, rang, nb_inscrit, moy, min, max) synthétisant les résultats d'un étudiant
à un tag donné. None sinon"""
return (
self.get_moy_from_resultats(tag, etudid),
self.get_coeff_from_resultats(tag, etudid),
self.get_rang_from_resultats(tag, etudid),
self.get_nbinscrits(),
self.get_moy_from_stats(tag),
self.get_min_from_stats(tag),
self.get_max_from_stats(tag),
)
# return self.tag_stats[tag]
# else :
# return self.pe_stats
# *****************************************************************************************************************
# Ajout des notes
# *****************************************************************************************************************
# -----------------------------------------------------------------------------------------------------------
def add_moyennesTag(self, tag, listMoyEtCoeff) -> bool:
"""
Mémorise les moyennes, les coeffs de pondération et les etudid dans resultats
avec calcul du rang
:param tag: Un tag
:param listMoyEtCoeff: Une liste donnant [ (moy, coeff, etudid) ]
TODO:: Inutile maintenant ?
"""
# ajout des moyennes au dictionnaire résultat
if listMoyEtCoeff:
self.moyennes_tags[tag] = {
etudid: (moyenne, somme_coeffs)
for (moyenne, somme_coeffs, etudid) in listMoyEtCoeff
}
# Calcule les rangs
lesMoyennesTriees = sorted(
listMoyEtCoeff,
reverse=True,
key=lambda col: col[0]
if isinstance(col[0], float)
else 0, # remplace les None et autres chaines par des zéros
) # triées
self.rangs[tag] = scu.comp_ranks(lesMoyennesTriees) # les rangs
# calcul des stats
self.comp_stats_d_un_tag(tag)
return True
return False
# *****************************************************************************************************************
# Méthodes dévolues aux calculs de statistiques (min, max, moy) sur chaque moyenne taguée
# *****************************************************************************************************************
def comp_stats_d_un_tag(self, tag):
"""
Calcule la moyenne generale, le min, le max pour un tag donné,
en ne prenant en compte que les moyennes significatives. Mémorise le resultat dans
self.statistiques
"""
stats = ("-NA-", "-", "-")
if tag not in self.moyennes_tags:
return stats
notes = [
self.get_moy_from_resultats(tag, etudid)
for etudid in self.moyennes_tags[tag]
] # les notes du tag
notes_valides = [
note for note in notes if isinstance(note, float) and note != None
]
nb_notes_valides = len(notes_valides)
if nb_notes_valides > 0:
(moy, _) = moyenne_ponderee_terme_a_terme(notes_valides, force=True)
self.statistiques[tag] = (moy, max(notes_valides), min(notes_valides))
# ************************************************************************
# Méthodes dévolues aux affichages -> a revoir
# ************************************************************************
def str_resTag_d_un_etudiant(self, tag, etudid, delim=";"):
"""Renvoie une chaine de caractères (valable pour un csv)
décrivant la moyenne et le rang d'un étudiant, pour un tag donné ;
"""
if tag not in self.get_all_tags() or etudid not in self.moyennes_tags[tag]:
return ""
moystr = TableTag.str_moytag(
self.get_moy_from_resultats(tag, etudid),
self.get_rang_from_resultats(tag, etudid),
self.get_nbinscrits(),
delim=delim,
)
return moystr
def str_res_d_un_etudiant(self, etudid, delim=";"):
"""Renvoie sur une ligne les résultats d'un étudiant à tous les tags (par ordre alphabétique)."""
return delim.join(
[self.str_resTag_d_un_etudiant(tag, etudid) for tag in self.get_all_tags()]
)
# -----------------------------------------------------------------------
def str_moytag(cls, moyenne, rang, nbinscrit, delim=";"):
"""Renvoie une chaine de caractères représentant une moyenne (float ou string) et un rang
pour différents formats d'affichage : HTML, debug ligne de commande, csv"""
moystr = (
"%2.2f%s%s%s%d" % (moyenne, delim, rang, delim, nbinscrit)
if isinstance(moyenne, float)
else str(moyenne) + delim + str(rang) + delim + str(nbinscrit)
)
return moystr
str_moytag = classmethod(str_moytag)
# -----------------------------------------------------------------------
def df_tagtable(self):
def df_notes(self):
"""Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags
Returns:
@ -299,69 +105,3 @@ class TableTag(object):
df = df.join(self.moyennes_tags[tag]["classements"].rename(f"class {tag}"))
return df.to_csv(sep=";")
# ************************************************************************
# Fonctions diverses
# ************************************************************************
# *********************************************
def moyenne_ponderee_terme_a_terme(notes, coefs=None, force=False):
"""
Calcule la moyenne pondérée d'une liste de notes avec d'éventuels coeffs de pondération.
Renvoie le résultat sous forme d'un tuple (moy, somme_coeff)
La liste de notes contient soit :
1) des valeurs numériques
2) des strings "-NA-" (pas de notes) ou "-NI-" (pas inscrit) ou "-c-" ue capitalisée,
3) None.
Le paramètre force indique si le calcul de la moyenne doit être forcée ou non, c'est à
dire s'il y a ou non omission des notes non numériques (auquel cas la moyenne est
calculée sur les notes disponibles) ; sinon renvoie (None, None).
"""
# Vérification des paramètres d'entrée
if not isinstance(notes, list) or (
coefs != None and not isinstance(coefs, list) and len(coefs) != len(notes)
):
raise ValueError("Erreur de paramètres dans moyenne_ponderee_terme_a_terme")
# Récupération des valeurs des paramètres d'entrée
coefs = [1] * len(notes) if coefs is None else coefs
# S'il n'y a pas de notes
if not notes: # Si notes = []
return (None, None)
# Liste indiquant les notes valides
notes_valides = [
(isinstance(note, float) and not np.isnan(note)) or isinstance(note, int)
for note in notes
]
# Si on force le calcul de la moyenne ou qu'on ne le force pas
# et qu'on a le bon nombre de notes
if force or sum(notes_valides) == len(notes):
moyenne, ponderation = 0.0, 0.0
for i in range(len(notes)):
if notes_valides[i]:
moyenne += coefs[i] * notes[i]
ponderation += coefs[i]
return (
(moyenne / (ponderation * 1.0), ponderation)
if ponderation != 0
else (None, 0)
)
# Si on ne force pas le calcul de la moyenne
return (None, None)
# -------------------------------------------------------------------------------------------
def conversionDate_StrToDate(date_fin):
"""Conversion d'une date fournie sous la forme d'une chaine de caractère de
type 'jj/mm/aaaa' en un objet date du package datetime.
Fonction servant au tri des semestres par date
"""
(d, m, y) = [int(x) for x in date_fin.split("/")]
date_fin_dst = datetime.date(y, m, d)
return date_fin_dst

View File

@ -1,4 +1,6 @@
import app.pe.pe_comp as pe_comp
import app.pe.pe_affichage as pe_affichage
from app.models import FormSemestre
from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date
@ -36,8 +38,7 @@ class Trajectoire:
"""Les semestres à aggréger"""
self.semestres_aggreges = {}
def add_semestres_a_aggreger(self, semestres: dict[int: FormSemestre]):
def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]):
"""Ajoute des semestres au semestre à aggréger
Args:
@ -45,18 +46,17 @@ class Trajectoire:
"""
self.semestres_aggreges = self.semestres_aggreges | semestres
def get_repr(self):
def get_repr(self, verbose=True) -> str:
"""Représentation textuelle d'une trajectoire
basée sur ses semestres aggrégés"""
noms = []
for fid in self.semestres_aggreges:
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}"
if noms:
if verbose and noms:
repr += " - " + "+".join(noms)
return repr
@ -72,11 +72,10 @@ class TrajectoiresJuryPE:
self.annee_diplome = annee_diplome
"""Toutes les trajectoires possibles"""
self.trajectoires: dict[tuple: Trajectoire] = {}
self.trajectoires: dict[tuple:Trajectoire] = {}
"""Quelle trajectoires pour quel étudiant :
dictionnaire {etudid: {nom_aggregat: Trajectoire}}"""
self.suivi: dict[int: str] = {}
self.suivi: dict[int:str] = {}
def cree_trajectoires(self, etudiants: EtudiantsJuryPE):
"""Créé toutes les trajectoires, au regard du cursus des étudiants
@ -84,15 +83,17 @@ class TrajectoiresJuryPE:
"""
for nom_aggregat in pe_comp.TOUS_LES_SEMESTRES + pe_comp.TOUS_LES_AGGREGATS:
"""L'aggrégat considéré (par ex: 3S=S1+S2+S3), son nom de son semestre terminal (par ex: S3) et son numéro (par ex: 3)"""
noms_semestre_de_aggregat = pe_comp.PARCOURS[nom_aggregat]["aggregat"]
nom_semestre_terminal = noms_semestre_de_aggregat[-1]
for etudid in etudiants.cursus:
if etudid not in self.suivi:
self.suivi[etudid] = {aggregat: None
for aggregat in pe_comp.TOUS_LES_SEMESTRES + pe_comp.TOUS_LES_AGGREGATS}
self.suivi[etudid] = {
aggregat: None
for aggregat in pe_comp.TOUS_LES_SEMESTRES
+ pe_comp.TOUS_LES_AGGREGATS
}
"""Le formsemestre terminal (dernier en date) associé au
semestre marquant la fin de l'aggrégat
@ -111,7 +112,9 @@ class TrajectoiresJuryPE:
"""La liste des semestres de l'étudiant à prendre en compte
pour cette trajectoire"""
semestres_a_aggreger = etudiants.get_trajectoire(etudid, formsemestre_final)
semestres_a_aggreger = etudiants.get_trajectoire(
etudid, formsemestre_final, nom_aggregat
)
"""Ajout des semestres à la trajectoire"""
trajectoire.add_semestres_a_aggreger(semestres_a_aggreger)
@ -129,7 +132,7 @@ def get_trajectoires_etudid(trajectoires, etudid):
trajectoires suivies par un étudiant
"""
if etudid not in trajectoires.suivi:
pe_comp.pe_print(f"{etudid} fait-il bien partie du jury ?")
pe_affichage.pe_print(f"{etudid} fait-il bien partie du jury ?")
liste = []
for aggregat in pe_comp.TOUS_LES_PARCOURS:
@ -138,7 +141,7 @@ def get_trajectoires_etudid(trajectoires, etudid):
liste.append(trajet.trajectoire_id)
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'),
@ -162,4 +165,3 @@ def get_semestres_a_aggreger(self, aggregat: str, formsemestre_id_terminal: int)
formsemestres_etudiant = cursus_etudiant[formsemestre_id_terminal]
formsemestres = formsemestres | formsemestres_etudiant
return formsemestres

View File

@ -39,40 +39,38 @@ Created on Fri Sep 9 09:15:05 2016
from app.comp import moy_sem
from app.comp.res_sem import load_formsemestre_results
from app.pe.pe_semtag import SemestreTag
from app.pe import pe_tabletags
import pandas as pd
import numpy as np
from app.pe.pe_trajectoire import Trajectoire
from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_tabletags import TableTag
class TrajectoireTag(TableTag):
"""Calcule les moyennes par tag d'une combinaison de semestres
(trajectoires), identifiée par un nom d'aggrégat (par ex: '3S') et
par un semestre terminal, pour extraire les classements par tag pour un
groupe d'étudiants donnés. Le groupe d'étudiants est formé par ceux ayant tous
participé au semestre terminal.
Par ex: fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S'
"""
# -------------------------------------------------------------------------------------------------------------------
def __init__(
self,
nom: str,
trajectoire: Trajectoire,
semestres_taggues: dict[int, SemestreTag]
):
""" """
TableTag.__init__(self, nom=nom)
"""Calcule les moyennes par tag d'une combinaison de semestres
(trajectoires), identifiée par un nom d'aggrégat (par ex: '3S') et
par un semestre terminal, pour extraire les classements par tag pour un
groupe d'étudiants donnés. Le groupe d'étudiants est formé par ceux ayant tous
participé au semestre terminal.
"""La trajectoire associée"""
Par ex: fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S'
"""
TableTag.__init__(self)
# La trajectoire associée
self.trajectoire_id = trajectoire.trajectoire_id
self.trajectoire = trajectoire
# Le nom de la trajectoire tagguée (identique à la trajectoire)
self.nom = self.get_repr()
"""Le formsemestre terminal et les semestres aggrégés"""
self.formsemestre_terminal = trajectoire.semestre_final
nt = load_formsemestre_results(self.formsemestre_terminal)
@ -116,10 +114,10 @@ class TrajectoireTag(TableTag):
"nb_inscrits": len(moy_gen_tag),
}
def get_repr(self):
def get_repr(self, verbose=False) -> str:
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
est basée)"""
return self.trajectoire.get_repr()
return self.trajectoire.get_repr(verbose=verbose)
def compute_notes_cube(self):
"""Construit le cube de notes (etudid x tags x semestre_aggregé)
@ -152,6 +150,10 @@ class TrajectoireTag(TableTag):
etudids_communs, tags_communs
]
# Supprime tout ce qui n'est pas numérique
for col in df.columns:
df[col] = pd.to_numeric(df[col], errors="coerce")
"""Stocke le df"""
dfs[frmsem_id] = df
@ -161,8 +163,6 @@ class TrajectoireTag(TableTag):
return etudids_x_tags_x_semestres
def do_taglist(self):
"""Synthétise les tags à partir des semestres (taggués) aggrégés
@ -175,6 +175,8 @@ class TrajectoireTag(TableTag):
return sorted(set(tags))
def compute_tag_moy(set_cube: np.array, etudids: list, tags: list):
"""Calcul de la moyenne par tag sur plusieurs semestres.
La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.6.86"
SCOVERSION = "9.6.87"
SCONAME = "ScoDoc"