From bcb801662ac28f45982cc702dd75411bb74707b3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 9 Feb 2024 21:52:33 +0100 Subject: [PATCH 01/41] =?UTF-8?q?WIP:=20PE=20:=20form=20param=C3=A9trage?= =?UTF-8?q?=20pe=5Fview=5Fsem=5Frecap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/forms/pe/pe_sem_recap.py | 49 +++++++++++++++++++++++++ app/pe/pe_view.py | 51 +++++++++++++++------------ app/templates/pe/pe_view_sem_recap.j2 | 23 +++--------- 3 files changed, 81 insertions(+), 42 deletions(-) create mode 100644 app/forms/pe/pe_sem_recap.py diff --git a/app/forms/pe/pe_sem_recap.py b/app/forms/pe/pe_sem_recap.py new file mode 100644 index 00000000..37fd155c --- /dev/null +++ b/app/forms/pe/pe_sem_recap.py @@ -0,0 +1,49 @@ +############################################################################## +# +# ScoDoc +# +# 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 +# +############################################################################## + +""" +Formulaire options génération table poursuite études (PE) +""" + +from flask_wtf import FlaskForm +from wtforms import BooleanField, HiddenField, SubmitField + + +class ParametrageClasseurPE(FlaskForm): + "Formulaire paramétrage génération classeur PE" + cohorte_restreinte = BooleanField( + "Restreindre aux étudiants inscrits dans le semestre" + ) + moyennes_tags = BooleanField("Générer les moyennes sur les tags de modules") + moyennes_ue_res_sae = BooleanField( + "Générer les moyennes des ressources et des SAEs par UE" + ) + moyennes_ues_rcues = BooleanField("Générer moyennes des UEs et RCUEs (compétences)") + min_max_moy = BooleanField("Colonnes min/max/moy") + synthese_individuelle_etud = BooleanField( + "Générer la feuille synthèse avec un onglet par étudiant" + ) + + submit = SubmitField("Générer les classeurs poursuites d'études") + cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 986627fa..03ad5432 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -38,6 +38,7 @@ from flask import flash, g, redirect, render_template, request, send_file, url_for from app.decorators import permission_required, scodoc +from app.forms.pe.pe_sem_recap import ParametrageClasseurPE from app.models import FormSemestre from app.pe import pe_comp from app.pe import pe_jury @@ -73,40 +74,44 @@ def pe_view_sem_recap(formsemestre_id: int): # Cosemestres diplomants cosemestres = pe_comp.get_cosemestres_diplomants(annee_diplome) + form = ParametrageClasseurPE() + if request.method == "GET": return render_template( "pe/pe_view_sem_recap.j2", annee_diplome=annee_diplome, + form=form, formsemestre=formsemestre, sco=ScoData(formsemestre=formsemestre), cosemestres=cosemestres, ) # request.method == "POST" - jury = pe_jury.JuryPE(annee_diplome) - if not jury.diplomes_ids: - flash("aucun étudiant à considérer !") - return redirect( - url_for( - "notes.pe_view_sem_recap", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, + if form.validate_on_submit(): + jury = pe_jury.JuryPE(annee_diplome, options=form.data) + if not jury.diplomes_ids: + flash("aucun étudiant à considérer !") + return redirect( + url_for( + "notes.pe_view_sem_recap", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) ) + + data = jury.get_zipped_data() + + return send_file( + data, + mimetype="application/zip", + download_name=scu.sanitize_filename(jury.nom_export_zip + ".zip"), + as_attachment=True, ) - data = jury.get_zipped_data() - - return send_file( - data, - mimetype="application/zip", - download_name=scu.sanitize_filename(jury.nom_export_zip + ".zip"), - as_attachment=True, - ) - - return render_template( - "pe/pe_view_sem_recap.j2", - annee_diplome=annee_diplome, - formsemestre=formsemestre, - sco=ScoData(formsemestre=formsemestre), - cosemestres=cosemestres, + return redirect( + url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) ) diff --git a/app/templates/pe/pe_view_sem_recap.j2 b/app/templates/pe/pe_view_sem_recap.j2 index 756b7f87..8d0a0fb1 100644 --- a/app/templates/pe/pe_view_sem_recap.j2 +++ b/app/templates/pe/pe_view_sem_recap.j2 @@ -1,4 +1,5 @@ {% extends "sco_page.j2" %} +{% import 'wtf.j2' as wtf %} {% block styles %} {{super()}} @@ -42,7 +43,9 @@

Avis de poursuites d'études de la promo {{ annee_diplome }}

-
+ {{ wtf.quick_form(form) }} + +
Seront (a minima) pris en compte les étudiants ayant été inscrits aux semestres suivants :
    @@ -54,22 +57,4 @@
-
- -
- -
- - - - {% endblock app_content %} \ No newline at end of file From e78a2d3ffe74bae379bccb059c9c419f30cfa7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Wed, 14 Feb 2024 14:34:22 +0100 Subject: [PATCH 02/41] Corrige bug sur l'analyse des abandons de formation --- app/pe/pe_comp.py | 10 +++ app/pe/pe_etudiant.py | 149 ++++++++++++++++++++++++++++-------------- app/pe/pe_jury.py | 7 +- app/pe/pe_view.py | 2 +- config.py | 4 +- 5 files changed, 119 insertions(+), 53 deletions(-) diff --git a/app/pe/pe_comp.py b/app/pe/pe_comp.py index 67805dfa..24edf207 100644 --- a/app/pe/pe_comp.py +++ b/app/pe/pe_comp.py @@ -284,3 +284,13 @@ def get_cosemestres_diplomants(annee_diplome: int) -> dict[int, FormSemestre]: cosemestres[fid] = cosem return cosemestres + + +def tri_semestres_par_rang(cosemestres: dict[int, FormSemestre]): + """Partant d'un dictionnaire de cosemestres, les tri par rang (semestre_id) dans un + dictionnaire {rang: [liste des semestres du dit rang]}""" + cosemestres_tries = {} + for sem in cosemestres.values(): + cosemestres_tries[sem.semestre_id] = cosemestres_tries.get(sem.semestre_id, []) + [sem] + return cosemestres_tries + diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 9bcb6c49..8b941d7f 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -37,6 +37,7 @@ Created on 17/01/2024 """ import pandas as pd +from app import ScoValueError from app.models import FormSemestre, Identite, Formation from app.pe import pe_comp, pe_affichage from app.scodoc import codes_cursus @@ -87,7 +88,7 @@ class EtudiantsJuryPE: self.abandons_ids = {} """Les etudids des étudiants redoublants/réorientés""" - def find_etudiants(self): + def find_etudiants(self, formsemestre_base: FormSemestre): """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``. @@ -116,7 +117,7 @@ class EtudiantsJuryPE: self.identites[etudid] = Identite.get_etud(etudid) # Analyse son cursus - self.analyse_etat_etudiant(etudid, cosemestres) + self.analyse_etat_etudiant(etudid, cosemestres, formsemestre_base) # Analyse son parcours pour atteindre chaque semestre de la formation self.structure_cursus_etudiant(etudid) @@ -187,7 +188,12 @@ class EtudiantsJuryPE: etudiants = {etudid: self.identites[etudid] for etudid in etudids} return etudiants - def analyse_etat_etudiant(self, etudid: int, cosemestres: dict[int, FormSemestre]): + def analyse_etat_etudiant( + self, + etudid: int, + cosemestres: dict[int, FormSemestre], + formsemestre_base: FormSemestre, + ): """Analyse le cursus d'un étudiant pouvant être : * l'un de ceux sur lesquels le jury va statuer (année de diplômation du jury considéré) @@ -198,8 +204,10 @@ class EtudiantsJuryPE: * à insérer une entrée dans ``self.cursus`` pour mémoriser son identité, avec son nom, prénom, etc... - * à analyser son parcours, pour déterminer s'il n'a (ou non) abandonné l'IUT en cours de - route (cf. clé abandon) + * à analyser son parcours, pour déterminer s'il a démissionné, redoublé (autre année de diplôme) + ou a abandonné l'IUT en cours de route (cf. clé abandon). Un étudiant est considéré en abandon s'il n'est + inscrit à aucun cosemestres de rang supérieur ou égal (et donc de dates) + à celui ayant servi à lancer le jury (`formsemestre_base`) Args: etudid: L'etudid d'un étudiant, à ajouter à ceux traiter par le jury @@ -232,15 +240,19 @@ class EtudiantsJuryPE: "abandon": False, # va être traité en dessous } - # Est-il démissionnaire : charge son dernier semestre pour connaitre son état ? - dernier_semes_etudiant = formsemestres[0] - res = load_formsemestre_results(dernier_semes_etudiant) - etud_etat = res.get_etud_etat(etudid) - if etud_etat == scu.DEMISSION: - self.cursus[etudid]["abandon"] |= True - else: - # Est-il réorienté ou a-t-il arrêté volontairement sa formation ? - self.cursus[etudid]["abandon"] |= arret_de_formation(identite, cosemestres) + # Si l'étudiant est succeptible d'être diplomé + if self.cursus[etudid]["diplome"] == self.annee_diplome: + # Est-il démissionnaire : charge son dernier semestre pour connaitre son état ? + dernier_semes_etudiant = formsemestres[0] + res = load_formsemestre_results(dernier_semes_etudiant) + etud_etat = res.get_etud_etat(etudid) + if etud_etat == scu.DEMISSION: + self.cursus[etudid]["abandon"] = True + else: + # Est-il réorienté ou a-t-il arrêté (volontairement) sa formation ? + self.cursus[etudid]["abandon"] = arret_de_formation( + identite, cosemestres, formsemestre_base + ) def get_semestres_significatifs(self, etudid: int): """Ensemble des semestres d'un étudiant, qui l'auraient amené à être diplomé @@ -446,8 +458,10 @@ def get_semestres_apc(identite: Identite) -> list: return semestres_apc -def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool: - """Détermine si un étudiant a arrêté sa formation. Il peut s'agir : +def arret_de_formation( + etud: Identite, cosemestres: dict[int, FormSemestre], formsemestre_base: FormSemestre +) -> bool: + """Détermine si un étudiant a arrêté sa formation (volontairement ou non). Il peut s'agir : * d'une réorientation à l'initiative du jury de semestre ou d'une démission (on pourrait utiliser les code NAR pour réorienté & DEM pour démissionnaire @@ -458,7 +472,9 @@ def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool: Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas dans l'un des "derniers" cosemestres (semestres conduisant à la même année de diplômation) - connu dans Scodoc. + connu dans Scodoc. Par "derniers" cosemestres, est fait le choix d'analyser tous les cosemestres + de rang/semestre_id supérieur ou égal (et donc de dates) à celui du ``formsemestre_base`` ayant servi à lancer + le jury PE. Par ex: au moment du jury PE en fin de S5 (pas de S6 renseigné dans Scodoc), l'étudiant doit appartenir à une instance des S5 qui conduisent à la diplomation dans @@ -493,41 +509,78 @@ def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool: if not semestres_apc: return True - # Son dernier semestre APC en date - dernier_formsemestre = get_dernier_semestre_en_date(semestres_apc) - numero_dernier_formsemestre = dernier_formsemestre.semestre_id + # Les cosemestres de rang supérieur ou égal à celui de formsemestre, triés par rang, + # sous la forme ``{semestre_id: [liste des comestres associé à ce semestre_id]}`` + cosemestres_tries_par_rang = pe_comp.tri_semestres_par_rang(cosemestres) - # Les numéro de semestres possible dans lesquels il pourrait s'incrire - # semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation) - if numero_dernier_formsemestre % 2 == 1: - numeros_possibles = list( - range(numero_dernier_formsemestre + 1, pe_comp.NBRE_SEMESTRES_DIPLOMANT) - ) - # semestre pair => passage en année supérieure ou redoublement - else: # - numeros_possibles = list( - range( - max(numero_dernier_formsemestre - 1, 1), - pe_comp.NBRE_SEMESTRES_DIPLOMANT, - ) - ) + cosemestres_superieurs = {} + for rang in cosemestres_tries_par_rang: + if rang >= formsemestre_base.semestre_id: + cosemestres_superieurs[rang] = cosemestres_tries_par_rang[rang] - # Y-a-t-il des cosemestres dans lesquels il aurait pu s'incrire ? - formsestres_superieurs_possibles = [] - for fid, sem in cosemestres.items(): # Les semestres ayant des inscrits - if ( - fid != dernier_formsemestre.formsemestre_id - and sem.semestre_id in numeros_possibles - and sem.date_debut.year >= dernier_formsemestre.date_debut.year - ): - # date de debut des semestres possibles postérieur au dernier semestre de l'étudiant - # et de niveau plus élevé que le dernier semestre valide de l'étudiant - formsestres_superieurs_possibles.append(fid) + # Si pas d'autres cosemestres postérieurs + if not cosemestres_superieurs: + return False - if len(formsestres_superieurs_possibles) > 0: - return True + # Pour chaque rang de (co)semestres, y-a-il un dans lequel il est inscrit ? + etat_inscriptions = {rang: False for rang in cosemestres_superieurs} + for rang in etat_inscriptions: + for sem in cosemestres_superieurs[rang]: + etudiants_du_sem = {ins.etudid for ins in sem.inscriptions} + if etud.etudid in etudiants_du_sem: + etat_inscriptions[rang] = True - return False + # Vérifie qu'il n'y a pas de "trous" dans les rangs des cosemestres + rangs = etat_inscriptions.keys() + if list(rangs) != list(range(min(rangs), max(rangs)+1)): + difference = set(range(min(rangs), max(rangs)+1)) - set(rangs) + affichage = ",".join([f"S{val}" for val in difference]) + raise ScoValueError(f"Il manque le(s) semestre(s) {affichage} au cursus de vos étudiants.") + + # Est-il inscrit à tous les semestres de rang supérieur ? Si non, est démissionnaire + est_demissionnaire = sum(etat_inscriptions.values()) != len(rangs) + if est_demissionnaire: + non_inscrit_a = [rang for rang in etat_inscriptions if not etat_inscriptions[rang]] + affichage = ",".join([f"S{val}" for val in non_inscrit_a]) + pe_affichage.pe_print(f"{etud.etat_civil} ({etud.etudid} considéré en abandon car non inscrit dans un (ou des) semestre(s) {affichage} amenant à diplômation") + + return est_demissionnaire + + # # Son dernier semestre APC en date + # dernier_formsemestre = get_dernier_semestre_en_date(semestres_apc) + # numero_dernier_formsemestre = dernier_formsemestre.semestre_id + # + # # Les numéro de semestres possible dans lesquels il pourrait s'incrire + # # semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation) + # if numero_dernier_formsemestre % 2 == 1: + # numeros_possibles = list( + # range(numero_dernier_formsemestre + 1, pe_comp.NBRE_SEMESTRES_DIPLOMANT) + # ) + # # semestre pair => passage en année supérieure ou redoublement + # else: # + # numeros_possibles = list( + # range( + # max(numero_dernier_formsemestre - 1, 1), + # pe_comp.NBRE_SEMESTRES_DIPLOMANT, + # ) + # ) + # + # # Y-a-t-il des cosemestres dans lesquels il aurait pu s'incrire ? + # formsestres_superieurs_possibles = [] + # for fid, sem in cosemestres.items(): # Les semestres ayant des inscrits + # if ( + # fid != dernier_formsemestre.formsemestre_id + # and sem.semestre_id in numeros_possibles + # and sem.date_debut.year >= dernier_formsemestre.date_debut.year + # ): + # # date de debut des semestres possibles postérieur au dernier semestre de l'étudiant + # # et de niveau plus élevé que le dernier semestre valide de l'étudiant + # formsestres_superieurs_possibles.append(fid) + # + # if len(formsestres_superieurs_possibles) > 0: + # return True + # + # return False def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSemestre: diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index a6c15b8c..b83f40f2 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -70,11 +70,14 @@ class JuryPE(object): diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX) """ - def __init__(self, diplome): + def __init__(self, diplome: int, formsemestre_base: FormSemestre): pe_affichage.pe_start_log() self.diplome = diplome "L'année du diplome" + self.formsemestre_base = formsemestre_base + "Le formsemestre ayant servi à lancer le jury PE (souvent un S3 ou un S5)" + self.nom_export_zip = f"Jury_PE_{self.diplome}" "Nom du zip où ranger les fichiers générés" @@ -87,7 +90,7 @@ class JuryPE(object): self.diplome}""" ) self.etudiants = EtudiantsJuryPE(self.diplome) # Les infos sur les étudiants - self.etudiants.find_etudiants() + self.etudiants.find_etudiants(formsemestre_base) self.diplomes_ids = self.etudiants.diplomes_ids self.zipdata = io.BytesIO() diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 8ef4fe44..0c2b733d 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -83,7 +83,7 @@ def pe_view_sem_recap(formsemestre_id: int): ) # request.method == "POST" - jury = pe_jury.JuryPE(annee_diplome) + jury = pe_jury.JuryPE(annee_diplome, formsemestre) if not jury.diplomes_ids: flash("aucun étudiant à considérer !") return redirect( diff --git a/config.py b/config.py index d98e9513..af1a173e 100755 --- a/config.py +++ b/config.py @@ -61,11 +61,11 @@ class DevConfig(Config): DEBUG = True TESTING = False SQLALCHEMY_DATABASE_URI = ( - os.environ.get("SCODOC_DATABASE_URI") or "postgresql:///SCODOC_DEV" + os.environ.get("SCODOC_DATABASE_URI") or "postgresql:///SCODOC" ) SECRET_KEY = os.environ.get("DEV_SECRET_KEY") or "bb3faec7d9a34eb68a8e3e710087d87a" # pour le avoir url_for dans le shell: - # SERVER_NAME = os.environ.get("SCODOC_TEST_SERVER_NAME") or "localhost" + # SERVER_NAME = "http://localhost:8080" class TestConfig(DevConfig): From 02a73de04db58b17c1106540292eeec0b0ab5a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Wed, 14 Feb 2024 15:19:21 +0100 Subject: [PATCH 03/41] =?UTF-8?q?Am=C3=A9liore=20l'analyse=20des=20abandon?= =?UTF-8?q?s=20de=20formation=20(sans=20prise=20en=20compte=20du=20formsem?= =?UTF-8?q?estre=5Fbase)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_etudiant.py | 50 +++++++++++++++------------ app/pe/pe_jury.py | 7 ++-- app/pe/pe_view.py | 8 +++-- app/templates/pe/pe_view_sem_recap.j2 | 6 ++-- 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 8b941d7f..7448bf04 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -88,7 +88,7 @@ class EtudiantsJuryPE: self.abandons_ids = {} """Les etudids des étudiants redoublants/réorientés""" - def find_etudiants(self, formsemestre_base: FormSemestre): + def find_etudiants(self): """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``. @@ -117,7 +117,7 @@ class EtudiantsJuryPE: self.identites[etudid] = Identite.get_etud(etudid) # Analyse son cursus - self.analyse_etat_etudiant(etudid, cosemestres, formsemestre_base) + self.analyse_etat_etudiant(etudid, cosemestres) # Analyse son parcours pour atteindre chaque semestre de la formation self.structure_cursus_etudiant(etudid) @@ -142,7 +142,7 @@ class EtudiantsJuryPE: assert nbre_abandons == len(self.abandons_ids) pe_affichage.pe_print( - f" => {nbre_abandons} étudiants non considérés (redoublement, réorientation, abandon" + f" => {nbre_abandons} étudiants traités mais non diplômés (redoublement, réorientation, abandon)" ) # pe_affichage.pe_print( # " => quelques étudiants futurs diplômés : " @@ -191,8 +191,7 @@ class EtudiantsJuryPE: def analyse_etat_etudiant( self, etudid: int, - cosemestres: dict[int, FormSemestre], - formsemestre_base: FormSemestre, + cosemestres: dict[int, FormSemestre] ): """Analyse le cursus d'un étudiant pouvant être : @@ -205,9 +204,10 @@ class EtudiantsJuryPE: * à insérer une entrée dans ``self.cursus`` pour mémoriser son identité, avec son nom, prénom, etc... * à analyser son parcours, pour déterminer s'il a démissionné, redoublé (autre année de diplôme) - ou a abandonné l'IUT en cours de route (cf. clé abandon). Un étudiant est considéré en abandon s'il n'est - inscrit à aucun cosemestres de rang supérieur ou égal (et donc de dates) - à celui ayant servi à lancer le jury (`formsemestre_base`) + ou a abandonné l'IUT en cours de route (cf. clé abandon). Un étudiant est considéré + en abandon si connaissant son dernier semestre (par ex. un S3) il n'est pas systématiquement + inscrit à l'un des S4, S5 ou S6 existants dans les cosemestres. + Args: etudid: L'etudid d'un étudiant, à ajouter à ceux traiter par le jury @@ -251,7 +251,7 @@ class EtudiantsJuryPE: else: # Est-il réorienté ou a-t-il arrêté (volontairement) sa formation ? self.cursus[etudid]["abandon"] = arret_de_formation( - identite, cosemestres, formsemestre_base + identite, cosemestres ) def get_semestres_significatifs(self, etudid: int): @@ -458,9 +458,7 @@ def get_semestres_apc(identite: Identite) -> list: return semestres_apc -def arret_de_formation( - etud: Identite, cosemestres: dict[int, FormSemestre], formsemestre_base: FormSemestre -) -> bool: +def arret_de_formation(etud: Identite, cosemestres: dict[int, FormSemestre]) -> bool: """Détermine si un étudiant a arrêté sa formation (volontairement ou non). Il peut s'agir : * d'une réorientation à l'initiative du jury de semestre ou d'une démission @@ -473,8 +471,7 @@ def arret_de_formation( Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas dans l'un des "derniers" cosemestres (semestres conduisant à la même année de diplômation) connu dans Scodoc. Par "derniers" cosemestres, est fait le choix d'analyser tous les cosemestres - de rang/semestre_id supérieur ou égal (et donc de dates) à celui du ``formsemestre_base`` ayant servi à lancer - le jury PE. + de rang/semestre_id supérieur (et donc de dates) au dernier semestre dans lequel il a été inscrit. Par ex: au moment du jury PE en fin de S5 (pas de S6 renseigné dans Scodoc), l'étudiant doit appartenir à une instance des S5 qui conduisent à la diplomation dans @@ -501,7 +498,6 @@ def arret_de_formation( Est-il réorienté, démissionnaire ou a-t-il arrêté de son propre chef sa formation ? TODO:: A reprendre pour le cas des étudiants à l'étranger - TODO:: A reprendre si BUT avec semestres décalés """ # Les semestres APC de l'étudiant semestres = get_semestres_apc(etud) @@ -509,13 +505,17 @@ def arret_de_formation( if not semestres_apc: return True + # Le dernier semestre de l'étudiant + dernier_formsemestre = semestres[0] + rang_dernier_semestre = dernier_formsemestre.semestre_id + # Les cosemestres de rang supérieur ou égal à celui de formsemestre, triés par rang, # sous la forme ``{semestre_id: [liste des comestres associé à ce semestre_id]}`` cosemestres_tries_par_rang = pe_comp.tri_semestres_par_rang(cosemestres) cosemestres_superieurs = {} for rang in cosemestres_tries_par_rang: - if rang >= formsemestre_base.semestre_id: + if rang > rang_dernier_semestre: cosemestres_superieurs[rang] = cosemestres_tries_par_rang[rang] # Si pas d'autres cosemestres postérieurs @@ -531,18 +531,24 @@ def arret_de_formation( etat_inscriptions[rang] = True # Vérifie qu'il n'y a pas de "trous" dans les rangs des cosemestres - rangs = etat_inscriptions.keys() - if list(rangs) != list(range(min(rangs), max(rangs)+1)): - difference = set(range(min(rangs), max(rangs)+1)) - set(rangs) + rangs = sorted(etat_inscriptions.keys()) + if list(rangs) != list(range(min(rangs), max(rangs) + 1)): + difference = set(range(min(rangs), max(rangs) + 1)) - set(rangs) affichage = ",".join([f"S{val}" for val in difference]) - raise ScoValueError(f"Il manque le(s) semestre(s) {affichage} au cursus de vos étudiants.") + raise ScoValueError( + f"Il manque le(s) semestre(s) {affichage} au cursus de {etud.etat_civil} ({etud.etudid})." + ) # Est-il inscrit à tous les semestres de rang supérieur ? Si non, est démissionnaire est_demissionnaire = sum(etat_inscriptions.values()) != len(rangs) if est_demissionnaire: - non_inscrit_a = [rang for rang in etat_inscriptions if not etat_inscriptions[rang]] + non_inscrit_a = [ + rang for rang in etat_inscriptions if not etat_inscriptions[rang] + ] affichage = ",".join([f"S{val}" for val in non_inscrit_a]) - pe_affichage.pe_print(f"{etud.etat_civil} ({etud.etudid} considéré en abandon car non inscrit dans un (ou des) semestre(s) {affichage} amenant à diplômation") + pe_affichage.pe_print( + f"{etud.etat_civil} ({etud.etudid}) considéré en abandon car non inscrit dans un (ou des) semestre(s) {affichage} amenant à diplômation" + ) return est_demissionnaire diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index b83f40f2..d486692f 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -70,14 +70,11 @@ class JuryPE(object): diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX) """ - def __init__(self, diplome: int, formsemestre_base: FormSemestre): + def __init__(self, diplome: int): pe_affichage.pe_start_log() self.diplome = diplome "L'année du diplome" - self.formsemestre_base = formsemestre_base - "Le formsemestre ayant servi à lancer le jury PE (souvent un S3 ou un S5)" - self.nom_export_zip = f"Jury_PE_{self.diplome}" "Nom du zip où ranger les fichiers générés" @@ -90,7 +87,7 @@ class JuryPE(object): self.diplome}""" ) self.etudiants = EtudiantsJuryPE(self.diplome) # Les infos sur les étudiants - self.etudiants.find_etudiants(formsemestre_base) + self.etudiants.find_etudiants() self.diplomes_ids = self.etudiants.diplomes_ids self.zipdata = io.BytesIO() diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 0c2b733d..28a8c8db 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -72,18 +72,20 @@ def pe_view_sem_recap(formsemestre_id: int): # Cosemestres diplomants cosemestres = pe_comp.get_cosemestres_diplomants(annee_diplome) - + cosemestres_tries = pe_comp.tri_semestres_par_rang(cosemestres) + affichage_cosemestres_tries = {rang: ", ".join([sem.titre_annee() for sem in cosemestres_tries[rang]]) for rang in cosemestres_tries} if request.method == "GET": return render_template( "pe/pe_view_sem_recap.j2", annee_diplome=annee_diplome, formsemestre=formsemestre, sco=ScoData(formsemestre=formsemestre), - cosemestres=cosemestres, + cosemestres=affichage_cosemestres_tries, + rangs_tries=sorted(affichage_cosemestres_tries.keys()) ) # request.method == "POST" - jury = pe_jury.JuryPE(annee_diplome, formsemestre) + jury = pe_jury.JuryPE(annee_diplome) if not jury.diplomes_ids: flash("aucun étudiant à considérer !") return redirect( diff --git a/app/templates/pe/pe_view_sem_recap.j2 b/app/templates/pe/pe_view_sem_recap.j2 index 756b7f87..250dc078 100644 --- a/app/templates/pe/pe_view_sem_recap.j2 +++ b/app/templates/pe/pe_view_sem_recap.j2 @@ -43,12 +43,12 @@

Avis de poursuites d'études de la promo {{ annee_diplome }}

- Seront (a minima) pris en compte les étudiants ayant été inscrits aux semestres suivants : + Seront pris en compte les étudiants ayant été inscrits à l'un des semestres suivants :
    - {% for fid in cosemestres %} + {% for rang in rangs_tries %}
  • - {{ cosemestres[fid].titre_annee() }} + Semestre {{rang}} : {{ cosemestres[rang] }}
  • {% endfor %}
From 267dbb6460d59643098758b3bf9d6d0045a8ac99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Wed, 14 Feb 2024 17:00:05 +0100 Subject: [PATCH 04/41] Ajoute les moy par ue et par tag au semtag --- app/pe/pe_affichage.py | 13 ++++--- app/pe/pe_semtag.py | 46 +++++++++++++++-------- app/pe/pe_tabletags.py | 84 ++++++++++++++++++++++++++---------------- 3 files changed, 89 insertions(+), 54 deletions(-) diff --git a/app/pe/pe_affichage.py b/app/pe/pe_affichage.py index 50f2e2ab..f1ee72a7 100644 --- a/app/pe/pe_affichage.py +++ b/app/pe/pe_affichage.py @@ -9,7 +9,7 @@ from flask import g from app import log -PE_DEBUG = False +PE_DEBUG = True # On stocke les logs PE dans g.scodoc_pe_log @@ -22,14 +22,15 @@ def pe_start_log() -> list[str]: def pe_print(*a): "Log (or print in PE_DEBUG mode) and store in g" - lines = getattr(g, "scodoc_pe_log") - if lines is None: - lines = pe_start_log() - msg = " ".join(a) - lines.append(msg) if PE_DEBUG: + msg = " ".join(a) print(msg) else: + lines = getattr(g, "scodoc_pe_log") + if lines is None: + lines = pe_start_log() + msg = " ".join(a) + lines.append(msg) log(msg) diff --git a/app/pe/pe_semtag.py b/app/pe/pe_semtag.py index 9ed2418b..8e602531 100644 --- a/app/pe/pe_semtag.py +++ b/app/pe/pe_semtag.py @@ -76,6 +76,7 @@ class SemestreTag(TableTag): # 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` @@ -94,12 +95,12 @@ class SemestreTag(TableTag): tags_personnalises = get_synthese_tags_personnalises_semestre( self.nt.formsemestre ) - noms_tags_perso = list(set(tags_personnalises.keys())) + noms_tags_perso = sorted(list(set(tags_personnalises.keys()))) ## 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())) - noms_tags_auto = ["but"] + noms_tags_comp + # dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre) + # noms_tags_comp = list(set(dict_ues_competences.values())) + noms_tags_auto = ["but"] # + noms_tags_comp self.tags = noms_tags_perso + noms_tags_auto """Tags du semestre taggué""" @@ -122,23 +123,31 @@ class SemestreTag(TableTag): """ raise ScoValueError(message) + ues_hors_sport = [ue for ue in self.ues if ue.type != UE_SPORT] + # Calcul des moyennes & les classements de chaque étudiant à chaque tag self.moyennes_tags = {} for tag in tags_personnalises: # pe_affichage.pe_print(f" -> Traitement du tag {tag}") - moy_gen_tag = self.compute_moyenne_tag(tag, tags_personnalises) - self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag) + moy_ues_tag = self.compute_moy_ues_tag(tag, tags_personnalises) + moy_gen_tag = self.compute_moy_gen_tag(moy_ues_tag) - # Ajoute les moyennes générales de BUT pour le semestre considéré + self.moyennes_tags[tag] = MoyenneTag( + 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"] = MoyenneTag("but", moy_gen_but) + self.moyennes_tags["but"] = MoyenneTag("but", ues_hors_sport, df_ues, moy_gen_but) - # Ajoute les moyennes par compétence - for ue_id, competence in dict_ues_competences.items(): - if competence not in self.moyennes_tags: - moy_ue = self.nt.etud_moy_ue[ue_id] - self.moyennes_tags[competence] = MoyenneTag(competence, moy_ue) self.tags_sorted = self.get_all_tags() """Tags (personnalisés+compétences) par ordre alphabétique""" @@ -156,8 +165,8 @@ class SemestreTag(TableTag): """Nom affiché pour le semestre taggué""" return app.pe.pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True) - def compute_moyenne_tag(self, tag: str, tags_infos: dict) -> pd.Series: - """Calcule la moyenne des étudiants pour le tag indiqué, + def compute_moy_ues_tag(self, tag: str, tags_infos: dict) -> pd.Series: + """Calcule la moyenne par UE des étudiants pour le tag indiqué, pour ce SemestreTag, en ayant connaissance des informations sur les tags (dictionnaire donnant les coeff de repondération) @@ -199,7 +208,12 @@ class SemestreTag(TableTag): 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 @@ -207,7 +221,7 @@ class SemestreTag(TableTag): # 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( - moyennes_ues_tag, + moy_ues_tag, ects, formation_id=self.formsemestre.formation_id, skip_empty_ues=True, diff --git a/app/pe/pe_tabletags.py b/app/pe/pe_tabletags.py index 68be8772..a761fa0b 100644 --- a/app/pe/pe_tabletags.py +++ b/app/pe/pe_tabletags.py @@ -42,19 +42,27 @@ 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 +from app.scodoc.codes_cursus import UE_SPORT TAGS_RESERVES = ["but"] class MoyenneTag: - def __init__(self, tag: str, notes: pd.Series): + 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 un dictionnaire : + d'étudiants à un tag donné, en stockant : `` { @@ -69,16 +77,26 @@ class MoyenneTag: Args: tag: Un tag - note: Une série de notes (moyenne) sous forme d'un pd.Series() + 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.index) # calcul à venir + self.etudids = list(notes_gen.index) # calcul à venir """Les id des étudiants""" - self.inscrits_ids = notes[notes.notnull()].index.to_list() - """Les id des étudiants dont la moyenne est non nulle""" - self.df: pd.DataFrame = self.comp_moy_et_stat(notes) - """Le dataframe retraçant les moyennes/classements/statistiques""" + 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""" @@ -88,7 +106,8 @@ class MoyenneTag: 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 (souvent une moyenne par tag) dans un dictionnaire spécifique. + 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). @@ -121,64 +140,65 @@ class MoyenneTag: # Les nb d'étudiants & nb d'inscrits df["nb_etuds"] = len(self.etudids) - df.loc[self.inscrits_ids, "nb_inscrits"] = len(self.inscrits_ids) + # 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[self.inscrits_ids] + notes_non_nulles = notes[inscrits_ids] (class_str, class_int) = comp_ranks_series(notes_non_nulles) - df.loc[self.inscrits_ids, "classement"] = class_int + df.loc[inscrits_ids, "classement"] = class_int # Le rang (classement/nb_inscrit) df["rang"] = df["rang"].astype(str) - df.loc[self.inscrits_ids, "rang"] = ( - df.loc[self.inscrits_ids, "classement"].astype(int).astype(str) + df.loc[inscrits_ids, "rang"] = ( + df.loc[inscrits_ids, "classement"].astype(int).astype(str) + "/" - + df.loc[self.inscrits_ids, "nb_inscrits"].astype(int).astype(str) + + df.loc[inscrits_ids, "nb_inscrits"].astype(int).astype(str) ) # Les stat (des inscrits) - df.loc[self.inscrits_ids, "min"] = notes.min() - df.loc[self.inscrits_ids, "max"] = notes.max() - df.loc[self.inscrits_ids, "moy"] = notes.mean() + 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["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(), + "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["note"].round(2) + return self.df_gen["note"].round(2) def get_rangs_inscrits(self) -> pd.Series: """Série des rangs classement/nbre_inscrit""" - return self.df["rang"] + return self.df_gen["rang"] def get_min(self) -> pd.Series: """Série des min""" - return self.df["min"].round(2) + return self.df_gen["min"].round(2) def get_max(self) -> pd.Series: """Série des max""" - return self.df["max"].round(2) + return self.df_gen["max"].round(2) def get_moy(self) -> pd.Series: """Série des moy""" - return self.df["moy"].round(2) - + 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["note"].loc[etudid], 2) + return round(self.df_gen["note"].loc[etudid], 2) def get_min_for_df(self) -> float: """Min renseigné pour affichage dans un df""" @@ -195,7 +215,7 @@ class MoyenneTag: 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["rang"].loc[etudid] + classement = self.df_gen["rang"].loc[etudid] if not pd.isna(classement): return classement else: From 883028216fa9741b46dece47de9ebb48479c763e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Thu, 15 Feb 2024 17:05:03 +0100 Subject: [PATCH 05/41] =?UTF-8?q?D=C3=A9bute=20l'aggr=C3=A9gation=20des=20?= =?UTF-8?q?moyennes=20dans=20des=20RCS=20de=20type=20Sx=20(prise=20en=20co?= =?UTF-8?q?mpte=20de=20la=20meilleure=20des=202=20UE=20en=20cas=20de=20red?= =?UTF-8?q?oublement)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/departements.py | 4 +- app/pe/pe_interclasstag.py | 7 +- app/pe/pe_jury.py | 138 ++++++------- app/pe/pe_moytag.py | 203 ++++++++++++++++++ app/pe/pe_rcs.py | 4 + app/pe/pe_rcstag.py | 13 +- app/pe/pe_ressemtag.py | 373 +++++++++++++++++++++++++++++++++ app/pe/pe_semtag.py | 408 +++++++++++++++---------------------- app/pe/pe_tabletags.py | 190 +---------------- config.py | 2 +- 10 files changed, 827 insertions(+), 515 deletions(-) create mode 100644 app/pe/pe_moytag.py create mode 100644 app/pe/pe_ressemtag.py diff --git a/app/api/departements.py b/app/api/departements.py index 7d056e46..b82966e3 100644 --- a/app/api/departements.py +++ b/app/api/departements.py @@ -187,7 +187,7 @@ def dept_etudiants(acronym: str): ] """ 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//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é. """ 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//formsemestres_ids") diff --git a/app/pe/pe_interclasstag.py b/app/pe/pe_interclasstag.py index 895595ed..000feacf 100644 --- a/app/pe/pe_interclasstag.py +++ b/app/pe/pe_interclasstag.py @@ -36,7 +36,8 @@ Created on Thu Sep 8 09:36:33 2016 import pandas as pd 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_rcs import RCS, RCSsJuryPE from app.pe.pe_rcstag import RCSTag @@ -107,10 +108,10 @@ class RCSInterclasseTag(TableTag): """Matrice des notes de l'aggrégat""" # 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: 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) self.significatif = len(self.tags_sorted) > 0 diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index d486692f..bfe72bd2 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -53,9 +53,9 @@ import pandas as pd from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE import app.pe.pe_affichage as pe_affichage 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_semtag import SemestreTag +from app.pe.pe_ressemtag import ResSemTag from app.pe.pe_interclasstag import RCSInterclasseTag @@ -96,11 +96,11 @@ class JuryPE(object): pe_affichage.pe_print("*** Aucun étudiant diplômé") else: self._gen_xls_diplomes(zipfile) - self._gen_xls_semestre_taggues(zipfile) - self._gen_xls_rcss_tags(zipfile) - self._gen_xls_interclassements_rcss(zipfile) - self._gen_xls_synthese_jury_par_tag(zipfile) - self._gen_xls_synthese_par_etudiant(zipfile) + self._gen_xls_resultats_semestres_taggues(zipfile) + # self._gen_xls_rcss_tags(zipfile) + # self._gen_xls_interclassements_rcss(zipfile) + # self._gen_xls_synthese_jury_par_tag(zipfile) + # self._gen_xls_synthese_par_etudiant(zipfile) # et le log self._add_log_to_zip(zipfile) @@ -131,19 +131,19 @@ class JuryPE(object): path="details", ) - def _gen_xls_semestre_taggues(self, zipfile: ZipFile): - "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.sems_tags = compute_semestres_tag(self.etudiants) + 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""" + pe_affichage.pe_print("*** Génère les résultats des semestres taggués") + self.res_sems_tags = compute_resultats_semestres_tag(self.etudiants) # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: - for formsemestretag in self.sems_tags.values(): - onglet = formsemestretag.nom - df = formsemestretag.df_moyennes_et_classements() + for res_sem_tag in self.res_sems_tags.values(): + onglet = res_sem_tag.get_repr() + df = res_sem_tag.df_moyennes_et_classements() # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) output.seek(0) @@ -156,20 +156,46 @@ class JuryPE(object): ) def _gen_xls_rcss_tags(self, zipfile: ZipFile): - """Génère les RCS (combinaisons de semestres suivis - par un étudiant) + """Génère : + + * 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( "*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants" ) - self.rcss = RCSsJuryPE(self.diplome) - self.rcss.cree_rcss(self.etudiants) + self.rcss_jury = pe_rcs.RCSsJuryPE(self.diplome) + self.rcss_jury.cree_rcss(self.etudiants) # Génère les moyennes par tags des trajectoires 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 output = io.BytesIO() @@ -177,7 +203,7 @@ class JuryPE(object): output, engine="openpyxl" ) as writer: 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() # écriture dans l'onglet 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 pe_affichage.pe_print("*** Génère les interclassements par aggrégat") 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 @@ -339,14 +365,14 @@ class JuryPE(object): df_synthese = pd.DataFrame.from_dict(donnees_etudiants, orient="index") # Ajout des aggrégats - for aggregat in TOUS_LES_RCS: - descr = TYPES_RCS[aggregat]["descr"] + for aggregat in pe_rcs.TOUS_LES_RCS: + descr = pe_rcs.TYPES_RCS[aggregat]["descr"] # Les trajectoires (tagguées) suivies par les étudiants pour l'aggrégat et le tag # considéré trajectoires_tagguees = [] for etudid in etudids: - trajectoire = self.rcss.suivi[etudid][aggregat] + trajectoire = self.rcss_jury.suivi[etudid][aggregat] if trajectoire: tid = trajectoire.rcs_id trajectoire_tagguee = self.rcss_tags[tid] @@ -485,14 +511,14 @@ class JuryPE(object): # Une ligne pour le 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 donnees[tag] |= get_defaut_dict_synthese_aggregat( aggregat, self.diplome ) # La trajectoire de l'étudiant sur l'aggrégat - trajectoire = self.rcss.suivi[etudid][aggregat] + trajectoire = self.rcss_jury.suivi[etudid][aggregat] if trajectoire: trajectoire_tagguee = self.rcss_tags[trajectoire.rcs_id] if tag in trajectoire_tagguee.moyennes_tags: @@ -541,7 +567,7 @@ def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict: 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. Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire des étudiants (cf. attribut etudiants.cursus). @@ -563,68 +589,28 @@ def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict: semestres_tags = {} for frmsem_id, formsemestre in formsemestres.items(): # Crée le semestre_tag et exécute les calculs de moyennes - formsemestretag = SemestreTag(frmsem_id) - pe_affichage.pe_print( - f" --> Semestre taggué {formsemestretag.nom} sur la base de {formsemestre}" - ) + formsemestretag = ResSemTag(frmsem_id) + # Stocke le semestre taggué semestres_tags[frmsem_id] = formsemestretag 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( etudiants: EtudiantsJuryPE, - trajectoires_jury_pe: RCSsJuryPE, - trajectoires_tagguees: dict[tuple, RCS], + trajectoires_jury_pe: pe_rcs.RCSsJuryPE, + trajectoires_tagguees: dict[tuple, pe_rcs.RCS], ): """Interclasse les étudiants, (nom d') aggrégat par aggrégat, pour fournir un classement sur la promo. Le classement est établi au regard du nombre d'étudiants ayant participé au même aggrégat. """ aggregats_interclasses_taggues = {} - for nom_aggregat in TOUS_LES_RCS: + for nom_aggregat in pe_rcs.TOUS_LES_RCS: pe_affichage.pe_print(f" --> Interclassement {nom_aggregat}") interclass = RCSInterclasseTag( nom_aggregat, etudiants, trajectoires_jury_pe, trajectoires_tagguees @@ -642,7 +628,7 @@ def get_defaut_dict_synthese_aggregat(nom_rcs: str, diplome: int) -> dict: diplôme : l'année du diplôme """ # 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}" donnees = { @@ -686,7 +672,7 @@ def get_dict_synthese_aggregat( à l'aggrégat donné et pour un tag donné""" donnees = {} # 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) note = np.nan diff --git a/app/pe/pe_moytag.py b/app/pe/pe_moytag.py new file mode 100644 index 00000000..159661d1 --- /dev/null +++ b/app/pe/pe_moytag.py @@ -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 diff --git a/app/pe/pe_rcs.py b/app/pe/pe_rcs.py index f9ec6688..ecd5d764 100644 --- a/app/pe/pe_rcs.py +++ b/app/pe/pe_rcs.py @@ -116,6 +116,10 @@ class RCS: self.semestres_aggreges = {} """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]): """Ajout de semestres aux semestres à regrouper diff --git a/app/pe/pe_rcstag.py b/app/pe/pe_rcstag.py index c3d3a05f..f44ba13a 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/pe_rcstag.py @@ -37,17 +37,19 @@ Created on Fri Sep 9 09:15:05 2016 """ 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 numpy as np 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): 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 (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) """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)""" for tag in self.tags_sorted: 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): """Egalité de 2 RCS taggués sur la base de leur identifiant""" @@ -172,6 +174,7 @@ class RCSTag(TableTag): tags = [] for frmsem_id in self.semestres_tags_aggreges: tags.extend(self.semestres_tags_aggreges[frmsem_id].tags_sorted) + pe_affichage.pe_print(f"* Tags : {', '.join(tags)}") return sorted(set(tags)) diff --git a/app/pe/pe_ressemtag.py b/app/pe/pe_ressemtag.py new file mode 100644 index 00000000..8729580f --- /dev/null +++ b/app/pe/pe_ressemtag.py @@ -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"
  • {tag}
  • " 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 {self.formsemestre.titre_annee()} + contient le{s} tag{s} réservé{s} suivant : +
      + {liste_intersection} +
    + 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 diff --git a/app/pe/pe_semtag.py b/app/pe/pe_semtag.py index 8e602531..645c3da7 100644 --- a/app/pe/pe_semtag.py +++ b/app/pe/pe_semtag.py @@ -35,290 +35,216 @@ 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 -from app.pe.pe_tabletags import TableTag, MoyenneTag -from app.scodoc import sco_tag_module -from app.scodoc.codes_cursus import UE_SPORT +from app.models import UniteEns +from app.pe import pe_affichage +from app.pe.pe_ressemtag import ResSemTag +import pandas as pd +import numpy as np +from app.pe.pe_rcs import RCS + +from app.pe.pe_tabletags import TableTag +from app.pe.pe_moytag import MoyennesTag -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. - """ +class SemTag(TableTag): + def __init__(self, rcs: RCS, semestres_taggues: dict[int, ResSemTag]): + """Calcule les moyennes/classements par tag à un RCS d'un seul semestre + (ici semestre) de type 'Sx' (par ex. 'S1', 'S2', ...) : + + * 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: - 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) - # Le semestre - self.formsemestre_id = formsemestre_id - self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + self.rcs_id = rcs.rcs_id + """Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)""" - # 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() + """Représentation textuelle du RCS taggué""" - # Les résultats du semestre - self.nt = load_formsemestre_results(self.formsemestre) + # Les données du formsemestre_terminal + self.formsemestre_terminal = rcs.formsemestre_final + """Le formsemestre terminal""" - # 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 résultats du formsemestre terminal + nt = load_formsemestre_results(self.formsemestre_terminal) - # 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 + self.semestres_aggreges = rcs.semestres_aggreges + """Les semestres aggrégés""" - # 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 + self.semestres_tags_aggreges = {} + """Les semestres tags associés aux semestres aggrégés""" + try: + for frmsem_id in self.semestres_aggreges: + self.semestres_tags_aggreges[frmsem_id] = semestres_taggues[frmsem_id] + except: + raise ValueError("Semestres taggués manquants") - # Les tags : - ## Saisis par l'utilisateur - tags_personnalises = get_synthese_tags_personnalises_semestre( - self.nt.formsemestre - ) - noms_tags_perso = sorted(list(set(tags_personnalises.keys()))) + # Les données des étudiants + self.etuds = nt.etuds + """Les étudiants""" + self.etudids = [etud.etudid for etud in self.etuds] + """Les etudids""" + self.etats_civils = { + etudid: self.etuds[etudid].etat_civil for etudid in self.etudids + } + """Les états civils""" - ## 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())) - noms_tags_auto = ["but"] # + noms_tags_comp - self.tags = noms_tags_perso + noms_tags_auto - """Tags du semestre taggué""" + # Les tags + self.tags_sorted = self.comp_tags_list() + """Tags extraits du semestre terminal de l'aggrégat""" - ## Vérifie l'unicité des tags - if len(set(self.tags)) != len(self.tags): - intersection = list(set(noms_tags_perso) & set(noms_tags_auto)) - liste_intersection = "\n".join( - [f"
  • {tag}
  • " for tag in intersection] - ) - 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 {self.formsemestre.titre_annee()} - contient le{s} tag{s} réservé{s} suivant : -
      - {liste_intersection} -
    - 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) + # Les UEs + self.ues = self.comp_ues(tag="but") + self.acronymes_ues_sorted = sorted([ue.acronyme for ue in self.ues.values()]) + """UEs extraites du semestre terminal de l'aggrégat (avec + check de concordance sur les UE des semestres_aggrégés)""" - 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.moyennes_tags = {} + self.notes: dict[str, pd.DataFrame] = {} + """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: - # pe_affichage.pe_print(f" -> Traitement du tag {tag}") - moy_ues_tag = self.compute_moy_ues_tag(tag, tags_personnalises) - moy_gen_tag = self.compute_moy_gen_tag(moy_ues_tag) + # Calcule des moyennes sous forme d'un dataframe""" + self.notes[tag] = compute_notes_ues(notes_cube, self.etudids, self.acronymes_ues) - self.moyennes_tags[tag] = MoyenneTag( - tag, ues_hors_sport, moy_ues_tag, moy_gen_tag - ) + # Les moyennes + self.moyennes_tags[tag] = MoyennesTag(tag, self.notes[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, ) + def __eq__(self, other): + """Egalité de 2 RCS taggués sur la base de leur identifiant""" + return self.rcs_id == other.rcs_id - # 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"] = MoyenneTag("but", ues_hors_sport, df_ues, moy_gen_but) + def get_repr(self, verbose=False) -> str: + """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle + est basée)""" + return self.rcs.get_repr(verbose=verbose) + 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() - """Tags (personnalisés+compétences) par ordre alphabétique""" + dfs = {} - # 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() - """Dataframe synthétique des notes par tag""" + # Charge les notes du semestre 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( - f" => Traitement des tags {', '.join(self.tags_sorted)}" - ) + # UEs communes à celles du SemTag (celles du dernier semestre du RCS) + ues_communes = list(set(acronymes_ues_sorted) & set(acronymes_ues_sem)) - def get_repr(self): - """Nom affiché pour le semestre taggué""" - return app.pe.pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True) + # Etudiants communs + etudids_communs = df.index.intersection(notes.index) - def compute_moy_ues_tag(self, tag: str, tags_infos: dict) -> pd.Series: - """Calcule la moyenne par UE des étudiants pour le tag indiqué, - pour ce SemestreTag, en ayant connaissance des informations sur - les tags (dictionnaire donnant les coeff de repondération) + # Recopie + df.loc[etudids_communs, ues_communes] = notes.loc[etudids_communs, ues_communes] - Sont pris en compte les modules implémentés associés au tag, - avec leur éventuel coefficient de **repondération**, en utilisant les notes - chargées pour ce SemestreTag. + # Supprime tout ce qui n'est pas numérique + for col in df.columns: + 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: - 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é - modimpls_mask = [ - modimpl.module.ue.type != UE_SPORT - for modimpl in self.formsemestre.modimpls_sorted - ] + def comp_ues(self, tag="but") -> dict[int, UniteEns]: + """Récupère les UEs à aggréger, en s'appuyant sur la moyenne générale + (tag but) du semestre final du RCS - # 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 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. + Returns: + Un dictionnaire donnant les UEs """ - # 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 + dernier_frmid = self.formsemestre_terminal.formsemestre_id + dernier_semestre_tag = self.semestres_tags_aggreges[dernier_frmid] + moy_tag = dernier_semestre_tag.moyennes_tags[tag] + return moy_tag.ues # les UEs -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 - aucoeff et pondération fournie avec le tag (par défaut 1 si non indiquée)). - +def compute_notes_ues(set_cube: np.array, etudids: list, acronymes_ues: list): + """Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE + par UE) obtenue par un étudiant à un semestre. Args: - formsemestre: Le formsemestre à la base de la recherche des tags - - Return: - Un dictionnaire de 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 - + set_cube: notes moyennes aux modules ndarray + (semestre_ids x etudids x UEs), des floats avec des NaN + etudids: liste des étudiants (dim. 0 du cube) + acronymes_ues: liste des acronymes des ues (dim. 1 du cube) Returns: - Dictionnaire {ue_id: nom_competence} lisant tous les noms des compétences - en les raccrochant à leur ue + Un DataFrame avec pour columns les moyennes par ues, + et pour rows les etudid """ - # Les résultats du semestre - nt = load_formsemestre_results(formsemestre) + nb_etuds, nb_ues, nb_semestres = set_cube.shape + assert nb_etuds == len(etudids) + assert nb_ues == len(acronymes_ues) - 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 + # Quelles entrées du cube contiennent des notes ? + mask = ~np.isnan(set_cube) + + # Enlève les NaN du cube pour les entrées manquantes + set_cube_no_nan = np.nan_to_num(set_cube, nan=-1.0) + + # 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 diff --git a/app/pe/pe_tabletags.py b/app/pe/pe_tabletags.py index a761fa0b..a31df300 100644 --- a/app/pe/pe_tabletags.py +++ b/app/pe/pe_tabletags.py @@ -37,195 +37,11 @@ Created on Thu Sep 8 09:36:33 2016 @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 -from app.scodoc.codes_cursus import UE_SPORT - 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): def __init__(self): """Classe centralisant différentes méthodes communes aux @@ -233,7 +49,6 @@ class TableTag(object): """ pass - # ----------------------------------------------------------------------------------------------------------- def get_all_tags(self): """Liste des tags de la table, triée par ordre alphabétique, extraite des clés du dictionnaire ``moyennes_tags`` connues (tags en doublon @@ -261,8 +76,9 @@ class TableTag(object): tags_tries = self.get_all_tags() for tag in tags_tries: moy_tag = self.moyennes_tags[tag] - df = df.join(moy_tag.synthese["notes"].rename(f"Moy {tag}")) - df = df.join(moy_tag.synthese["classements"].rename(f"Class {tag}")) + moy_gen = moy_tag.moy_gen + df = df.join(moy_gen.synthese["notes"].rename(f"Moy {tag}")) + df = df.join(moy_gen.synthese["classements"].rename(f"Class {tag}")) return df diff --git a/config.py b/config.py index af1a173e..eefebdfc 100755 --- a/config.py +++ b/config.py @@ -65,7 +65,7 @@ class DevConfig(Config): ) SECRET_KEY = os.environ.get("DEV_SECRET_KEY") or "bb3faec7d9a34eb68a8e3e710087d87a" # pour le avoir url_for dans le shell: - # SERVER_NAME = "http://localhost:8080" + SERVER_NAME = "http://localhost:8080" class TestConfig(DevConfig): From b8cb592ac93026d232b9f203da531e270ed547a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Fri, 16 Feb 2024 16:07:48 +0100 Subject: [PATCH 06/41] =?UTF-8?q?Calcul=20des=20RCS=20de=20type=20Sx=20(av?= =?UTF-8?q?ec=20s=C3=A9lection=20du=20max=20des=20UEs=20des=20redoublants)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_jury.py | 64 +++++++++++++++++++------- app/pe/pe_moytag.py | 66 ++++++++++++++++++++++++--- app/pe/pe_rcstag.py | 4 +- app/pe/pe_ressemtag.py | 52 +++++++-------------- app/pe/pe_semtag.py | 100 ++++++++++++++++++++++++++++------------- app/pe/pe_tabletags.py | 34 ++++++++++++-- 6 files changed, 224 insertions(+), 96 deletions(-) diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index bfe72bd2..70d97237 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -50,6 +50,7 @@ from zipfile import ZipFile import numpy as np import pandas as pd +from app.pe import pe_semtag from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE import app.pe.pe_affichage as pe_affichage from app.pe.pe_etudiant import * # TODO A éviter -> pe_etudiant. @@ -96,7 +97,9 @@ class JuryPE(object): pe_affichage.pe_print("*** Aucun étudiant diplômé") else: self._gen_xls_diplomes(zipfile) + self._gen_rcss() self._gen_xls_resultats_semestres_taggues(zipfile) + self._gen_xls_semestres_taggues(zipfile) # self._gen_xls_rcss_tags(zipfile) # self._gen_xls_interclassements_rcss(zipfile) # self._gen_xls_synthese_jury_par_tag(zipfile) @@ -142,12 +145,53 @@ class JuryPE(object): output, engine="openpyxl" ) as writer: for res_sem_tag in self.res_sems_tags.values(): - onglet = res_sem_tag.get_repr() + onglet = res_sem_tag.get_repr(verbose=False) df = res_sem_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"resultats_semestres_taggues_{self.diplome}.xlsx", + output.read(), + path="details", + ) + + def _gen_rcss(self): + """Génère les RCS (attribut `rcss_jury`), combinaisons de semestres suivis par les étudiants au sens + d'un nom de RCS (par ex: '3S'). + """ + pe_affichage.pe_print( + "*** Génère les RCS (différentes combinaisons de semestres) des étudiants" + ) + self.rcss_jury = pe_rcs.RCSsJuryPE(self.diplome) + self.rcss_jury.cree_rcss(self.etudiants) + + def _gen_xls_semestres_taggues(self, zipfile: ZipFile): + """Génère les semestres taggués en s'appuyant sur les RCS de type Sx (pour + identifier les redoublements impactant les semestres taggués). + """ + # Génère les moyennes des RCS de type Sx + pe_affichage.pe_print("*** Calcule les moyennes de semestres = RCS de type Sx") + + self.sems_tags = {} + for rcs_id, rcs in self.rcss_jury.rcss.items(): + if rcs.nom.startswith("S"): + self.sems_tags[rcs_id] = pe_semtag.SemTag(rcs, self.res_sems_tags) + + # Intègre le bilan des semestres taggués au zip final + output = io.BytesIO() + with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated + output, engine="openpyxl" + ) as writer: + for sem_tag in self.sems_tags.values(): + onglet = sem_tag.get_repr(verbose=False) + df = sem_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"semestres_taggues_{self.diplome}.xlsx", @@ -156,11 +200,7 @@ class JuryPE(object): ) def _gen_xls_rcss_tags(self, zipfile: ZipFile): - """Génère : - - * 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 + """Génère 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 @@ -183,14 +223,9 @@ class JuryPE(object): """ - pe_affichage.pe_print( - "*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants" - ) - self.rcss_jury = pe_rcs.RCSsJuryPE(self.diplome) - self.rcss_jury.cree_rcss(self.etudiants) - # Génère les moyennes par tags des trajectoires - pe_affichage.pe_print("*** Calcule les moyennes par tag des RCS possibles") + # Génère les moyennes des RCS de type Sx + pe_affichage.pe_print("*** Calcule les moyennes des RCS de type Sx") self.rcss_tags = {} for rcs_id, rcs in self.rcss_jury.rcss.items(): @@ -597,9 +632,6 @@ def compute_resultats_semestres_tag(etudiants: EtudiantsJuryPE) -> dict: return semestres_tags - - - def compute_interclassements( etudiants: EtudiantsJuryPE, trajectoires_jury_pe: pe_rcs.RCSsJuryPE, diff --git a/app/pe/pe_moytag.py b/app/pe/pe_moytag.py index 159661d1..a2428f34 100644 --- a/app/pe/pe_moytag.py +++ b/app/pe/pe_moytag.py @@ -1,9 +1,11 @@ import numpy as np import pandas as pd +from app import comp from app.comp.moy_sem import comp_ranks_series from app.models import UniteEns from app.pe import pe_affichage +from app.scodoc.codes_cursus import UE_SPORT class Moyenne: @@ -157,8 +159,9 @@ class MoyennesTag: def __init__( self, tag: str, - ues: list[UniteEns], + ues: dict[int, UniteEns], notes_ues: pd.DataFrame, + ues_inscr_parcours_df: pd.DataFrame # notes_gen: pd.Series, ): """Classe centralisant la synthèse des moyennes/classements d'une série @@ -169,23 +172,41 @@ class MoyennesTag: 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 + ues_inscr_parcours_df: Les inscriptions des etudid au UE # 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} + self.ues: dict[int, UniteEns] = 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" + acronymes: list[str] = [self.ues[ue_id].acronyme for ue_id in self.ues] + assert len(set(acronymes)) == len( + colonnes + ), "Deux UEs ne peuvent pas avoir le même acronyme" + + # Les inscriptions des etudids aux UEs + self.ues_inscr_parcours_df: pd.DataFrame = ues_inscr_parcours_df + """Les inscriptions des etudids au UE en fonction de leur parcours""" + + # Les coefficients à appliquer aux UEs pour la moyenne générale = ECTS + self.ects = self.ues_inscr_parcours_df.fillna(0.0) * [ + ue.ects + for ue in self.ues.values() # if ue.type != UE_SPORT <= déjà supprimé + ] + # Les profils d'ects (pour debug) + profils_ects = [] + for val in list(self.ects.values): + if tuple(val) not in profils_ects: + profils_ects.append(tuple(val)) # Les moyennes par UE - self.notes_ues = notes_ues + self.notes_ues: pd.DataFrame = notes_ues """Les notes aux UEs (dataframe)""" - self.notes_ues.columns = acronymes # remplace les ue.id par leur acronyme + 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: @@ -193,11 +214,42 @@ class MoyennesTag: self.moys_ues[ue.acronyme] = Moyenne(notes) # Les moyennes générales + notes_gen = self.compute_moy_gen(self.notes_ues, self.ects) 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""" + pe_affichage.pe_print(f"> MoyTag pour {tag} avec") + pe_affichage.pe_print(f" - ues={acronymes}") + pe_affichage.pe_print(f" - ects={profils_ects}") + + def __eq__(self, other): """Egalité de deux MoyenneTag lorsque leur tag sont identiques""" return self.tag == other.tag + + def compute_moy_gen( + self, moy_ues: pd.DataFrame, coeff_ues: pd.DataFrame + ) -> pd.Series: + """Calcule la moyenne générale (toutes UE confondus) + pour le tag considéré, en pondérant les notes obtenues au UE + par les crédits ECTS. + + Args: + moy_ues: Les moyennes etudids x acronymes_ues + coeff_ues: Les coeff etudids x ueids + """ + + # Calcule la moyenne générale dans le semestre (pondérée par le ECTS) + try: + moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects( + moy_ues, + coeff_ues, + # formation_id=self.formsemestre.formation_id, + skip_empty_ues=True, + ) + except TypeError as e: + raise TypeError("Pb dans le calcul de la moyenne toutes UEs confondues") + + return moy_gen_tag diff --git a/app/pe/pe_rcstag.py b/app/pe/pe_rcstag.py index f44ba13a..ef458589 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/pe_rcstag.py @@ -94,7 +94,7 @@ class RCSTag(TableTag): self.etuds = nt.etuds # assert self.etuds == trajectoire.suivi # manque-t-il des étudiants ? - self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds} + self.etats_civils = {etud.etudid: etud.nomprenom for etud in self.etuds} self.tags_sorted = self.do_taglist() """Tags extraits de tous les semestres""" @@ -102,7 +102,7 @@ class RCSTag(TableTag): self.notes_cube = self.compute_notes_cube() """Cube de notes""" - etudids = list(self.etudiants.keys()) + etudids = list(self.etats_civils.keys()) self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted) """Calcul les moyennes par tag sous forme d'un dataframe""" diff --git a/app/pe/pe_ressemtag.py b/app/pe/pe_ressemtag.py index 8729580f..5dff02ea 100644 --- a/app/pe/pe_ressemtag.py +++ b/app/pe/pe_ressemtag.py @@ -70,19 +70,15 @@ class ResSemTag(TableTag): self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id) # Le nom du res_semestre taggué - self.nom = self.get_repr(mode="long") + self.nom = self.get_repr(verbose=True) - pe_affichage.pe_print( - f"--> Résultats de Semestre taggués {self.nom}" - ) + 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 étudiants (etuds, états civils & etudis) + self.add_etuds(self.nt.etuds) # Les notes, les modules implémentés triés, les étudiants, les coeffs, # récupérés notamment de py:mod:`res_but` @@ -109,12 +105,17 @@ class ResSemTag(TableTag): for tag in tags_dict["personnalises"]: # pe_affichage.pe_print(f" -> Traitement du tag {tag}") - infos_tag = tags_dict["personnalises"][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) + ues_dict = {ue.id: ue for ue in ues_hors_sport} self.moyennes_tags[tag] = MoyennesTag( - tag, ues_hors_sport, moy_ues_tag # moy_gen_tag + tag, + ues_dict, + moy_ues_tag, + self.ues_inscr_parcours_df + # moy_gen_tag ) # Ajoute les d'UE moyennes générales de BUT pour le semestre considéré @@ -128,8 +129,10 @@ class ResSemTag(TableTag): ) # moy_ues = self.nt.etud_moy_ue[ue_id] # moy_gen_but = self.nt.etud_moy_gen + ues_dict = {ue.id: ue for ue in ues_hors_sport} + self.moyennes_tags["but"] = MoyennesTag( - "but", ues_hors_sport, df_ues #, moy_gen_but + "but", ues_dict, df_ues, self.ues_inscr_parcours_df # , moy_gen_but ) self.tags_sorted = self.get_all_tags() @@ -144,11 +147,11 @@ class ResSemTag(TableTag): # f" => Traitement des tags {', '.join(self.tags_sorted)}" # ) - def get_repr(self, mode="long"): + def get_repr(self, verbose=False): """Nom affiché pour le semestre taggué""" - if mode == "short": + if verbose: return f"{self.formsemestre} ({self.formsemestre_id})" - else: # mode == "long" + else: return pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True) def compute_moy_ues_tag(self, info_tag: dict[int, dict]) -> pd.DataFrame: @@ -192,25 +195,6 @@ class ResSemTag(TableTag): ) 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, @@ -272,8 +256,6 @@ class ResSemTag(TableTag): 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) diff --git a/app/pe/pe_semtag.py b/app/pe/pe_semtag.py index 645c3da7..d98cd084 100644 --- a/app/pe/pe_semtag.py +++ b/app/pe/pe_semtag.py @@ -49,7 +49,7 @@ from app.pe.pe_moytag import MoyennesTag class SemTag(TableTag): - def __init__(self, rcs: RCS, semestres_taggues: dict[int, ResSemTag]): + def __init__(self, rcs: RCS, res_sems_tags: dict[int, ResSemTag]): """Calcule les moyennes/classements par tag à un RCS d'un seul semestre (ici semestre) de type 'Sx' (par ex. 'S1', 'S2', ...) : @@ -62,7 +62,7 @@ class SemTag(TableTag): Args: rcs: Un RCS (identifié par un nom et l'id de son semestre terminal) - semestres_taggues: Les données sur les semestres taggués + res_sems_tags: Les données sur les résultats des semestres taggués """ TableTag.__init__(self) @@ -88,27 +88,20 @@ class SemTag(TableTag): self.semestres_aggreges = rcs.semestres_aggreges """Les semestres aggrégés""" - self.semestres_tags_aggreges = {} - """Les semestres tags associés aux semestres aggrégés""" + self.res_sems_tags = {} + """Les résultats des semestres taggués (limités aux semestres aggrégés)""" try: for frmsem_id in self.semestres_aggreges: - self.semestres_tags_aggreges[frmsem_id] = semestres_taggues[frmsem_id] + self.res_sems_tags[frmsem_id] = res_sems_tags[frmsem_id] except: - raise ValueError("Semestres taggués manquants") + raise ValueError("Résultats des semestres taggués manquants") - # Les données des étudiants - self.etuds = nt.etuds - """Les étudiants""" - self.etudids = [etud.etudid for etud in self.etuds] - """Les etudids""" - self.etats_civils = { - etudid: self.etuds[etudid].etat_civil for etudid in self.etudids - } - """Les états civils""" + # Les étudiants (etuds, états civils & etudis) + self.add_etuds(nt.etuds) # Les tags self.tags_sorted = self.comp_tags_list() - """Tags extraits du semestre terminal de l'aggrégat""" + """Tags (extraits uniquement du semestre terminal de l'aggrégat)""" # Les UEs self.ues = self.comp_ues(tag="but") @@ -116,20 +109,33 @@ class SemTag(TableTag): """UEs extraites du semestre terminal de l'aggrégat (avec check de concordance sur les UE des semestres_aggrégés)""" + # Les inscriptions aux UEs + self.ues_inscr_parcours_df = self.comp_ues_inscr_parcours(tag="but") + """Les inscriptions aux UEs (extraites uniquement du semestre terminal)""" + self.moyennes_tags: dict[str, MoyennesTag] = {} """Moyennes/classements par tag (qu'ils soient personnalisés ou automatiques)""" - self.notes: dict[str, pd.DataFrame] = {} - """Les notes aux différents tags""" + self.moyennes_tags: dict[str, pd.DataFrame] = {} + """Les notes aux UEs dans différents tags""" + # Masque des inscriptions + inscr_mask = self.ues_inscr_parcours_df.to_numpy() for tag in self.tags_sorted: # Cube de note - notes_cube = self.compute_notes_ues_cube(tag, self.acronymes_ues) + notes_cube = self.compute_notes_ues_cube(tag, self.acronymes_ues_sorted) # Calcule des moyennes sous forme d'un dataframe""" - self.notes[tag] = compute_notes_ues(notes_cube, self.etudids, self.acronymes_ues) - + moys_ues = compute_notes_ues( + notes_cube, + self.etudids, + self.acronymes_ues_sorted, + inscr_mask, + ) # Les moyennes - self.moyennes_tags[tag] = MoyennesTag(tag, self.notes[tag]) + self.moyennes_tags[tag] = MoyennesTag(tag, + self.ues, + moys_ues, + self.ues_inscr_parcours_df) def __eq__(self, other): """Egalité de 2 RCS taggués sur la base de leur identifiant""" @@ -147,7 +153,7 @@ class SemTag(TableTag): # 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()) + semestres_id = list(self.res_sems_tags.keys()) dfs = {} @@ -156,10 +162,12 @@ class SemTag(TableTag): df = pd.DataFrame(np.nan, index=etudids, columns=acronymes_ues_sorted) # Charge les notes du semestre tag - sem_tag = self.semestres_tags_aggreges[frmsem_id] + sem_tag = self.res_sems_tags[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 + notes = moys_tag.notes_ues # dataframe etudids x ues + acronymes_ues_sem = list( + notes.columns + ) # les acronymes des UEs du semestre tag # UEs communes à celles du SemTag (celles du dernier semestre du RCS) ues_communes = list(set(acronymes_ues_sorted) & set(acronymes_ues_sem)) @@ -168,7 +176,9 @@ class SemTag(TableTag): etudids_communs = df.index.intersection(notes.index) # Recopie - df.loc[etudids_communs, ues_communes] = notes.loc[etudids_communs, ues_communes] + df.loc[etudids_communs, ues_communes] = notes.loc[ + etudids_communs, ues_communes + ] # Supprime tout ce qui n'est pas numérique for col in df.columns: @@ -182,7 +192,7 @@ class SemTag(TableTag): 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): + def comp_tags_list(self) -> list[str]: """Récupère les tag du semestre taggué associé au semestre final du RCS Returns: @@ -190,7 +200,7 @@ class SemTag(TableTag): """ tags = [] dernier_frmid = self.formsemestre_terminal.formsemestre_id - dernier_semestre_tag = self.semestres_tags_aggreges[dernier_frmid] + dernier_semestre_tag = self.res_sems_tags[dernier_frmid] tags = dernier_semestre_tag.tags_sorted pe_affichage.pe_print(f"* Tags : {', '.join(tags)}") return tags @@ -203,12 +213,29 @@ class SemTag(TableTag): Un dictionnaire donnant les UEs """ dernier_frmid = self.formsemestre_terminal.formsemestre_id - dernier_semestre_tag = self.semestres_tags_aggreges[dernier_frmid] + dernier_semestre_tag = self.res_sems_tags[dernier_frmid] moy_tag = dernier_semestre_tag.moyennes_tags[tag] return moy_tag.ues # les UEs + def comp_ues_inscr_parcours(self, tag="but") -> pd.DataFrame: + """Récupère les informations d'inscription des étudiants aux UEs : ne + conserve que les UEs du semestre terminal (pour les redoublants) -def compute_notes_ues(set_cube: np.array, etudids: list, acronymes_ues: list): + Returns: + Un dataFrame etudids x UE indiquant si un étudiant est inscrit à une UE + """ + dernier_frmid = self.formsemestre_terminal.formsemestre_id + dernier_semestre_tag = self.res_sems_tags[dernier_frmid] + moy_tag = dernier_semestre_tag.moyennes_tags[tag] + return moy_tag.ues_inscr_parcours_df + + +def compute_notes_ues( + set_cube: np.array, + etudids: list, + acronymes_ues: list, + inscr_mask: np.array, +): """Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE par UE) obtenue par un étudiant à un semestre. @@ -217,18 +244,27 @@ def compute_notes_ues(set_cube: np.array, etudids: list, acronymes_ues: list): (semestre_ids x etudids x UEs), des floats avec des NaN etudids: liste des étudiants (dim. 0 du cube) acronymes_ues: liste des acronymes des ues (dim. 1 du cube) + inscr_mask: masque etudids x UE traduisant les inscriptions des + étudiants aux UE (du semestre terminal) Returns: Un DataFrame avec pour columns les moyennes par ues, et pour rows les etudid """ nb_etuds, nb_ues, nb_semestres = set_cube.shape + nb_etuds_mask, nb_ues_mask = inscr_mask.shape assert nb_etuds == len(etudids) assert nb_ues == len(acronymes_ues) + assert nb_etuds == nb_etuds_mask + assert nb_ues == nb_ues_mask # Quelles entrées du cube contiennent des notes ? mask = ~np.isnan(set_cube) - # Enlève les NaN du cube pour les entrées manquantes + # Entrées à garder dans le cube en fonction du mask d'inscription + inscr_mask_3D = np.stack([inscr_mask]*nb_semestres, axis=-1) + set_cube = set_cube*inscr_mask_3D + + # Enlève les NaN du cube pour les entrées manquantes : NaN -> -1.0 set_cube_no_nan = np.nan_to_num(set_cube, nan=-1.0) # Les moyennes par ues diff --git a/app/pe/pe_tabletags.py b/app/pe/pe_tabletags.py index a31df300..f876dedb 100644 --- a/app/pe/pe_tabletags.py +++ b/app/pe/pe_tabletags.py @@ -39,6 +39,8 @@ Created on Thu Sep 8 09:36:33 2016 import pandas as pd +from app.models import Identite + TAGS_RESERVES = ["but"] @@ -47,7 +49,23 @@ class TableTag(object): """Classe centralisant différentes méthodes communes aux SemestreTag, TrajectoireTag, AggregatInterclassTag """ - pass + # Les étudiants + self.etuds: list[Identite] = None # A venir + """Les étudiants""" + self.etats_civils: dict[int, Identite] = None + """Les états civils""" + self.etudids: list[int] = None + """Les etudids""" + + def add_etuds(self, etuds: list[Identite]): + """Mémorise les informations sur les étudiants + + Args: + etuds: la liste des identités de l'étudiant + """ + self.etuds = etuds + self.etats_civils = {etud.etudid: etud.etat_civil for etud in self.etuds} + self.etudids = list(self.etats_civils.keys()) def get_all_tags(self): """Liste des tags de la table, triée par ordre alphabétique, @@ -70,15 +88,23 @@ class TableTag(object): Le dataframe des notes et des classements """ - etudiants = self.etudiants + etudiants = self.etats_civils df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"]) tags_tries = self.get_all_tags() for tag in tags_tries: moy_tag = self.moyennes_tags[tag] + for acronyme in moy_tag.moys_ues: + moy = moy_tag.moys_ues[acronyme] # une moyenne + df = df.join(moy.synthese["notes"].rename(f"Moy {tag}-{acronyme}")) + df = df.join( + moy.synthese["classements"].rename(f"Class {tag}-{acronyme}") + ) moy_gen = moy_tag.moy_gen - df = df.join(moy_gen.synthese["notes"].rename(f"Moy {tag}")) - df = df.join(moy_gen.synthese["classements"].rename(f"Class {tag}")) + df = df.join(moy_gen.synthese["notes"].rename(f"Moy {tag} (gen)")) + df = df.join(moy_gen.synthese["classements"].rename(f"Class {tag} (gen)")) + + df.sort_values(by=['nom']) return df From 828c619c74395f1a29a570f02524b2a9e6126d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Sat, 17 Feb 2024 02:35:43 +0100 Subject: [PATCH 07/41] =?UTF-8?q?Am=C3=A9liorations=20diverses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_etudiant.py | 72 +++++++----- app/pe/pe_jury.py | 114 ++++++++----------- app/pe/pe_rcs.py | 102 +++++++++-------- app/pe/pe_rcstag.py | 42 +++---- app/pe/pe_ressemtag.py | 64 +++-------- app/pe/{pe_semtag.py => pe_sxtag.py} | 164 +++++++++++---------------- app/pe/pe_tabletags.py | 8 +- 7 files changed, 259 insertions(+), 307 deletions(-) rename app/pe/{pe_semtag.py => pe_sxtag.py} (58%) diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 7448bf04..9851e313 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2024 Emmanuel Viennet. c 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 @@ -59,7 +59,7 @@ class EtudiantsJuryPE: self.identites: dict[int, Identite] = {} # ex. ETUDINFO_DICT "Les identités des étudiants traités pour le jury" - self.cursus: dict[int, dict] = {} + self.trajectoires: dict[int, dict] = {} "Les cursus (semestres suivis, abandons) des étudiants" self.trajectoires = {} @@ -164,7 +164,7 @@ class EtudiantsJuryPE: """ etudids = [ etudid - for etudid, cursus_etud in self.cursus.items() + for etudid, cursus_etud in self.trajectoires.items() if cursus_etud["diplome"] == self.annee_diplome and cursus_etud["abandon"] is False ] @@ -181,7 +181,7 @@ class EtudiantsJuryPE: """ etudids = [ etudid - for etudid, cursus_etud in self.cursus.items() + for etudid, cursus_etud in self.trajectoires.items() if cursus_etud["diplome"] != self.annee_diplome or cursus_etud["abandon"] is True ] @@ -225,7 +225,7 @@ class EtudiantsJuryPE: if formsemestre.formation.is_apc() } - self.cursus[etudid] = { + self.trajectoires[etudid] = { "etudid": etudid, # les infos sur l'étudiant "etat_civil": identite.etat_civil, # Ajout à la table jury "nom": identite.nom, @@ -241,32 +241,36 @@ class EtudiantsJuryPE: } # Si l'étudiant est succeptible d'être diplomé - if self.cursus[etudid]["diplome"] == self.annee_diplome: + if self.trajectoires[etudid]["diplome"] == self.annee_diplome: # Est-il démissionnaire : charge son dernier semestre pour connaitre son état ? dernier_semes_etudiant = formsemestres[0] res = load_formsemestre_results(dernier_semes_etudiant) etud_etat = res.get_etud_etat(etudid) if etud_etat == scu.DEMISSION: - self.cursus[etudid]["abandon"] = True + self.trajectoires[etudid]["abandon"] = True else: # Est-il réorienté ou a-t-il arrêté (volontairement) sa formation ? - self.cursus[etudid]["abandon"] = arret_de_formation( + self.trajectoires[etudid]["abandon"] = arret_de_formation( identite, cosemestres ) def get_semestres_significatifs(self, etudid: int): - """Ensemble des semestres d'un étudiant, qui l'auraient amené à être diplomé - l'année visée (supprime les semestres qui conduisent à une diplomation - postérieure à celle du jury visé) + """Ensemble des semestres d'un étudiant, qui : + + * l'amènent à être diplômé à l'année visée + * l'auraient amené à être diplômé à l'année visée s'il n'avait pas redoublé et sera donc + diplômé plus tard + + Supprime les semestres qui conduisent à une diplomation postérieure à celle du jury visé. Args: etudid: L'identifiant d'un étudiant Returns: - Un dictionnaire ``{fid: FormSemestre(fid)`` dans lequel les semestres - amènent à une diplomation avant l'annee de diplomation du jury + Un dictionnaire ``{fid: FormSemestre(fid)}`` dans lequel les semestres + amènent à une diplômation antérieur à celle de la diplômation visée par le jury jury """ - semestres_etudiant = self.cursus[etudid]["formsemestres"] + semestres_etudiant = self.trajectoires[etudid]["formsemestres"] semestres_significatifs = {} for fid in semestres_etudiant: semestre = semestres_etudiant[fid] @@ -281,7 +285,7 @@ class EtudiantsJuryPE: Cette structuration s'appuie sur les numéros de semestre: pour chaque Si, stocke : le dernier semestre (en date) de numéro i qu'il a suivi (1 ou 0 si pas encore suivi). - Ce semestre influera les interclassement par semestre dans la promo. + Ce semestre influera les interclassements par semestre dans la promo. """ semestres_significatifs = self.get_semestres_significatifs(etudid) @@ -293,14 +297,14 @@ class EtudiantsJuryPE: for fid, sem_sig in semestres_significatifs.items() if sem_sig.semestre_id == i } - self.cursus[etudid][f"S{i}"] = semestres_i + self.trajectoires[etudid][f"S{i}"] = semestres_i - def get_formsemestres_terminaux_aggregat( - self, aggregat: str + def get_formsemestres_finals_des_rcs( + self, nom_rcs: str ) -> dict[int, FormSemestre]: - """Pour un aggrégat donné, ensemble des formsemestres terminaux possibles pour l'aggrégat - (pour l'aggrégat '3S' incluant S1+S2+S3, a pour semestre terminal S3). - Ces formsemestres traduisent : + """Pour un nom de RCS donné, ensemble des formsemestres finals possibles + pour les RCS. Par ex. un RCS '3S' incluant S1+S2+S3 a pour semestre final un S3. + Les formsemestres finals obtenus traduisent : * les différents parcours des étudiants liés par exemple au choix de modalité (par ex: S1 FI + S2 FI + S3 FI ou S1 FI + S2 FI + S3 UFA), en renvoyant les @@ -311,14 +315,14 @@ class EtudiantsJuryPE: renvoyant les formsemestre_id du S3 (1ère session) et du S3 (2ème session) Args: - aggregat: L'aggrégat + nom_rcs: Le nom du RCS (parmi Sx, xA, xS) Returns: Un dictionnaire ``{fid: FormSemestre(fid)}`` """ formsemestres_terminaux = {} for trajectoire_aggr in self.trajectoires.values(): - trajectoire = trajectoire_aggr[aggregat] + trajectoire = trajectoire_aggr[nom_rcs] if trajectoire: # Le semestre terminal de l'étudiant de l'aggrégat fid = trajectoire.formsemestre_final.formsemestre_id @@ -334,7 +338,7 @@ class EtudiantsJuryPE: """ nbres_semestres = [] for etudid in etudids: - nbres_semestres.append(self.cursus[etudid]["nb_semestres"]) + nbres_semestres.append(self.trajectoires[etudid]["nb_semestres"]) if not nbres_semestres: return 0 return max(nbres_semestres) @@ -355,7 +359,7 @@ class EtudiantsJuryPE: for etudid in etudids: etudiant = self.identites[etudid] - cursus = self.cursus[etudid] + cursus = self.trajectoires[etudid] formsemestres = cursus["formsemestres"] if cursus["diplome"]: @@ -442,7 +446,7 @@ def get_annee_diplome(etud: Identite) -> int | None: def get_semestres_apc(identite: Identite) -> list: - """Liste des semestres d'un étudiant qui corresponde à une formation APC. + """Liste des semestres d'un étudiant qui correspondent à une formation APC. Args: identite: L'identité d'un étudiant @@ -675,3 +679,19 @@ def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str: description.append(f"({semestre.formsemestre_id})") return " ".join(description) + + +def convert_trajectoire_to_sxtag_id(trajectoire: dict[int:FormSemestre]) -> (int, int): + """Partant d'une trajectoire (dictionnaire de la forme {fid: FormSemestre}), + renvoie l'identifiant (rang_sem, fid du semestre_terminal) associé""" + if not trajectoire: + return None + rangs = [formsemestre.semestre_id for formsemestre in trajectoire.values()] + assert rangs == min(rangs), "Les trajectoires doivent être de même rang" + rang = min(rangs) + fid_terminal = list(trajectoire.values())[0].formsemestre_id + for fid, formsemestre in trajectoire.items(): + if trajectoire[fid_terminal].date_fin <= formsemestre.date_fin: + fid_terminal = fid + return (rang, fid_terminal) + diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 70d97237..e63cd227 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -50,13 +50,13 @@ from zipfile import ZipFile import numpy as np import pandas as pd -from app.pe import pe_semtag +from app.pe import pe_sxtag from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE import app.pe.pe_affichage as pe_affichage from app.pe.pe_etudiant import * # TODO A éviter -> pe_etudiant. import app.pe.pe_rcs as pe_rcs from app.pe.pe_rcstag import RCSTag -from app.pe.pe_ressemtag import ResSemTag +from app.pe.pe_ressemtag import ResSemBUTTag from app.pe.pe_interclasstag import RCSInterclasseTag @@ -97,10 +97,10 @@ class JuryPE(object): pe_affichage.pe_print("*** Aucun étudiant diplômé") else: self._gen_xls_diplomes(zipfile) - self._gen_rcss() - self._gen_xls_resultats_semestres_taggues(zipfile) - self._gen_xls_semestres_taggues(zipfile) - # self._gen_xls_rcss_tags(zipfile) + + self._gen_xls_ressembuttags(zipfile) + self._gen_xls_sxtags(zipfile) + self._gen_xls_rcss_tags(zipfile) # self._gen_xls_interclassements_rcss(zipfile) # self._gen_xls_synthese_jury_par_tag(zipfile) # self._gen_xls_synthese_par_etudiant(zipfile) @@ -134,17 +134,26 @@ class JuryPE(object): path="details", ) - 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""" - pe_affichage.pe_print("*** Génère les résultats des semestres taggués") - self.res_sems_tags = compute_resultats_semestres_tag(self.etudiants) + def _gen_xls_ressembuttags(self, zipfile: ZipFile): + """Calcule les moyennes par tag des résultats des Semestres BUT""" + pe_affichage.pe_print("*** Génère les ResSemBUTTag (résultats des semestres BUT taggués)") + + formsemestres = get_formsemestres_etudiants(self.etudiants) + pe_affichage.pe_print(f" --> {len(formsemestres)} résultats de semestres à considérer") + + ressembuttags = {} + for frmsem_id, formsemestre in formsemestres.items(): + # Crée le semestre_tag et exécute les calculs de moyennes + ressembuttags[frmsem_id] = ResSemBUTTag(formsemestre) + + return ressembuttags # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: - for res_sem_tag in self.res_sems_tags.values(): + for res_sem_tag in self.ressembuttags.values(): onglet = res_sem_tag.get_repr(verbose=False) df = res_sem_tag.df_moyennes_et_classements() # écriture dans l'onglet @@ -153,14 +162,14 @@ class JuryPE(object): self.add_file_to_zip( zipfile, - f"resultats_semestres_taggues_{self.diplome}.xlsx", + f"ResSemBUTTags_{self.diplome}.xlsx", output.read(), path="details", ) def _gen_rcss(self): """Génère les RCS (attribut `rcss_jury`), combinaisons de semestres suivis par les étudiants au sens - d'un nom de RCS (par ex: '3S'). + d'un nom de RCS (par ex: 'S2' ou '3S'). """ pe_affichage.pe_print( "*** Génère les RCS (différentes combinaisons de semestres) des étudiants" @@ -168,24 +177,35 @@ class JuryPE(object): self.rcss_jury = pe_rcs.RCSsJuryPE(self.diplome) self.rcss_jury.cree_rcss(self.etudiants) - def _gen_xls_semestres_taggues(self, zipfile: ZipFile): + def _gen_xls_sxtags(self, zipfile: ZipFile): """Génère les semestres taggués en s'appuyant sur les RCS de type Sx (pour identifier les redoublements impactant les semestres taggués). """ # Génère les moyennes des RCS de type Sx - pe_affichage.pe_print("*** Calcule les moyennes de semestres = RCS de type Sx") + pe_affichage.pe_print("*** Calcule les moyennes des SxTag") - self.sems_tags = {} - for rcs_id, rcs in self.rcss_jury.rcss.items(): - if rcs.nom.startswith("S"): - self.sems_tags[rcs_id] = pe_semtag.SemTag(rcs, self.res_sems_tags) + # Les regroupements de Sx + self.regroupements = {} + for rang in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT+1): + self.regroupements[rang] = {} + for etudid in self.etudiants.etudiants_ids: + trajectoire = self.etudiants.cursus[etudid][f"S{rang}"] + self.regroupements[rang] |= trajectoire + + # Les SxTag + self.sxtags = {} + for rang in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT+1): + trajectoire = self.regroupements[rang] + sxtag_id = pe_sxtag.get_sxtag_from_semestres(trajectoire) + ressemstags = {fid: self.ressembuttags[fid] for fid in trajectoire} + self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, ressemtags) # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: - for sem_tag in self.sems_tags.values(): + for sem_tag in self.sxtags.values(): onglet = sem_tag.get_repr(verbose=False) df = sem_tag.df_moyennes_et_classements() # écriture dans l'onglet @@ -200,11 +220,11 @@ class JuryPE(object): ) def _gen_xls_rcss_tags(self, zipfile: ZipFile): - """Génère les RCS tagguées des RCS, en calculant les moyennes et les classements par tag - pour chacune. + """Génère les RCS taggués (autres que ceux de type Sx), etc... + en calculant les moyennes et les classements par tag pour chaque RCS. Stocke le résultat dans self.rccs_tag, un dictionnaire de - la forme ``{nom_aggregat: {fid_terminal: SetTag(fid_terminal)} }`` + la forme ``{nom_aggregat: {fid_terminal: RCSTag(fid_terminal)} }`` Pour rappel : Chaque RCS est identifié par un nom d'aggrégat et par un formsemestre terminal. @@ -213,19 +233,13 @@ class JuryPE(object): * 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) - - """ # Génère les moyennes des RCS de type Sx - pe_affichage.pe_print("*** Calcule les moyennes des RCS de type Sx") + pe_affichage.pe_print("*** Calcule les moyennes des RCS") self.rcss_tags = {} for rcs_id, rcs in self.rcss_jury.rcss.items(): @@ -409,7 +423,7 @@ class JuryPE(object): for etudid in etudids: trajectoire = self.rcss_jury.suivi[etudid][aggregat] if trajectoire: - tid = trajectoire.rcs_id + tid = trajectoire.sxtag_id trajectoire_tagguee = self.rcss_tags[tid] if ( tag in trajectoire_tagguee.moyennes_tags @@ -555,7 +569,7 @@ class JuryPE(object): # La trajectoire de l'étudiant sur l'aggrégat trajectoire = self.rcss_jury.suivi[etudid][aggregat] if trajectoire: - trajectoire_tagguee = self.rcss_tags[trajectoire.rcs_id] + trajectoire_tagguee = self.rcss_tags[trajectoire.sxtag_id] if tag in trajectoire_tagguee.moyennes_tags: # L'interclassement interclass = self.interclassements_taggues[aggregat] @@ -581,9 +595,8 @@ class JuryPE(object): def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict: """Ayant connaissance des étudiants dont il faut calculer les moyennes pour - le jury PE (attribut `self.etudiant_ids) et de leur cursus (semestres - parcourus), - renvoie un dictionnaire ``{fid: FormSemestre(fid)}`` + le jury PE (attribut `self.etudiant_ids) et de leurs trajectoires (semestres + parcourus), renvoie un dictionnaire ``{fid: FormSemestre(fid)}`` contenant l'ensemble des formsemestres de leurs cursus, dont il faudra calculer la moyenne. @@ -592,44 +605,17 @@ def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict: Returns: Un dictionnaire de la forme `{fid: FormSemestre(fid)}` - """ semestres = {} for etudid in etudiants.etudiants_ids: - for cle in etudiants.cursus[etudid]: + for cle in etudiants.trajectoires[etudid]: if cle.startswith("S"): - semestres = semestres | etudiants.cursus[etudid][cle] + semestres = semestres | etudiants.trajectoires[etudid][cle] return semestres -def compute_resultats_semestres_tag(etudiants: EtudiantsJuryPE) -> dict: - """Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés. - Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire - des étudiants (cf. attribut etudiants.cursus). - En crééant le semestre taggué, sont calculées les moyennes/classements par tag associé. - . - Args: - etudiants: Un groupe d'étudiants participant au jury - Returns: - Un dictionnaire {fid: SemestreTag(fid)} - """ - - # Création des semestres taggués, de type 'S1', 'S2', ... - pe_affichage.pe_print("*** Création des semestres taggués") - - formsemestres = get_formsemestres_etudiants(etudiants) - - semestres_tags = {} - for frmsem_id, formsemestre in formsemestres.items(): - # Crée le semestre_tag et exécute les calculs de moyennes - formsemestretag = ResSemTag(frmsem_id) - - # Stocke le semestre taggué - semestres_tags[frmsem_id] = formsemestretag - - return semestres_tags def compute_interclassements( diff --git a/app/pe/pe_rcs.py b/app/pe/pe_rcs.py index ecd5d764..136109e4 100644 --- a/app/pe/pe_rcs.py +++ b/app/pe/pe_rcs.py @@ -12,9 +12,9 @@ Created on 01-2024 import app.pe.pe_comp as pe_comp from app.models import FormSemestre +from app.pe import pe_sxtag from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date - TYPES_RCS = { "S1": { "aggregat": ["S1"], @@ -81,11 +81,11 @@ TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")] class RCS: - """Modélise un ensemble de semestres d'étudiants + """Modélise un ensemble de semestres finals d'étudiants associé à un type de regroupement cohérent de semestres donné (par ex: 'S2', '3S', '2A'). - Si le RCS est un semestre de type Si, stocke le (ou les) + Si le RCS est un semestre de type Si, stocke le formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si (en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants) @@ -104,38 +104,40 @@ class RCS: """ def __init__(self, nom_rcs: str, semestre_final: FormSemestre): - self.nom = nom_rcs + self.nom: str = nom_rcs """Nom du RCS""" - self.formsemestre_final = semestre_final + self.formsemestre_final: FormSemestre = semestre_final """FormSemestre terminal du RCS""" - self.rcs_id = (nom_rcs, semestre_final.formsemestre_id) + self.rcs_id: (str, int) = (nom_rcs, semestre_final.formsemestre_id) """Identifiant du RCS sous forme (nom_rcs, id du semestre_terminal)""" - self.semestres_aggreges = {} - """Semestres regroupés dans le RCS""" + # self.semestres_aggreges: dict[int:FormSemestre] = {} + # """Semestres regroupés dans le RCS""" + + self.sxtags_aggreges: dict[(str, int): pe_sxtag.SxTag] = {} + """Les SxTag aggrégés""" 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]): - """Ajout de semestres aux semestres à regrouper + def add_sxtags_a_aggreger(self, sxtags: dict[(str,int): pe_sxtag.SxTag]): + """Ajout des SxTag aux semestres à regrouper Args: - semestres: Dictionnaire ``{fid: FormSemestre(fid)}`` à ajouter + sxtags: Dictionnaire ``{(str,fid): SxTag}`` à ajouter """ - self.semestres_aggreges = self.semestres_aggreges | semestres + self.sxtags_aggreges = self.sxtags_aggreges | sxtags def get_repr(self, verbose=True) -> str: """Représentation textuelle d'un RCS - basé sur ses semestres aggrégés""" + basé sur ses sxtags aggrégés""" noms = [] - for fid in self.semestres_aggreges: - semestre = self.semestres_aggreges[fid] - noms.append(f"S{semestre.semestre_id}({fid})") + for sxtag_id, sxtag in self.sxtags_aggreges.items(): + noms.append(f"S{sxtag.semestre_id}") noms = sorted(noms) title = f"""{self.nom} ({ self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}""" @@ -145,7 +147,7 @@ class RCS: class RCSsJuryPE: - """Classe centralisant toutes les regroupements cohérents de + """Classe centralisant tous les regroupements cohérents de semestres (RCS) des étudiants à prendre en compte dans un jury PE Args: @@ -163,21 +165,24 @@ class RCSsJuryPE: """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, son RCS : {etudid: {nom_RCS: RCS}}""" - def cree_rcss(self, etudiants: EtudiantsJuryPE): + def cree_rcss(self, etudiants: EtudiantsJuryPE, sxtags: dict[(str, int), pe_sxtag.SxTag]): """Créé tous les RCS, au regard du cursus des étudiants - analysés + les mémorise dans les données de l'étudiant + analysés et des SxTag calculés. + + Les mémorise dans les données de chaque étudiant. Args: etudiants: Les étudiants à prendre en compte dans le Jury PE + pe_sxtag: Les Sx taggués """ for nom_rcs in pe_comp.TOUS_LES_SEMESTRES + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM: # 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 = TYPES_RCS[nom_rcs]["aggregat"] - nom_semestre_terminal = noms_semestre_de_aggregat[-1] + noms_semestre_de_aggregat = TYPES_RCS[nom_rcs]["aggregat"] # ["S1", "S2", "S3"] + nom_semestre_terminal = noms_semestre_de_aggregat[-1] # "S3" - for etudid in etudiants.cursus: + for etudid in etudiants.trajectoires: if etudid not in self.suivi: self.suivi[etudid] = { aggregat: None @@ -188,54 +193,57 @@ class RCSsJuryPE: # Le formsemestre terminal (dernier en date) associé au # semestre marquant la fin de l'aggrégat # (par ex: son dernier S3 en date) - semestres = etudiants.cursus[etudid][nom_semestre_terminal] + semestres = etudiants.trajectoires[etudid][nom_semestre_terminal] if semestres: formsemestre_final = get_dernier_semestre_en_date(semestres) # Ajout ou récupération de la trajectoire - trajectoire_id = (nom_rcs, formsemestre_final.formsemestre_id) - if trajectoire_id not in self.rcss: - trajectoire = RCS(nom_rcs, formsemestre_final) - self.rcss[trajectoire_id] = trajectoire + rcs_id = (nom_rcs, formsemestre_final.formsemestre_id) + if rcs_id not in self.rcss: + rcs = RCS(nom_rcs, formsemestre_final) + self.rcss[rcs_id] = rcs else: - trajectoire = self.rcss[trajectoire_id] + rcs = self.rcss[rcs_id] # La liste des semestres de l'étudiant à prendre en compte # pour cette trajectoire - semestres_a_aggreger = get_rcs_etudiant( - etudiants.cursus[etudid], formsemestre_final, nom_rcs + semestres_a_aggreger = get_trajectoire_etudiant( + etudiants.trajectoires[etudid], formsemestre_final, nom_rcs ) + # Extrait les sxtags correspondants aux semestres à aggréger + # (par ex. des 2 semestres S1(18)+S1(26) récupère le sxtag S1(26) + sxtags_a_aggreger = {} + semestres_tries = pe_comp.tri_semestres_par_rang(semestres_a_aggreger) + for rang in semestres_tries: + sems = semestres_tries[rang] # les 1 ou 2 semestres de même rang suivi + sxtag_id = pe_sxtag.get_sxtag_from_semestres(sems, sxtags) + if not sxtag_id: + raise ValueError(f"Il manque un sxtag pour {sems}") + sxtags_a_aggreger[sxtag_id] = sxtags[sxtag_id] + # Ajout des semestres à la trajectoire - trajectoire.add_semestres_a_aggreger(semestres_a_aggreger) + rcs.add_sxtags_a_aggreger(sxtags_a_aggreger) # Mémoire la trajectoire suivie par l'étudiant - self.suivi[etudid][nom_rcs] = trajectoire + self.suivi[etudid][nom_rcs] = rcs -def get_rcs_etudiant( +def get_trajectoire_etudiant( semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str ) -> dict[int, FormSemestre]: - """Ensemble des semestres parcourus par un étudiant, connaissant - les semestres de son cursus, - dans le cadre du RCS visé et ayant pour semestre terminal `formsemestre_final`. + """Ensemble des semestres parcourus (trajectoire) + par un étudiant dans le cadre + d'un RCS de type Sx, iA ou iS et ayant pour semestre terminal `formsemestre_final`. - Si le RCS 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 le RCS 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é). + Par ex: pour un RCS "3S", dont le formsemestre_terminal est un S3, regroupe + le ou les S1 qu'il a suivi (1 ou 2 si redoublement) + le ou les S2 + le ou les S3. Les semestres parcourus sont antérieurs (en terme de date de fin) au formsemestre_terminal. Args: - cursus: Dictionnaire {fid: FormSemestre(fid)} donnant l'ensemble des semestres + cursus: Dictionnaire {fid: Formsemestre} donnant l'ensemble des semestres dans lesquels l'étudiant a été inscrit formsemestre_final: le semestre final visé nom_rcs: Nom du RCS visé diff --git a/app/pe/pe_rcstag.py b/app/pe/pe_rcstag.py index ef458589..5af9ee04 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/pe_rcstag.py @@ -38,7 +38,7 @@ Created on Fri Sep 9 09:15:05 2016 from app.comp.res_sem import load_formsemestre_results from app.pe import pe_affichage -from app.pe.pe_ressemtag import ResSemTag +from app.pe.pe_ressemtag import ResSemBUTTag import pandas as pd import numpy as np from app.pe.pe_rcs import RCS @@ -48,26 +48,25 @@ from app.pe.pe_moytag import MoyennesTag class RCSTag(TableTag): + def __init__( - self, rcs: RCS, semestres_taggues: dict[int, ResSemTag] + self, rcs: RCS, semestres_taggues: dict[int, ResSemBUTTag] ): """Calcule les moyennes par tag d'une combinaison de semestres (RCS), 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. - Args: 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) - - self.rcs_id = rcs.rcs_id + self.rcs_id: tuple(str, int) = rcs.rcs_id """Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)""" - self.rcs = rcs + self.rcs: RCS = rcs """RCS associé au RCS taggué""" self.nom = self.get_repr() @@ -82,20 +81,21 @@ class RCSTag(TableTag): self.semestres_aggreges = rcs.semestres_aggreges """Les semestres aggrégés""" - self.semestres_tags_aggreges = {} + self.res_sems_tags = {} """Les semestres tags associés aux semestres aggrégés""" - for frmsem_id in self.semestres_aggreges: - try: - self.semestres_tags_aggreges[frmsem_id] = semestres_taggues[frmsem_id] - except: - raise ValueError("Semestres taggués manquants") + try: + for frmsem_id in self.semestres_aggreges: + self.res_sems_tags[frmsem_id] = semestres_taggues[frmsem_id] + except: + raise ValueError("Semestres taggués manquants") - """Les étudiants (état civil + cursus connu)""" - self.etuds = nt.etuds + # Les étudiants (etuds, états civils & etudis) + self.add_etuds(nt.etuds) - # assert self.etuds == trajectoire.suivi # manque-t-il des étudiants ? - self.etats_civils = {etud.etudid: etud.nomprenom for etud in self.etuds} + # Les compétences (extraites des ues de tous les semestres) + self.ues = self.comp_ues(tag="but") + # Les tags self.tags_sorted = self.do_taglist() """Tags extraits de tous les semestres""" @@ -114,7 +114,7 @@ class RCSTag(TableTag): def __eq__(self, other): """Egalité de 2 RCS taggués sur la base de leur identifiant""" - return self.rcs_id == other.rcs_id + return self.rcs_id == other.sxtag_id def get_repr(self, verbose=False) -> str: """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle @@ -132,7 +132,7 @@ class RCSTag(TableTag): # Index du cube (etudids -> dim 0, tags -> dim 1) etudids = [etud.etudid for etud in self.etuds] tags = self.tags_sorted - semestres_id = list(self.semestres_tags_aggreges.keys()) + semestres_id = list(self.res_sems_tags.keys()) dfs = {} @@ -141,7 +141,7 @@ class RCSTag(TableTag): df = pd.DataFrame(np.nan, index=etudids, columns=tags) # Charge les notes du semestre tag - notes = self.semestres_tags_aggreges[frmsem_id].notes + notes = self.res_sems_tags[frmsem_id].notes # Les étudiants & les tags commun au dataframe final et aux notes du semestre) etudids_communs = df.index.intersection(notes.index) @@ -172,8 +172,8 @@ class RCSTag(TableTag): Une liste de tags triés par ordre alphabétique """ tags = [] - for frmsem_id in self.semestres_tags_aggreges: - tags.extend(self.semestres_tags_aggreges[frmsem_id].tags_sorted) + for frmsem_id in self.res_sems_tags: + tags.extend(self.res_sems_tags[frmsem_id].tags_sorted) pe_affichage.pe_print(f"* Tags : {', '.join(tags)}") return sorted(set(tags)) diff --git a/app/pe/pe_ressemtag.py b/app/pe/pe_ressemtag.py index 5dff02ea..8274e02d 100644 --- a/app/pe/pe_ressemtag.py +++ b/app/pe/pe_ressemtag.py @@ -37,37 +37,38 @@ Created on Fri Sep 9 09:15:05 2016 """ import pandas as pd -import app.pe.pe_etudiant from app import db, ScoValueError from app import comp +from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_sem import load_formsemestre_results from app.models import FormSemestre 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 +import app.pe.pe_tabletags as pe_tabletags 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): +class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): """ - Un ResSemTag représente les résultats des étudiants à un semestre, en donnant + Un ResSemBUTTag 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): + def __init__(self, formsemestre: FormSemestre): """ Args: - formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base + formsemestre: le ``FormSemestre`` sur lequel il se base """ - TableTag.__init__(self) + ResultatsSemestreBUT.__init__(self, formsemestre) + pe_tabletags.TableTag.__init__(self) # Le semestre - self.formsemestre_id = formsemestre_id - self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + # self.formsemestre_id = self.formsemestre.formsemestre_id + # self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id) # Le nom du res_semestre taggué self.nom = self.get_repr(verbose=True) @@ -75,25 +76,14 @@ class ResSemTag(TableTag): 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) + # self.nt = load_formsemestre_results(self.formsemestre) - # Les étudiants (etuds, états civils & etudis) - self.add_etuds(self.nt.etuds) - - # 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 étudiants (etuds, états civils & etudis) ajouté + self.add_etuds(self.etuds) # 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 + self.ues_inscr_parcours_df = self.load_ues_inscr_parcours() # Les tags personnalisés et auto: tags_dict = self._get_tags_dict() @@ -124,7 +114,7 @@ class ResSemTag(TableTag): # 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}, + {ue.id: self.etud_moy_ue[ue.id] for ue in ues_hors_sport}, index=self.etudids, ) # moy_ues = self.nt.etud_moy_ue[ue_id] @@ -150,7 +140,7 @@ class ResSemTag(TableTag): def get_repr(self, verbose=False): """Nom affiché pour le semestre taggué""" if verbose: - return f"{self.formsemestre} ({self.formsemestre_id})" + return f"{self.formsemestre} ({self.formsemestre.formsemestre_id})" else: return pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True) @@ -209,7 +199,7 @@ class ResSemTag(TableTag): dict_tags = {"personnalises": dict(), "auto": dict()} # Les tags perso dict_tags["personnalises"] = get_synthese_tags_personnalises_semestre( - self.nt.formsemestre + self.formsemestre ) noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys()))) pe_affichage.pe_print( @@ -256,26 +246,6 @@ class ResSemTag(TableTag): 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 & diff --git a/app/pe/pe_semtag.py b/app/pe/pe_sxtag.py similarity index 58% rename from app/pe/pe_semtag.py rename to app/pe/pe_sxtag.py index d98cd084..1dd1960f 100644 --- a/app/pe/pe_semtag.py +++ b/app/pe/pe_sxtag.py @@ -37,87 +37,79 @@ Created on Fri Sep 9 09:15:05 2016 """ from app.comp.res_sem import load_formsemestre_results -from app.models import UniteEns +from app.models import UniteEns, FormSemestre from app.pe import pe_affichage -from app.pe.pe_ressemtag import ResSemTag +from app.pe.pe_ressemtag import ResSemBUTTag import pandas as pd import numpy as np -from app.pe.pe_rcs import RCS from app.pe.pe_tabletags import TableTag from app.pe.pe_moytag import MoyennesTag -class SemTag(TableTag): - def __init__(self, rcs: RCS, res_sems_tags: dict[int, ResSemTag]): - """Calcule les moyennes/classements par tag à un RCS d'un seul semestre - (ici semestre) de type 'Sx' (par ex. 'S1', 'S2', ...) : +class SxTag(TableTag): + def __init__(self, sxtag_id: (int, int), ressembuttags: dict[int, ResSemBUTTag]): + """Calcule les moyennes/classements par tag d'un semestre de type 'Sx' + (par ex. 'S1', 'S2', ...) : * 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) + dans les (2) 'Sx' qu'il a suivi - Les **tags considérés** sont uniquement ceux du dernier semestre du RCS + Un SxTag peut donc regrouper plusieurs semestres. + + Un SxTag est identifié par un tuple (x, fid) où x est le numéro (semestre_id) + du semestre et fid le formsemestre_id du semestre final (le plus récent) du + regrouprement. + + Les **tags**, les **UE** et les inscriptions aux UEs (pour les etudiants) + considérés sont uniquement ceux du semestre final. Args: - rcs: Un RCS (identifié par un nom et l'id de son semestre terminal) - res_sems_tags: Les données sur les résultats des semestres taggués + sxtag_id: L'identifiant de SxTag + ressembuttags: Un dictionnaire de la forme `{fid: ResSemBUTTag(fid)}` donnant + les semestres à regrouper et les résultats/moyennes par tag des + semestres """ TableTag.__init__(self) - self.rcs_id = rcs.rcs_id - """Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)""" + assert sxtag_id and len(sxtag_id) == 2 and sxtag_id in ressembuttags - self.rcs = rcs - """RCS associé au RCS taggué""" + self.sxtag_id: (int, int) = sxtag_id + """Identifiant du SxTag de la forme (semestre_id, fid_semestre_final)""" - 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() - """Représentation textuelle du RCS taggué""" + self.ressembuttags = ressembuttags + """Les ResSemBUTTags à regrouper dans le SxTag""" - # Les données du formsemestre_terminal - self.formsemestre_terminal = rcs.formsemestre_final - """Le formsemestre terminal""" - - # Les résultats du formsemestre terminal - nt = load_formsemestre_results(self.formsemestre_terminal) - - self.semestres_aggreges = rcs.semestres_aggreges - """Les semestres aggrégés""" - - self.res_sems_tags = {} - """Les résultats des semestres taggués (limités aux semestres aggrégés)""" - try: - for frmsem_id in self.semestres_aggreges: - self.res_sems_tags[frmsem_id] = res_sems_tags[frmsem_id] - except: - raise ValueError("Résultats des semestres taggués manquants") + # Les données du semestre final + self.fid_final = sxtag_id[1] + self.ressembuttag_final = ressembuttags[self.fid_final] + """Le ResSemBUTTag final""" # Les étudiants (etuds, états civils & etudis) - self.add_etuds(nt.etuds) + self.etuds = ressembuttags[self.fid_final].etuds + self.add_etuds(self.etuds) # Les tags - self.tags_sorted = self.comp_tags_list() - """Tags (extraits uniquement du semestre terminal de l'aggrégat)""" + self.tags_sorted = self.ressembuttag_final.tags_sorted + """Tags (extraits uniquement du semestre final)""" + pe_affichage.pe_print(f"* Tags : {', '.join(self.tags_sorted)}") - # Les UEs - self.ues = self.comp_ues(tag="but") + # Les UE + self.ues = self.ressembuttag_final.moyennes_tags["but"].ues + + # Les acronymes des UE self.acronymes_ues_sorted = sorted([ue.acronyme for ue in self.ues.values()]) - """UEs extraites du semestre terminal de l'aggrégat (avec - check de concordance sur les UE des semestres_aggrégés)""" - # Les inscriptions aux UEs - self.ues_inscr_parcours_df = self.comp_ues_inscr_parcours(tag="but") - """Les inscriptions aux UEs (extraites uniquement du semestre terminal)""" - - self.moyennes_tags: dict[str, MoyennesTag] = {} - """Moyennes/classements par tag (qu'ils soient personnalisés ou automatiques)""" + # Les inscriptions des étudiants aux UEs + # => ne conserve que les UEs du semestre final (pour les redoublants) + self.ues_inscr_parcours_df = self.ressembuttag_final.moyennes_tags["but"].ues_inscr_parcours_df + # Les moyennes par tag self.moyennes_tags: dict[str, pd.DataFrame] = {} """Les notes aux UEs dans différents tags""" + # Masque des inscriptions inscr_mask = self.ues_inscr_parcours_df.to_numpy() for tag in self.tags_sorted: @@ -132,19 +124,19 @@ class SemTag(TableTag): inscr_mask, ) # Les moyennes - self.moyennes_tags[tag] = MoyennesTag(tag, - self.ues, - moys_ues, - self.ues_inscr_parcours_df) + self.moyennes_tags[tag] = MoyennesTag( + tag, self.ues, moys_ues, self.ues_inscr_parcours_df + ) def __eq__(self, other): - """Egalité de 2 RCS taggués sur la base de leur identifiant""" - return self.rcs_id == other.rcs_id + """Egalité de 2 SxTag sur la base de leur identifiant""" + return self.sxtag_id == other.sxtag_id def get_repr(self, verbose=False) -> str: """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle est basée)""" - return self.rcs.get_repr(verbose=verbose) + affichage = str(fid) for fid in self.res + return f"S{sxtag_id[0]}Tag ({'+'.join()})" def compute_notes_ues_cube(self, tag, acronymes_ues_sorted): """Construit le cube de notes des UEs (etudid x accronyme_ue x semestre_aggregé) @@ -153,7 +145,7 @@ class SemTag(TableTag): # 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.res_sems_tags.keys()) + semestres_id = list(self.ressembuttags.keys()) dfs = {} @@ -162,7 +154,7 @@ class SemTag(TableTag): df = pd.DataFrame(np.nan, index=etudids, columns=acronymes_ues_sorted) # Charge les notes du semestre tag - sem_tag = self.res_sems_tags[frmsem_id] + sem_tag = self.ressembuttags[frmsem_id] moys_tag = sem_tag.moyennes_tags[tag] notes = moys_tag.notes_ues # dataframe etudids x ues acronymes_ues_sem = list( @@ -192,43 +184,6 @@ class SemTag(TableTag): 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) -> list[str]: - """Récupère les tag du semestre taggué associé au semestre final du RCS - - Returns: - Une liste de tags triés par ordre alphabétique - """ - tags = [] - dernier_frmid = self.formsemestre_terminal.formsemestre_id - dernier_semestre_tag = self.res_sems_tags[dernier_frmid] - tags = dernier_semestre_tag.tags_sorted - pe_affichage.pe_print(f"* Tags : {', '.join(tags)}") - return tags - - def comp_ues(self, tag="but") -> dict[int, UniteEns]: - """Récupère les UEs à aggréger, en s'appuyant sur la moyenne générale - (tag but) du semestre final du RCS - - Returns: - Un dictionnaire donnant les UEs - """ - dernier_frmid = self.formsemestre_terminal.formsemestre_id - dernier_semestre_tag = self.res_sems_tags[dernier_frmid] - moy_tag = dernier_semestre_tag.moyennes_tags[tag] - return moy_tag.ues # les UEs - - def comp_ues_inscr_parcours(self, tag="but") -> pd.DataFrame: - """Récupère les informations d'inscription des étudiants aux UEs : ne - conserve que les UEs du semestre terminal (pour les redoublants) - - Returns: - Un dataFrame etudids x UE indiquant si un étudiant est inscrit à une UE - """ - dernier_frmid = self.formsemestre_terminal.formsemestre_id - dernier_semestre_tag = self.res_sems_tags[dernier_frmid] - moy_tag = dernier_semestre_tag.moyennes_tags[tag] - return moy_tag.ues_inscr_parcours_df - def compute_notes_ues( set_cube: np.array, @@ -261,8 +216,8 @@ def compute_notes_ues( mask = ~np.isnan(set_cube) # Entrées à garder dans le cube en fonction du mask d'inscription - inscr_mask_3D = np.stack([inscr_mask]*nb_semestres, axis=-1) - set_cube = set_cube*inscr_mask_3D + inscr_mask_3D = np.stack([inscr_mask] * nb_semestres, axis=-1) + set_cube = set_cube * inscr_mask_3D # Enlève les NaN du cube pour les entrées manquantes : NaN -> -1.0 set_cube_no_nan = np.nan_to_num(set_cube, nan=-1.0) @@ -284,3 +239,16 @@ def compute_notes_ues( etud_moy_tag_df.fillna(np.nan) return etud_moy_tag_df + + +def get_sxtag_from_semestres( + formsemestres: dict[int:FormSemestre], sxtags: dict[(str, int):SxTag] +) -> (str, int): + """Partant d'un dictionnaire de SxTags, renvoie l'identifiant (str, fid) du + sxtag correspondant aux semestres de formsemestres. + (Utilisé pour transformer une trajectoire d'étudiants (par ex. S1+S2+S1+S2+S3) + en une suite de sxtags (S1+S2+S3) + """ + for sxtag_id, sxtag in sxtags: + if set(formsemestres.keys()) == set(sxtag.semestres_aggreges.keys()): + return sxtag_id diff --git a/app/pe/pe_tabletags.py b/app/pe/pe_tabletags.py index f876dedb..596aeae4 100644 --- a/app/pe/pe_tabletags.py +++ b/app/pe/pe_tabletags.py @@ -50,11 +50,11 @@ class TableTag(object): SemestreTag, TrajectoireTag, AggregatInterclassTag """ # Les étudiants - self.etuds: list[Identite] = None # A venir + # self.etuds: list[Identite] = None # A venir """Les étudiants""" - self.etats_civils: dict[int, Identite] = None + # self.etats_civils: dict[int, Identite] = None """Les états civils""" - self.etudids: list[int] = None + # self.etudids: list[int] = None """Les etudids""" def add_etuds(self, etuds: list[Identite]): @@ -63,7 +63,7 @@ class TableTag(object): Args: etuds: la liste des identités de l'étudiant """ - self.etuds = etuds + # self.etuds = etuds self.etats_civils = {etud.etudid: etud.etat_civil for etud in self.etuds} self.etudids = list(self.etats_civils.keys()) From 5e49384a909be8d5cb428838975fc885ca5775a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Sat, 17 Feb 2024 02:35:58 +0100 Subject: [PATCH 08/41] =?UTF-8?q?Am=C3=A9liorations=20diverses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_jury.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index e63cd227..878c0261 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -198,7 +198,7 @@ class JuryPE(object): trajectoire = self.regroupements[rang] sxtag_id = pe_sxtag.get_sxtag_from_semestres(trajectoire) ressemstags = {fid: self.ressembuttags[fid] for fid in trajectoire} - self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, ressemtags) + self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, ressemstags) # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() From 1716daafde9a35f0c285b4819f80250be2f472f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Sat, 17 Feb 2024 03:30:19 +0100 Subject: [PATCH 09/41] =?UTF-8?q?Am=C3=A9liorations=20diverses=20(suite)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_etudiant.py | 2 +- app/pe/pe_jury.py | 54 ++++++++++++++++++++++++------------------- app/pe/pe_sxtag.py | 25 +++++++------------- 3 files changed, 39 insertions(+), 42 deletions(-) diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 9851e313..f608976e 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -687,7 +687,7 @@ def convert_trajectoire_to_sxtag_id(trajectoire: dict[int:FormSemestre]) -> (int if not trajectoire: return None rangs = [formsemestre.semestre_id for formsemestre in trajectoire.values()] - assert rangs == min(rangs), "Les trajectoires doivent être de même rang" + assert len(set(rangs)) == 1, "Les trajectoires doivent être de même rang" rang = min(rangs) fid_terminal = list(trajectoire.values())[0].formsemestre_id for fid, formsemestre in trajectoire.items(): diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 878c0261..c0736271 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -53,11 +53,12 @@ import pandas as pd from app.pe import pe_sxtag from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE import app.pe.pe_affichage as pe_affichage -from app.pe.pe_etudiant import * # TODO A éviter -> pe_etudiant. +import app.pe.pe_etudiant as pe_etudiant import app.pe.pe_rcs as pe_rcs from app.pe.pe_rcstag import RCSTag from app.pe.pe_ressemtag import ResSemBUTTag from app.pe.pe_interclasstag import RCSInterclasseTag +import app.pe.pe_comp as pe_comp class JuryPE(object): @@ -87,7 +88,9 @@ class JuryPE(object): f"""*** Recherche et chargement des étudiants diplômés en { self.diplome}""" ) - self.etudiants = EtudiantsJuryPE(self.diplome) # Les infos sur les étudiants + self.etudiants = pe_etudiant.EtudiantsJuryPE( + self.diplome + ) # Les infos sur les étudiants self.etudiants.find_etudiants() self.diplomes_ids = self.etudiants.diplomes_ids @@ -100,7 +103,7 @@ class JuryPE(object): self._gen_xls_ressembuttags(zipfile) self._gen_xls_sxtags(zipfile) - self._gen_xls_rcss_tags(zipfile) + # self._gen_xls_rcss_tags(zipfile) # self._gen_xls_interclassements_rcss(zipfile) # self._gen_xls_synthese_jury_par_tag(zipfile) # self._gen_xls_synthese_par_etudiant(zipfile) @@ -136,17 +139,19 @@ class JuryPE(object): def _gen_xls_ressembuttags(self, zipfile: ZipFile): """Calcule les moyennes par tag des résultats des Semestres BUT""" - pe_affichage.pe_print("*** Génère les ResSemBUTTag (résultats des semestres BUT taggués)") + pe_affichage.pe_print( + "*** Génère les ResSemBUTTag (résultats des semestres BUT taggués)" + ) formsemestres = get_formsemestres_etudiants(self.etudiants) - pe_affichage.pe_print(f" --> {len(formsemestres)} résultats de semestres à considérer") + pe_affichage.pe_print( + f" --> {len(formsemestres)} résultats de semestres à considérer" + ) - ressembuttags = {} + self.ressembuttags = {} for frmsem_id, formsemestre in formsemestres.items(): # Crée le semestre_tag et exécute les calculs de moyennes - ressembuttags[frmsem_id] = ResSemBUTTag(formsemestre) - - return ressembuttags + self.ressembuttags[frmsem_id] = ResSemBUTTag(formsemestre) # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() @@ -186,24 +191,29 @@ class JuryPE(object): # Les regroupements de Sx self.regroupements = {} - for rang in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT+1): + for rang in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1): self.regroupements[rang] = {} for etudid in self.etudiants.etudiants_ids: - trajectoire = self.etudiants.cursus[etudid][f"S{rang}"] - self.regroupements[rang] |= trajectoire + trajectoire = self.etudiants.trajectoires[etudid][f"S{rang}"] + if trajectoire: + sxtag_id = pe_etudiant.convert_trajectoire_to_sxtag_id(trajectoire) + if sxtag_id not in self.regroupements[rang]: + self.regroupements[rang][sxtag_id] = {} + else: + self.regroupements[rang][sxtag_id] |= trajectoire # Les SxTag self.sxtags = {} - for rang in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT+1): - trajectoire = self.regroupements[rang] - sxtag_id = pe_sxtag.get_sxtag_from_semestres(trajectoire) - ressemstags = {fid: self.ressembuttags[fid] for fid in trajectoire} - self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, ressemstags) + for rang in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1): + for sxtag_id in self.regroupements[rang]: + trajectoires = self.regroupements[rang][sxtag_id] + ressemstags = {fid: self.ressembuttags[fid] for fid in trajectoires} + self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, ressemstags) # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated - output, engine="openpyxl" + output, engine="openpyxl" ) as writer: for sem_tag in self.sxtags.values(): onglet = sem_tag.get_repr(verbose=False) @@ -593,7 +603,7 @@ class JuryPE(object): return df -def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict: +def get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict: """Ayant connaissance des étudiants dont il faut calculer les moyennes pour le jury PE (attribut `self.etudiant_ids) et de leurs trajectoires (semestres parcourus), renvoie un dictionnaire ``{fid: FormSemestre(fid)}`` @@ -614,12 +624,8 @@ def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict: return semestres - - - - def compute_interclassements( - etudiants: EtudiantsJuryPE, + etudiants: pe_etudiant.EtudiantsJuryPE, trajectoires_jury_pe: pe_rcs.RCSsJuryPE, trajectoires_tagguees: dict[tuple, pe_rcs.RCS], ): diff --git a/app/pe/pe_sxtag.py b/app/pe/pe_sxtag.py index 1dd1960f..71363028 100644 --- a/app/pe/pe_sxtag.py +++ b/app/pe/pe_sxtag.py @@ -74,7 +74,7 @@ class SxTag(TableTag): """ TableTag.__init__(self) - assert sxtag_id and len(sxtag_id) == 2 and sxtag_id in ressembuttags + assert sxtag_id and len(sxtag_id) == 2 and sxtag_id[1] in ressembuttags self.sxtag_id: (int, int) = sxtag_id """Identifiant du SxTag de la forme (semestre_id, fid_semestre_final)""" @@ -91,6 +91,8 @@ class SxTag(TableTag): self.etuds = ressembuttags[self.fid_final].etuds self.add_etuds(self.etuds) + pe_affichage.pe_print(f"--> {self.get_repr()}") + # Les tags self.tags_sorted = self.ressembuttag_final.tags_sorted """Tags (extraits uniquement du semestre final)""" @@ -104,7 +106,9 @@ class SxTag(TableTag): # Les inscriptions des étudiants aux UEs # => ne conserve que les UEs du semestre final (pour les redoublants) - self.ues_inscr_parcours_df = self.ressembuttag_final.moyennes_tags["but"].ues_inscr_parcours_df + self.ues_inscr_parcours_df = self.ressembuttag_final.moyennes_tags[ + "but" + ].ues_inscr_parcours_df # Les moyennes par tag self.moyennes_tags: dict[str, pd.DataFrame] = {} @@ -135,8 +139,8 @@ class SxTag(TableTag): def get_repr(self, verbose=False) -> str: """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle est basée)""" - affichage = str(fid) for fid in self.res - return f"S{sxtag_id[0]}Tag ({'+'.join()})" + affichage = [str(fid) for fid in self.ressembuttags] + return f"S{self.sxtag_id[0]}Tag ({'+'.join(affichage)})" def compute_notes_ues_cube(self, tag, acronymes_ues_sorted): """Construit le cube de notes des UEs (etudid x accronyme_ue x semestre_aggregé) @@ -239,16 +243,3 @@ def compute_notes_ues( etud_moy_tag_df.fillna(np.nan) return etud_moy_tag_df - - -def get_sxtag_from_semestres( - formsemestres: dict[int:FormSemestre], sxtags: dict[(str, int):SxTag] -) -> (str, int): - """Partant d'un dictionnaire de SxTags, renvoie l'identifiant (str, fid) du - sxtag correspondant aux semestres de formsemestres. - (Utilisé pour transformer une trajectoire d'étudiants (par ex. S1+S2+S1+S2+S3) - en une suite de sxtags (S1+S2+S3) - """ - for sxtag_id, sxtag in sxtags: - if set(formsemestres.keys()) == set(sxtag.semestres_aggreges.keys()): - return sxtag_id From 68bd20f8de55e75f72efe5f2bc442594144fc998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Sun, 18 Feb 2024 19:24:03 +0100 Subject: [PATCH 10/41] =?UTF-8?q?Mise=20en=20place=20des=20RCRCF=20+=20de?= =?UTF-8?q?=20l'agr=C3=A9gation=20des=20coeff=20pour=20les=20moyennes=20de?= =?UTF-8?q?=20RCSTag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_etudiant.py | 15 -- app/pe/pe_interclasstag.py | 2 +- app/pe/pe_jury.py | 76 +++++---- app/pe/pe_moytag.py | 89 ++++------ app/pe/pe_rcs.py | 338 +++++++++++++++++++++++++++---------- app/pe/pe_rcstag.py | 190 +++++++++++++-------- app/pe/pe_ressemtag.py | 56 +++--- app/pe/pe_sxtag.py | 44 +++-- app/pe/pe_tabletags.py | 6 +- 9 files changed, 501 insertions(+), 315 deletions(-) diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index f608976e..35e9f942 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -680,18 +680,3 @@ def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str: return " ".join(description) - -def convert_trajectoire_to_sxtag_id(trajectoire: dict[int:FormSemestre]) -> (int, int): - """Partant d'une trajectoire (dictionnaire de la forme {fid: FormSemestre}), - renvoie l'identifiant (rang_sem, fid du semestre_terminal) associé""" - if not trajectoire: - return None - rangs = [formsemestre.semestre_id for formsemestre in trajectoire.values()] - assert len(set(rangs)) == 1, "Les trajectoires doivent être de même rang" - rang = min(rangs) - fid_terminal = list(trajectoire.values())[0].formsemestre_id - for fid, formsemestre in trajectoire.items(): - if trajectoire[fid_terminal].date_fin <= formsemestre.date_fin: - fid_terminal = fid - return (rang, fid_terminal) - diff --git a/app/pe/pe_interclasstag.py b/app/pe/pe_interclasstag.py index 000feacf..b015136b 100644 --- a/app/pe/pe_interclasstag.py +++ b/app/pe/pe_interclasstag.py @@ -98,7 +98,7 @@ class RCSInterclasseTag(TableTag): """Association entre chaque étudiant et la trajectoire tagguée à prendre en compte pour l'aggrégat""" for etudid in self.diplomes_ids: - self.suivi[etudid] = rcss_jury_pe.suivi[etudid][nom_rcs] + self.suivi[etudid] = rcss_jury_pe.rcss_suivis[etudid][nom_rcs] self.tags_sorted = self.do_taglist() """Liste des tags (triés par ordre alphabétique)""" diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index c0736271..f61b601e 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -94,6 +94,9 @@ class JuryPE(object): self.etudiants.find_etudiants() self.diplomes_ids = self.etudiants.diplomes_ids + self.rcss_jury = pe_rcs.RCSsJuryPE(self.diplome) + """Les informations sur les regroupements de semestres""" + self.zipdata = io.BytesIO() with ZipFile(self.zipdata, "w") as zipfile: if not self.diplomes_ids: @@ -102,8 +105,10 @@ class JuryPE(object): self._gen_xls_diplomes(zipfile) self._gen_xls_ressembuttags(zipfile) + self._gen_rcss() self._gen_xls_sxtags(zipfile) - # self._gen_xls_rcss_tags(zipfile) + # self._gen_rcrcfs() + # self._gen_xls_rcss_tags(zipfile) # self._gen_xls_interclassements_rcss(zipfile) # self._gen_xls_synthese_jury_par_tag(zipfile) # self._gen_xls_synthese_par_etudiant(zipfile) @@ -173,51 +178,46 @@ class JuryPE(object): ) def _gen_rcss(self): - """Génère les RCS (attribut `rcss_jury`), combinaisons de semestres suivis par les étudiants au sens - d'un nom de RCS (par ex: 'S2' ou '3S'). + """Génère les RCS (attribut `rcss_jury`), combinaisons de semestres + suivis par les étudiants au sens d'un nom de RCS (par ex: 'S2' ou '3S'). """ pe_affichage.pe_print( "*** Génère les RCS (différentes combinaisons de semestres) des étudiants" ) - self.rcss_jury = pe_rcs.RCSsJuryPE(self.diplome) + self.rcss_jury.cree_rcss(self.etudiants) + self.rcss_jury.cree_rcfs(self.etudiants) def _gen_xls_sxtags(self, zipfile: ZipFile): """Génère les semestres taggués en s'appuyant sur les RCS de type Sx (pour identifier les redoublements impactant les semestres taggués). """ + # Génère les regroupements de semestres de type Sx + pe_affichage.pe_print("*** Génère les RCF (RCS de type Sx)***") + self.rcss_jury.cree_rcfs(self.etudiants) + # Génère les moyennes des RCS de type Sx pe_affichage.pe_print("*** Calcule les moyennes des SxTag") - # Les regroupements de Sx - self.regroupements = {} - for rang in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1): - self.regroupements[rang] = {} - for etudid in self.etudiants.etudiants_ids: - trajectoire = self.etudiants.trajectoires[etudid][f"S{rang}"] - if trajectoire: - sxtag_id = pe_etudiant.convert_trajectoire_to_sxtag_id(trajectoire) - if sxtag_id not in self.regroupements[rang]: - self.regroupements[rang][sxtag_id] = {} - else: - self.regroupements[rang][sxtag_id] |= trajectoire - - # Les SxTag + # Les SxTag (moyenne de Sx par UE) self.sxtags = {} - for rang in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1): - for sxtag_id in self.regroupements[rang]: - trajectoires = self.regroupements[rang][sxtag_id] - ressemstags = {fid: self.ressembuttags[fid] for fid in trajectoires} - self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, ressemstags) + for rcf_id, rcf in self.rcss_jury.rcfs.items(): + # SxTag traduisant le RCF + sxtag_id = rcf_id + # Les resultats des semestres taggués à prendre en compte dans le RCF + ressemstags = { + fid: self.ressembuttags[fid] for fid in rcf.semestres_aggreges + } + self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, ressemstags) # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: - for sem_tag in self.sxtags.values(): - onglet = sem_tag.get_repr(verbose=False) - df = sem_tag.df_moyennes_et_classements() + for sxtag in self.sxtags.values(): + onglet = sxtag.get_repr(verbose=False) + df = sxtag.df_moyennes_et_classements() # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) output.seek(0) @@ -229,9 +229,16 @@ class JuryPE(object): path="details", ) - def _gen_xls_rcss_tags(self, zipfile: ZipFile): - """Génère les RCS taggués (autres que ceux de type Sx), etc... - en calculant les moyennes et les classements par tag pour chaque RCS. + def _gen_rcrcfs(self): + """Génère les regroupements cohérents de RCFs qu'ont suivi chaque étudiant""" + + pe_affichage.pe_print("*** Génère les RCRCF (regroupements de RCF de type Sx) amenant du S1 à un semestre final***") + self.rcss_jury.cree_rcrcfs(self.etudiants) + + + def _gen_xls_rcrcss_tags(self, zipfile: ZipFile): + """Génère les RCS taggués traduisant les moyennes (orientées compétences) + de regroupements de semestre de type Sx, xA ou xS. Stocke le résultat dans self.rccs_tag, un dictionnaire de la forme ``{nom_aggregat: {fid_terminal: RCSTag(fid_terminal)} }`` @@ -249,12 +256,11 @@ class JuryPE(object): """ # Génère les moyennes des RCS de type Sx - pe_affichage.pe_print("*** Calcule les moyennes des RCS") + pe_affichage.pe_print("*** Calcule les moyennes des RC de RCFS") 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) + for rcs_id, rcrcf in self.rcss_jury.rcrcfs.items(): + self.rcss_tags[rcs_id] = RCSTag(rcrcf, self.sxtags) # Intègre le bilan des trajectoires tagguées au zip final output = io.BytesIO() @@ -431,7 +437,7 @@ class JuryPE(object): # considéré trajectoires_tagguees = [] for etudid in etudids: - trajectoire = self.rcss_jury.suivi[etudid][aggregat] + trajectoire = self.rcss_jury.rcss_suivis[etudid][aggregat] if trajectoire: tid = trajectoire.sxtag_id trajectoire_tagguee = self.rcss_tags[tid] @@ -577,7 +583,7 @@ class JuryPE(object): ) # La trajectoire de l'étudiant sur l'aggrégat - trajectoire = self.rcss_jury.suivi[etudid][aggregat] + trajectoire = self.rcss_jury.rcss_suivis[etudid][aggregat] if trajectoire: trajectoire_tagguee = self.rcss_tags[trajectoire.sxtag_id] if tag in trajectoire_tagguee.moyennes_tags: diff --git a/app/pe/pe_moytag.py b/app/pe/pe_moytag.py index a2428f34..05d3882b 100644 --- a/app/pe/pe_moytag.py +++ b/app/pe/pe_moytag.py @@ -155,101 +155,74 @@ class Moyenne: return self.synthese["nb_inscrits"] > 0 + class MoyennesTag: def __init__( self, tag: str, - ues: dict[int, UniteEns], - notes_ues: pd.DataFrame, - ues_inscr_parcours_df: pd.DataFrame - # notes_gen: pd.Series, + matrice_notes: pd.DataFrame, # etudids x colonnes + matrice_coeffs: pd.DataFrame # etudids x colonnes ): """Classe centralisant la synthèse des moyennes/classements d'une série d'étudiants à un tag donné, en différenciant les notes obtenues aux UE et au général (toutes UEs confondues) + Args: tag: Un tag - 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 - ues_inscr_parcours_df: Les inscriptions des etudid au UE + matrice_notes: Les moyennes (etudid x acronymes_ues ou etudid x compétences) aux différentes UEs ou compétences + matrice_coeffs: Les coeff à appliquer pour le calcul de la moyenne générale # notes_gen: Une série de notes (moyenne) sous forme d'un pd.Series() (toutes UEs confondues) """ self.tag = tag """Le tag associé aux moyennes""" - # Les UE - self.ues: dict[int, UniteEns] = ues - """Les UEs sur lesquelles sont calculées les moyennes""" - - colonnes = list(notes_ues.columns) - acronymes: list[str] = [self.ues[ue_id].acronyme for ue_id in self.ues] - assert len(set(acronymes)) == len( - colonnes - ), "Deux UEs ne peuvent pas avoir le même acronyme" - - # Les inscriptions des etudids aux UEs - self.ues_inscr_parcours_df: pd.DataFrame = ues_inscr_parcours_df - """Les inscriptions des etudids au UE en fonction de leur parcours""" - - # Les coefficients à appliquer aux UEs pour la moyenne générale = ECTS - self.ects = self.ues_inscr_parcours_df.fillna(0.0) * [ - ue.ects - for ue in self.ues.values() # if ue.type != UE_SPORT <= déjà supprimé - ] - # Les profils d'ects (pour debug) - profils_ects = [] - for val in list(self.ects.values): - if tuple(val) not in profils_ects: - profils_ects.append(tuple(val)) - # Les moyennes par UE - self.notes_ues: pd.DataFrame = 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] = {} + self.matrice_notes: pd.DataFrame = matrice_notes + """Les notes aux UEs ou aux compétences (DataFrame)""" + + self.matrice_coeffs: pd.DataFrame = matrice_coeffs + """Les coeffs à appliquer pour le calcul des moyennes générales + (toutes UE ou compétences confondues). NaN si étudiant non inscrit""" + + self.moyennes: 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) + colonnes = self.matrice_notes.columns + for col in colonnes: # if ue.type != UE_SPORT: + notes = matrice_notes[col] + self.moyennes[col] = Moyenne(notes) # Les moyennes générales - notes_gen = self.compute_moy_gen(self.notes_ues, self.ects) + notes_gen = self.compute_moy_gen(self.matrice_notes, self.matrice_coeffs) self.notes_gen = notes_gen """Les notes générales (moyenne toutes UEs confonudes)""" - self.moy_gen = Moyenne(notes_gen) + self.moyenne_gen = Moyenne(notes_gen) """Le dataframe retraçant les moyennes/classements/statistiques général""" - pe_affichage.pe_print(f"> MoyTag pour {tag} avec") - pe_affichage.pe_print(f" - ues={acronymes}") - pe_affichage.pe_print(f" - ects={profils_ects}") - def __eq__(self, other): - """Egalité de deux MoyenneTag lorsque leur tag sont identiques""" - return self.tag == other.tag - - def compute_moy_gen( - self, moy_ues: pd.DataFrame, coeff_ues: pd.DataFrame - ) -> pd.Series: - """Calcule la moyenne générale (toutes UE confondus) + def compute_moy_gen(self, moys: pd.DataFrame, coeffs: pd.DataFrame) -> pd.Series: + """Calcule la moyenne générale (toutes UE/compétences confondus) pour le tag considéré, en pondérant les notes obtenues au UE - par les crédits ECTS. + par les coeff (généralement les crédits ECTS). Args: - moy_ues: Les moyennes etudids x acronymes_ues - coeff_ues: Les coeff etudids x ueids + moys: Les moyennes etudids x acronymes_ues/compétences + coeff: Les coeff etudids x ueids/compétences """ # Calcule la moyenne générale dans le semestre (pondérée par le ECTS) try: moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects( - moy_ues, - coeff_ues, + moys, + coeffs.fillna(0.0), # formation_id=self.formsemestre.formation_id, skip_empty_ues=True, ) except TypeError as e: - raise TypeError("Pb dans le calcul de la moyenne toutes UEs confondues") + raise TypeError( + "Pb dans le calcul de la moyenne toutes UEs/compétences confondues" + ) return moy_gen_tag + diff --git a/app/pe/pe_rcs.py b/app/pe/pe_rcs.py index 136109e4..ab245a05 100644 --- a/app/pe/pe_rcs.py +++ b/app/pe/pe_rcs.py @@ -12,7 +12,7 @@ Created on 01-2024 import app.pe.pe_comp as pe_comp from app.models import FormSemestre -from app.pe import pe_sxtag +from app.pe import pe_sxtag, pe_affichage from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date TYPES_RCS = { @@ -80,28 +80,14 @@ TOUS_LES_RCS = list(TYPES_RCS.keys()) TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")] +def get_descr_rcs(nom_rcs: str) -> str: + """Renvoie la description pour les tableurs de synthèse + Excel d'un nom de RCS""" + return TYPES_RCS[nom_rcs]["descr"] + + class RCS: - """Modélise un ensemble de semestres finals d'étudiants - associé à un type de regroupement cohérent de semestres - donné (par ex: 'S2', '3S', '2A'). - - Si le RCS est un semestre de type Si, stocke le - formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si - (en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants) - - Pour le RCS de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie - les semestres que les étudiants ont suivis pour les amener jusqu'au semestre - terminal de la trajectoire (par ex: ici un S3). - - Ces semestres peuvent être : - - * des S1+S2+S1+S2+S3 si redoublement de la 1ère année - * des S1+S2+(année de césure)+S3 si césure, ... - - Args: - nom_rcs: Un nom du RCS (par ex: '5S') - semestre_final: Le semestre final du RCS - """ + """Modélise un regroupement cohérent de semestres (formsemestre ou de Sx)""" def __init__(self, nom_rcs: str, semestre_final: FormSemestre): self.nom: str = nom_rcs @@ -110,42 +96,155 @@ class RCS: self.formsemestre_final: FormSemestre = semestre_final """FormSemestre terminal du RCS""" + self.rang_final = self.formsemestre_final.semestre_id + """Le rang du formsemestre final""" + self.rcs_id: (str, int) = (nom_rcs, semestre_final.formsemestre_id) """Identifiant du RCS sous forme (nom_rcs, id du semestre_terminal)""" - # self.semestres_aggreges: dict[int:FormSemestre] = {} - # """Semestres regroupés dans le RCS""" + def get_formsemestre_id_final(self) -> int: + """Renvoie l'identifiant du formsemestre final du RCS - self.sxtags_aggreges: dict[(str, int): pe_sxtag.SxTag] = {} - """Les SxTag aggrégés""" - - def get_formsemestre_id_final(self): - """Renvoie l'identifiant du formsemestre final du RCS""" + Returns: + L'id du formsemestre final (marquant la fin) du RCS + """ return self.formsemestre_final.formsemestre_id - def add_sxtags_a_aggreger(self, sxtags: dict[(str,int): pe_sxtag.SxTag]): - """Ajout des SxTag aux semestres à regrouper + def __repr__(self): + """Représentation textuelle d'un RCS""" + return f"{self.nom} ({self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}" + + def get_repr(self): + return self.__repr__() + + def __eq(self, other): + """Egalité de RCS""" + return ( + self.nom == other.nom + and self.formsemestre_final == other.formsemestre_final + ) + + +class RCF(RCS): + """Modélise un ensemble de (form)semestres d'étudiants + associé à un type de regroupement cohérent de semestres + donné (par ex: 'S2', '3S', '2A'). + + Si le RCF est un semestre de type Si, stocke les + formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si + (en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants) + + Pour le RCF de type iS ou iA (par ex, 3A=S1+S2+S3), identifie + les semestres que les étudiants ont suivis pour les amener jusqu'au semestre + terminal du RCS (par ex: ici un S3). + + Ces semestres peuvent être : + + * des S1+S2+S1+S2+S3 si redoublement de la 1ère année + * des S1+S2+(année de césure)+S3 si césure, ... + + Args: + nom_rcs: Un nom du RCS (par ex: '5S') + semestre_final: Le formsemestre final du RCS + """ + + def __init__(self, nom_rcs: str, semestre_final: FormSemestre): + RCS.__init__(self, nom_rcs, semestre_final) + + self.semestres_aggreges: dict[int:FormSemestre] = {} + """Formsemestres regroupés dans le RCS""" + + def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]): + """Ajout de semestres aux semestres à regrouper Args: - sxtags: Dictionnaire ``{(str,fid): SxTag}`` à ajouter + semestres: Dictionnaire ``{fid: Formsemestre)`` """ - self.sxtags_aggreges = self.sxtags_aggreges | sxtags + self.semestres_aggreges = self.semestres_aggreges | semestres def get_repr(self, verbose=True) -> str: """Représentation textuelle d'un RCS - basé sur ses sxtags aggrégés""" + basé sur ses semestres aggrégés""" noms = [] - for sxtag_id, sxtag in self.sxtags_aggreges.items(): - noms.append(f"S{sxtag.semestre_id}") + for fid in self.semestres_aggreges: + semestre = self.semestres_aggreges[fid] + noms.append(f"S{semestre.semestre_id}({fid})") noms = sorted(noms) - title = f"""{self.nom} ({ - self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}""" + title = f"""{str(self)}""" if verbose and noms: title += " - " + "+".join(noms) return title + +def get_rcf_from_semestres_aggreges( + rcfs: dict[(str, int):RCF], semestres_a_aggreges: list[FormSemestre] + ) -> (str, int): + """Partant d'un dictionnaire de RCFs (de la forme + ``{ (nom_rcs, fid): RCF }, et connaissant une liste + de (form)semestres à aggréger, renvoie l'identifiant + (nom_rcs, fid) du RCFs qui lui correspond (c'est à dire celui dont + les semestres_aggregés par le RCF sont les même que les + semestres_a_aggreger. + + Returns: + rcf_id: L'identifiant du RCF trouvé + """ + fids_semestres_a_aggreger = set( + [frms.formsemestre_id for frms in semestres_a_aggreges] + ) + for rcf_id, rcf in rcfs.items(): + fids_rcf = set(rcf.semestres_aggreges) + if fids_rcf == fids_semestres_a_aggreger: + return rcf_id + return None + + +class RCRCF: + """Modélise les RCF d'étudiants suivis par un étudiant dans + le cadre d'un RCS donné (par ex: 3S=S1+S2+S3). + + Pour rappel : un RCF (par ex. S1) combine les semestres 1 qu'a suivi + l'étudiant pour valider son S1 (1 si étudiant standard, 2 si redoublant). + + Le RCRCF 3S est donc le regroupement du RCF S1 + RCF S2 + RCF S3. + + Il est identifié par le formsemestre de S3 marquant la fin du regroupement. + + + Args: + nom_rcs: Un nom du RCS (par ex: '5S') + semestre_final: Le semestre final du RCS + """ + + def __init__(self, nom_rcs: str, semestre_final: FormSemestre): + RCS.__init__(self, nom_rcs, semestre_final) + + self.rcfs_aggreges: dict[(str, int) : pe_sxtag.SxTag] = {} + """Les RCFs à aggréger""" + + def add_rcfs_a_aggreger(self, rcfs: dict[(str, int):RCF]): + """Ajout des RCFs aux RCFS à regrouper + + Args: + rcfs: Dictionnaire ``{(str,fid): RCF}`` à ajouter + """ + self.rcfs_aggreges = self.rcfs_aggreges | rcfs + + def get_repr(self, verbose=True) -> str: + """Représentation textuelle d'un RCSF + basé sur ses RCF aggrégés""" + + noms = [] + for rcf_id, rcf in self.rcfs_aggreges.items(): + noms.append(rcf.get_repr()) + title = f"""{str(self)}""" + if verbose and noms: + title += " : " + "+".join(noms) + return title + + class RCSsJuryPE: """Classe centralisant tous les regroupements cohérents de semestres (RCS) des étudiants à prendre en compte dans un jury PE @@ -158,78 +257,151 @@ class RCSsJuryPE: self.annee_diplome = annee_diplome """Année de diplômation""" - self.rcss: dict[tuple:RCS] = {} - """Ensemble des RCS recensés : {(nom_RCS, fid_terminal): RCS}""" + self.rcss: dict[tuple(int, str) : RCF] = {} + """Ensemble des RCS recensés""" - self.suivi: dict[int:str] = {} + self.rcss_suivis: dict[int:dict] = {} """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, - son RCS : {etudid: {nom_RCS: RCS}}""" + son RCS : {etudid: {nom_RCS: RCS}}""" - def cree_rcss(self, etudiants: EtudiantsJuryPE, sxtags: dict[(str, int), pe_sxtag.SxTag]): + self.rcfs: dict[tuple(int, str) : RCF] = {} + """Ensemble des RCF recensés : {(nom_RCS, fid_terminal): RCF}""" + + self.rcfs_suivis: dict[int:dict] = {} + """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, + son RCS : {etudid: {nom_RCS: RCF}}""" + + self.rcrcfs: dict[tuple(int, str) : RCRCF] = {} + """Ensemble des RCS recensés : {(nom_RCS, fid_terminal): RCRCF}""" + + self.rcrcfs_suivis: dict[int:str] = {} + """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, + son RCRCF : {etudid: {nom_RCS: RCSx}}""" + + def cree_rcss(self, etudiants: EtudiantsJuryPE): """Créé tous les RCS, au regard du cursus des étudiants - analysés et des SxTag calculés. - - Les mémorise dans les données de chaque étudiant. + analysés + les mémorise dans les données de l'étudiant Args: etudiants: Les étudiants à prendre en compte dans le Jury PE - pe_sxtag: Les Sx taggués """ + for etudid in etudiants.trajectoires: + self.rcss_suivis[etudid] = { + aggregat: None + for aggregat in pe_comp.TOUS_LES_SEMESTRES + + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM + } + for nom_rcs in pe_comp.TOUS_LES_SEMESTRES + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM: # 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 = TYPES_RCS[nom_rcs]["aggregat"] # ["S1", "S2", "S3"] - nom_semestre_terminal = noms_semestre_de_aggregat[-1] # "S3" + noms_semestre_de_aggregat = TYPES_RCS[nom_rcs]["aggregat"] + nom_semestre_terminal = noms_semestre_de_aggregat[-1] for etudid in etudiants.trajectoires: - if etudid not in self.suivi: - self.suivi[etudid] = { - aggregat: None - for aggregat in pe_comp.TOUS_LES_SEMESTRES - + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM - } - # Le formsemestre terminal (dernier en date) associé au # semestre marquant la fin de l'aggrégat # (par ex: son dernier S3 en date) - semestres = etudiants.trajectoires[etudid][nom_semestre_terminal] - if semestres: - formsemestre_final = get_dernier_semestre_en_date(semestres) + trajectoire = etudiants.trajectoires[etudid][nom_semestre_terminal] + if trajectoire: + formsemestre_final = get_dernier_semestre_en_date(trajectoire) - # Ajout ou récupération de la trajectoire + # Ajout ou récupération du RCS associé rcs_id = (nom_rcs, formsemestre_final.formsemestre_id) if rcs_id not in self.rcss: - rcs = RCS(nom_rcs, formsemestre_final) - self.rcss[rcs_id] = rcs - else: - rcs = self.rcss[rcs_id] + self.rcss[rcs_id] = RCF(nom_rcs, formsemestre_final) + rcs = self.rcss[rcs_id] # La liste des semestres de l'étudiant à prendre en compte # pour cette trajectoire - semestres_a_aggreger = get_trajectoire_etudiant( + semestres_a_aggreger = get_rcs_etudiant( etudiants.trajectoires[etudid], formsemestre_final, nom_rcs ) - # Extrait les sxtags correspondants aux semestres à aggréger - # (par ex. des 2 semestres S1(18)+S1(26) récupère le sxtag S1(26) - sxtags_a_aggreger = {} - semestres_tries = pe_comp.tri_semestres_par_rang(semestres_a_aggreger) - for rang in semestres_tries: - sems = semestres_tries[rang] # les 1 ou 2 semestres de même rang suivi - sxtag_id = pe_sxtag.get_sxtag_from_semestres(sems, sxtags) - if not sxtag_id: - raise ValueError(f"Il manque un sxtag pour {sems}") - sxtags_a_aggreger[sxtag_id] = sxtags[sxtag_id] + # Ajout des semestres au RCS + rcs.add_semestres_a_aggreger(semestres_a_aggreger) - # Ajout des semestres à la trajectoire - rcs.add_sxtags_a_aggreger(sxtags_a_aggreger) + # Mémorise le RCS suivi par l'étudiant + self.rcss_suivis[etudid][nom_rcs] = rcs - # Mémoire la trajectoire suivie par l'étudiant - self.suivi[etudid][nom_rcs] = rcs + # Affichage pour debug + jeunes = list(enumerate(self.rcss_suivis)) + for no_etud, etudid in jeunes[:20]: + pe_affichage.pe_print(f"-> {etudiants.identites[etudid].nomprenom} :") + for nom_rcs, rcs in self.rcss_suivis[etudid].items(): + if rcs: + pe_affichage.pe_print(f" > RCS {nom_rcs}: {rcs.get_repr()}") + + def cree_rcfs(self, etudiants: EtudiantsJuryPE): + """Créé les RCFs en ne conservant dans les RCS que les regroupements + de type Sx""" + self.rcfs = {} + for rcs_id, rcs in self.rcss.items(): + if rcs and rcs.nom in pe_comp.TOUS_LES_SEMESTRES: + self.rcfs[rcs_id] = rcs + print(self.rcfs) + + for etudid in self.rcss_suivis: + for nom_rcs, rcs in self.rcss_suivis[etudid].items(): + if rcs and nom_rcs in pe_comp.TOUS_LES_SEMESTRES: + if etudid not in self.rcfs_suivis: + self.rcfs_suivis[etudid] = {} + self.rcfs_suivis[etudid][nom_rcs] = rcs + + # Affichage pour debug + jeunes = list(enumerate(self.rcfs_suivis)) + for no_etud, etudid in jeunes[:20]: + pe_affichage.pe_print(f"-> {etudiants.identites[etudid].nomprenom} :") + for nom_rcs, rcs in self.rcfs_suivis[etudid].items(): + if rcs: + pe_affichage.pe_print(f" > RCF {nom_rcs}: {rcs.get_repr()}") + else: + pe_affichage.pe_print(f" > RCF {nom_rcs}: !!! ") + + def cree_rcrcfs(self, etudiants: EtudiantsJuryPE): + """Créé tous les RCRCF, au regard du cursus des étudiants + analysés (trajectoires traduisant son parcours dans les + différents semestres) + les mémorise dans les données de l'étudiant + """ + + # Pour tous les étudiants du jury + for etudid in self.rcss_suivis: + self.rcrcfs_suivis[etudid] = {} + + for rcf_id, rcf in self.rcfs_suivis[etudid].items(): # Pour chaque RCS + semestres_a_aggreger = rcf.semestres_aggreges + + # Tri des semestres par rang + semestres_tries = pe_comp.tri_semestres_par_rang(semestres_a_aggreger) + + # Récupére les RCFs de type Sx traduisant sa trajectoire + rcfs_a_aggreger = {} + for semestres_du_rang in semestres_tries.values(): + if semestres_du_rang: + rcf_id = get_rcf_from_semestres_aggreges( + self.rcfs, semestres_du_rang + ) + if rcf_id: + raise ValueError( + "Il manque un RCF pour créer les RCRCFs dans cree_rcrcfs" + ) + rcfs_a_aggreger[rcf_id] = self.rcfs[rcf_id] + + # Ajout du RCRCF + if rcf_id not in self.rcrcfs: + self.rcrfs[rcf_id] = RCRCF(rcf_id, rcf.formsemestre_final) + rcrcf = self.rcrcfs[rcf_id] + + # Ajout des RCFs au RCRCF + rcrcf.add_rcfs_a_aggreger(rcfs_a_aggreger) + + # Mémoire la trajectoire RCRCF suivie par l'étudiant + nom_rcs = rcrcf.nom + self.rcrcfs_suivis[etudid][nom_rcs] = rcrcf -def get_trajectoire_etudiant( +def get_rcs_etudiant( semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str ) -> dict[int, FormSemestre]: """Ensemble des semestres parcourus (trajectoire) @@ -273,9 +445,3 @@ def get_trajectoire_etudiant( ): semestres_aggreges[fid] = semestre return semestres_aggreges - - -def get_descr_rcs(nom_rcs: str) -> str: - """Renvoie la description pour les tableurs de synthèse - Excel d'un nom de RCS""" - return TYPES_RCS[nom_rcs]["descr"] diff --git a/app/pe/pe_rcstag.py b/app/pe/pe_rcstag.py index 5af9ee04..9284ef6a 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/pe_rcstag.py @@ -41,76 +41,81 @@ from app.pe import pe_affichage from app.pe.pe_ressemtag import ResSemBUTTag import pandas as pd import numpy as np -from app.pe.pe_rcs import RCS +from app.pe.pe_rcs import RCS, RCRCF +from app.pe.pe_sxtag import SxTag from app.pe.pe_tabletags import TableTag from app.pe.pe_moytag import MoyennesTag class RCSTag(TableTag): - - def __init__( - self, rcs: RCS, semestres_taggues: dict[int, ResSemBUTTag] - ): - """Calcule les moyennes par tag d'une combinaison de semestres - (RCS), pour extraire les classements par tag pour un + def __init__(self, rcrcf: RCS, sxstags: dict[(str, int): SxTag]): + """Calcule les moyennes par tag (orientées compétences) + d'un regroupement de SxTag + (RCRCF), 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. + participé au même semestre terminal. Args: rcs: Un RCS (identifié par un nom et l'id de son semestre terminal) - semestres_taggues: Les données sur les semestres taggués + sxstags: Les données sur les RCF taggués """ TableTag.__init__(self) - self.rcs_id: tuple(str, int) = rcs.rcs_id + self.rcs_id: tuple(str, int) = rcrcf.rcs_id """Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)""" - self.rcs: RCS = rcs - """RCS associé au RCS taggué""" + self.rcrcf: RCRCF = rcrcf + """RCRCF associé au RCS taggué""" self.nom = self.get_repr() """Représentation textuelle du RCS taggué""" - self.formsemestre_terminal = rcs.formsemestre_final + self.formsemestre_terminal = rcrcf.formsemestre_final """Le formsemestre terminal""" # Les résultats du formsemestre terminal nt = load_formsemestre_results(self.formsemestre_terminal) - self.semestres_aggreges = rcs.semestres_aggreges - """Les semestres aggrégés""" + self.rcfs_aggreges = rcrcf.rcfs_aggreges + """Les RCFs aggrégés""" - self.res_sems_tags = {} - """Les semestres tags associés aux semestres aggrégés""" + self.sxstags = {} + """Les SxTag associés aux RCF aggrégés""" try: - for frmsem_id in self.semestres_aggreges: - self.res_sems_tags[frmsem_id] = semestres_taggues[frmsem_id] + for rcf_id in self.rcfs_aggreges: + self.sxstags[rcf_id] = sxstags[rcf_id] except: - raise ValueError("Semestres taggués manquants") + raise ValueError("Semestres SxTag manquants") # Les étudiants (etuds, états civils & etudis) + self.etuds = nt.etuds self.add_etuds(nt.etuds) - # Les compétences (extraites des ues de tous les semestres) - self.ues = self.comp_ues(tag="but") + # Les compétences (extraites de tous les Sxtags) + self.competences_sorted = self.do_complist() + """Compétences extraites de tous les SxTag aggrégés""" # Les tags self.tags_sorted = self.do_taglist() - """Tags extraits de tous les semestres""" - - self.notes_cube = self.compute_notes_cube() - """Cube de notes""" - - etudids = list(self.etats_civils.keys()) - self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted) - """Calcul les moyennes par tag sous forme d'un dataframe""" + """Tags extraits de tous les SxTag aggrégés""" + # Les moyennes self.moyennes_tags: dict[str, MoyennesTag] = {} """Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)""" + for tag in self.tags_sorted: - moy_gen_tag = self.notes[tag] - self.moyennes_tags[tag] = MoyennesTag(tag, moy_gen_tag) + # Cube de note + notes_cube, coeffs_cube = self.compute_notes_comps_cube(tag) + + # Calcule des moyennes/coeffs sous forme d'un dataframe""" + moys_competences, coeffs_competences = compute_notes_competences( + notes_cube, coeffs_cube, self.etudids, self.competences_sorted + ) + + # Les moyennes + self.moyennes_tags[tag] = MoyennesTag(tag, moys_competences, + coeffs_competences) def __eq__(self, other): """Egalité de 2 RCS taggués sur la base de leur identifiant""" @@ -119,11 +124,14 @@ class RCSTag(TableTag): def get_repr(self, verbose=False) -> str: """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle est basée)""" - return self.rcs.get_repr(verbose=verbose) + return self.rcrcf.get_repr(verbose=verbose) - def compute_notes_cube(self): - """Construit le cube de notes (etudid x tags x semestre_aggregé) - nécessaire au calcul des moyennes de l'aggrégat + def compute_notes_comps_cube(self, tag): + """Pour un tag donné, construit : + * le cube de notes (etudid x competences x SxTag) nécessaire au calcul des moyennes, + en remplaçant les données d'UE (obtenus du SxTag) par les compétences + * le cube de coeffs (etudid x competences x SxTag) (traduisant les inscriptions) + appliqué au calcul des différents SxTag """ # nb_tags = len(self.tags_sorted) # nb_etudiants = len(self.etuds) @@ -131,55 +139,90 @@ class RCSTag(TableTag): # Index du cube (etudids -> dim 0, tags -> dim 1) etudids = [etud.etudid for etud in self.etuds] - tags = self.tags_sorted - semestres_id = list(self.res_sems_tags.keys()) + competences_sorted = self.competences_sorted + sxstags_ids = list(self.sxstags.keys()) - dfs = {} + notes_dfs = {} + coeffs_dfs = {} - for frmsem_id in semestres_id: + for sxtag_id, sxtag in sxstags_ids.item(): # Partant d'un dataframe vierge - df = pd.DataFrame(np.nan, index=etudids, columns=tags) + notes_df = pd.DataFrame(np.nan, index=etudids, columns=competences_sorted) + coeffs_df = pd.DataFrame(np.nan, index=etudids, columns=competences_sorted) - # Charge les notes du semestre tag - notes = self.res_sems_tags[frmsem_id].notes + moys_tag = sxtag.moyennes_tags[tag] - # Les étudiants & les tags commun au dataframe final et aux notes du semestre) - etudids_communs = df.index.intersection(notes.index) - tags_communs = df.columns.intersection(notes.columns) + # Charge les notes et les coeffs du semestre tag + notes = moys_tag.matrice_notes.copy() # avec une copie + coeffs = moys_tag.matrice_coeffs.copy() # les coeffs - # Injecte les notes par tag - df.loc[etudids_communs, tags_communs] = notes.loc[ - etudids_communs, tags_communs + # Traduction des UE en compétences + association_ues_comp = moys_tag.competences + ues_columns_df = notes.columns + comp_associes_aux_ues = [association_ues_comp[ue] for ue in ues_columns_df] + notes.columns = comp_associes_aux_ues + coeffs.columns = comp_associes_aux_ues + + # Compétences communes + comp_communes = list(set(competences_sorted) & set(comp_associes_aux_ues)) + + # Etudiants communs + etudids_communs = notes_df.index.intersection(notes.index) + + # Recopie des notes et des coeffs + notes_df.loc[etudids_communs, comp_communes] = notes.loc[ + etudids_communs, comp_communes + ] + coeffs_df.loc[etudids_communs, comp_communes] = coeffs.loc[ + etudids_communs, comp_communes ] # Supprime tout ce qui n'est pas numérique - for col in df.columns: - df[col] = pd.to_numeric(df[col], errors="coerce") + for col in notes_df.columns: + notes_df[col] = pd.to_numeric(notes_df[col], errors="coerce") - # Stocke le df - dfs[frmsem_id] = df + # Stocke les dfs + notes_dfs[sxtag_id] = notes_df + coeffs_dfs[sxtag_id] = coeffs_df - """Réunit les notes sous forme d'un cube etdids x tags x semestres""" - semestres_x_etudids_x_tags = [dfs[fid].values for fid in dfs] - etudids_x_tags_x_semestres = np.stack(semestres_x_etudids_x_tags, axis=-1) + """Réunit les notes sous forme d'un cube etudids x competences x semestres""" + sxtag_x_etudids_x_comps = [notes_dfs[fid].values for fid in notes_dfs] + notes_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1) - return etudids_x_tags_x_semestres + """Réunit les coeffs sous forme d'un cube etudids x competences x semestres""" + sxtag_x_etudids_x_comps = [coeffs_dfs[fid].values for fid in notes_dfs] + coeffs_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1) + + return notes_etudids_x_comps_x_sxtag, coeffs_etudids_x_comps_x_sxtag def do_taglist(self): - """Synthétise les tags à partir des semestres (taggués) aggrégés + """Synthétise les tags à partir des Sxtags aggrégés Returns: Une liste de tags triés par ordre alphabétique """ tags = [] - for frmsem_id in self.res_sems_tags: - tags.extend(self.res_sems_tags[frmsem_id].tags_sorted) + for frmsem_id in self.sxstags: + tags.extend(self.sxstags[frmsem_id].tags_sorted) pe_affichage.pe_print(f"* Tags : {', '.join(tags)}") return sorted(set(tags)) + def do_complist(self): + """Synthétise les compétences à partir des Sxtags aggrégés""" + competences = [] + for sxtag_id, sxtag in self.sxstags: + comp = sxtag.moyennes_tags["but"].competences + competences.extend(comp) + return sorted(set(competences)) + + +def compute_notes_competences( + set_cube: np.array, coeff_cube: np.array, etudids: list, competences: list +): + """Calcule: + * la moyenne par compétences à un tag donné sur plusieurs semestres (partant du set_cube). + * la somme des coeffs à utiliser pour la moyenne générale. -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 *Remarque* : Adaptation de moy_ue.compute_ue_moys_apc au cas des moyennes de tag @@ -187,34 +230,41 @@ def compute_tag_moy(set_cube: np.array, etudids: list, tags: list): Args: set_cube: notes moyennes aux modules ndarray - (etuds x modimpls x UEs), des floats avec des NaN + (etuds x UEs|compétences x sxtags), des floats avec des NaN + coeffs_cube: somme des coeffs impliqués dans la moyennes etudids: liste des étudiants (dim. 0 du cube) + competences: list tags: liste des tags (dim. 1 du cube) Returns: Un DataFrame avec pour columns les moyennes par tags, et pour rows les etudid """ - nb_etuds, nb_tags, nb_semestres = set_cube.shape + nb_etuds, nb_comps, nb_semestres = set_cube.shape assert nb_etuds == len(etudids) - assert nb_tags == len(tags) + assert nb_comps == len(competences) # Quelles entrées du cube contiennent des notes ? mask = ~np.isnan(set_cube) - # Enlève les NaN du cube pour les entrées manquantes + # Enlève les NaN du cube de notes pour les entrées manquantes set_cube_no_nan = np.nan_to_num(set_cube, nan=0.0) + coeffs_cube_no_nan = no.nan_to_num(coeff_cube, nan=0.0) # Les moyennes par tag with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) etud_moy_tag = np.sum(set_cube_no_nan, axis=2) / np.sum(mask, axis=2) + # La somme des coeffs + coeff_tag = np.sum(coeffs_cube_no_nan, axis=2) - # Le dataFrame + # Le dataFrame des notes moyennes etud_moy_tag_df = pd.DataFrame( etud_moy_tag, index=etudids, # les etudids - columns=tags, # les tags + columns=competences, # les competences ) - etud_moy_tag_df.fillna(np.nan) - return etud_moy_tag_df + coeffs_df = pd.DataFrame(coeff_tag, index=etudids, columns=competences) + coeffs_df.fillna(np.nan) + + return etud_moy_tag_df, coeffs_df diff --git a/app/pe/pe_ressemtag.py b/app/pe/pe_ressemtag.py index 8274e02d..0c4bc748 100644 --- a/app/pe/pe_ressemtag.py +++ b/app/pe/pe_ressemtag.py @@ -85,11 +85,25 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): ues_hors_sport = [ue for ue in self.ues if ue.type != UE_SPORT] self.ues_inscr_parcours_df = self.load_ues_inscr_parcours() + # Les compétences associées aux UEs + self.competences = {} + for ue in self.ues: + if ue.type != UE_SPORT: + assert ue.niveau_competence, ScoValueError("Des UEs ne sont pas rattachées à des compétences") + nom = ue.niveau_competence.competence.titre + self.competences[ue.ue_id] = nom + # 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]] + # Les coefficients + matrice_coeffs = self.ues_inscr_parcours_df * [ + ue.ects + for ue in ues_hors_sport # if ue.type != UE_SPORT <= déjà supprimé + ] + # Calcul des moyennes & les classements de chaque étudiant à chaque tag self.moyennes_tags = {} @@ -99,13 +113,10 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): moy_ues_tag = self.compute_moy_ues_tag(infos_tag) # moy_gen_tag = self.compute_moy_gen_tag(moy_ues_tag) - ues_dict = {ue.id: ue for ue in ues_hors_sport} self.moyennes_tags[tag] = MoyennesTag( tag, - ues_dict, moy_ues_tag, - self.ues_inscr_parcours_df - # moy_gen_tag + matrice_coeffs ) # Ajoute les d'UE moyennes générales de BUT pour le semestre considéré @@ -117,12 +128,8 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): {ue.id: self.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 - ues_dict = {ue.id: ue for ue in ues_hors_sport} - self.moyennes_tags["but"] = MoyennesTag( - "but", ues_dict, df_ues, self.ues_inscr_parcours_df # , moy_gen_but + "but", df_ues, matrice_coeffs # , moy_gen_but ) self.tags_sorted = self.get_all_tags() @@ -183,6 +190,13 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): self.dispense_ues, block=self.formsemestre.block_moyennes, ) + + # Transforme les UEs en acronyme + colonnes = moyennes_ues_tag.columns + ue_to_acro = {ue.id: ue.acronyme for ue in self.ues} + acronymes = [ue_to_acro[col] for col in colonnes] + moyennes_ues_tag.columns = acronymes + return moyennes_ues_tag def _get_tags_dict(self): @@ -299,27 +313,3 @@ def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre): 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 diff --git a/app/pe/pe_sxtag.py b/app/pe/pe_sxtag.py index 71363028..00b41c07 100644 --- a/app/pe/pe_sxtag.py +++ b/app/pe/pe_sxtag.py @@ -50,7 +50,7 @@ from app.pe.pe_moytag import MoyennesTag class SxTag(TableTag): def __init__(self, sxtag_id: (int, int), ressembuttags: dict[int, ResSemBUTTag]): """Calcule les moyennes/classements par tag d'un semestre de type 'Sx' - (par ex. 'S1', 'S2', ...) : + (par ex. 'S1', 'S2', ...) avec une orientation par UE : * pour les étudiants non redoublants, ce sont les moyennes/classements du semestre suivi @@ -91,6 +91,7 @@ class SxTag(TableTag): self.etuds = ressembuttags[self.fid_final].etuds self.add_etuds(self.etuds) + # Affichage pe_affichage.pe_print(f"--> {self.get_repr()}") # Les tags @@ -99,38 +100,53 @@ class SxTag(TableTag): pe_affichage.pe_print(f"* Tags : {', '.join(self.tags_sorted)}") # Les UE - self.ues = self.ressembuttag_final.moyennes_tags["but"].ues + moy_sem_final = self.ressembuttag_final.moyennes_tags["but"] + self.ues = list(moy_sem_final.matrice_notes.columns) # Les acronymes des UE - self.acronymes_ues_sorted = sorted([ue.acronyme for ue in self.ues.values()]) + self.acronymes_ues_sorted = sorted(self.ues) # Les inscriptions des étudiants aux UEs # => ne conserve que les UEs du semestre final (pour les redoublants) - self.ues_inscr_parcours_df = self.ressembuttag_final.moyennes_tags[ + self.matrice_coeffs = self.ressembuttag_final.moyennes_tags[ "but" - ].ues_inscr_parcours_df + ].matrice_coeffs + self.ues_inscr_parcours = ~np.isnan(self.matrice_coeffs.to_numpy()) + # Les moyennes par tag self.moyennes_tags: dict[str, pd.DataFrame] = {} """Les notes aux UEs dans différents tags""" # Masque des inscriptions - inscr_mask = self.ues_inscr_parcours_df.to_numpy() + inscr_mask = self.ues_inscr_parcours for tag in self.tags_sorted: - # Cube de note + # Cube de note etudids x UEs notes_cube = self.compute_notes_ues_cube(tag, self.acronymes_ues_sorted) # Calcule des moyennes sous forme d'un dataframe""" - moys_ues = compute_notes_ues( + matrice_moys_ues = compute_notes_ues( notes_cube, self.etudids, self.acronymes_ues_sorted, inscr_mask, ) + + # Les profils d'ects (pour debug) + profils_ects = [] + for i in self.matrice_coeffs.index: + val = tuple(self.matrice_coeffs.loc[i].fillna("x")) + if tuple(val) not in profils_ects: + profils_ects.append(tuple(val)) + # Les moyennes - self.moyennes_tags[tag] = MoyennesTag( - tag, self.ues, moys_ues, self.ues_inscr_parcours_df - ) + self.moyennes_tags[tag] = MoyennesTag(tag, + matrice_moys_ues, + self.matrice_coeffs) + + pe_affichage.pe_print(f"> MoyTag pour {tag} avec") + pe_affichage.pe_print(f" - ues={self.acronymes_ues_sorted}") + pe_affichage.pe_print(f" - ects={profils_ects}") def __eq__(self, other): """Egalité de 2 SxTag sur la base de leur identifiant""" @@ -140,7 +156,7 @@ class SxTag(TableTag): """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle est basée)""" affichage = [str(fid) for fid in self.ressembuttags] - return f"S{self.sxtag_id[0]}Tag ({'+'.join(affichage)})" + return f"{self.sxtag_id[0]}Tag ({'+'.join(affichage)})" def compute_notes_ues_cube(self, tag, acronymes_ues_sorted): """Construit le cube de notes des UEs (etudid x accronyme_ue x semestre_aggregé) @@ -160,7 +176,7 @@ class SxTag(TableTag): # Charge les notes du semestre tag sem_tag = self.ressembuttags[frmsem_id] moys_tag = sem_tag.moyennes_tags[tag] - notes = moys_tag.notes_ues # dataframe etudids x ues + notes = moys_tag.matrice_notes # dataframe etudids x ues acronymes_ues_sem = list( notes.columns ) # les acronymes des UEs du semestre tag @@ -183,7 +199,7 @@ class SxTag(TableTag): # Stocke le df dfs[frmsem_id] = df - """Réunit les notes sous forme d'un cube semestres x etdids x ues""" + """Réunit les notes sous forme d'un cube etudids x ues x semestres""" 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 diff --git a/app/pe/pe_tabletags.py b/app/pe/pe_tabletags.py index 596aeae4..ea789ef5 100644 --- a/app/pe/pe_tabletags.py +++ b/app/pe/pe_tabletags.py @@ -94,13 +94,13 @@ class TableTag(object): tags_tries = self.get_all_tags() for tag in tags_tries: moy_tag = self.moyennes_tags[tag] - for acronyme in moy_tag.moys_ues: - moy = moy_tag.moys_ues[acronyme] # une moyenne + for acronyme in moy_tag.moyennes: + moy = moy_tag.moyennes[acronyme] # une moyenne df = df.join(moy.synthese["notes"].rename(f"Moy {tag}-{acronyme}")) df = df.join( moy.synthese["classements"].rename(f"Class {tag}-{acronyme}") ) - moy_gen = moy_tag.moy_gen + moy_gen = moy_tag.moyenne_gen df = df.join(moy_gen.synthese["notes"].rename(f"Moy {tag} (gen)")) df = df.join(moy_gen.synthese["classements"].rename(f"Class {tag} (gen)")) From 70f399e8b798440386c8bd1e34ae251d5b0cf4b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Sun, 18 Feb 2024 19:50:49 +0100 Subject: [PATCH 11/41] =?UTF-8?q?Coquilles=20(=C3=A9tat=20interm=C3=A9diai?= =?UTF-8?q?re)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_jury.py | 4 ++-- app/pe/pe_rcs.py | 15 ++++++++++++--- app/pe/pe_rcstag.py | 6 +++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index f61b601e..c56f70c5 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -107,8 +107,8 @@ class JuryPE(object): self._gen_xls_ressembuttags(zipfile) self._gen_rcss() self._gen_xls_sxtags(zipfile) - # self._gen_rcrcfs() - # self._gen_xls_rcss_tags(zipfile) + self._gen_rcrcfs() + self._gen_xls_rcrcss_tags(zipfile) # self._gen_xls_interclassements_rcss(zipfile) # self._gen_xls_synthese_jury_par_tag(zipfile) # self._gen_xls_synthese_par_etudiant(zipfile) diff --git a/app/pe/pe_rcs.py b/app/pe/pe_rcs.py index ab245a05..0233841f 100644 --- a/app/pe/pe_rcs.py +++ b/app/pe/pe_rcs.py @@ -369,7 +369,7 @@ class RCSsJuryPE: for etudid in self.rcss_suivis: self.rcrcfs_suivis[etudid] = {} - for rcf_id, rcf in self.rcfs_suivis[etudid].items(): # Pour chaque RCS + for nom_rcs, rcf in self.rcfs_suivis[etudid].items(): # Pour chaque RCS semestres_a_aggreger = rcf.semestres_aggreges # Tri des semestres par rang @@ -382,7 +382,7 @@ class RCSsJuryPE: rcf_id = get_rcf_from_semestres_aggreges( self.rcfs, semestres_du_rang ) - if rcf_id: + if not rcf_id: raise ValueError( "Il manque un RCF pour créer les RCRCFs dans cree_rcrcfs" ) @@ -390,7 +390,7 @@ class RCSsJuryPE: # Ajout du RCRCF if rcf_id not in self.rcrcfs: - self.rcrfs[rcf_id] = RCRCF(rcf_id, rcf.formsemestre_final) + self.rcrcfs[rcf_id] = RCRCF(rcf_id, rcf.formsemestre_final) rcrcf = self.rcrcfs[rcf_id] # Ajout des RCFs au RCRCF @@ -400,6 +400,15 @@ class RCSsJuryPE: nom_rcs = rcrcf.nom self.rcrcfs_suivis[etudid][nom_rcs] = rcrcf + # Affichage pour debug + jeunes = list(enumerate(self.rcrcfs_suivis)) + for no_etud, etudid in jeunes[:20]: + pe_affichage.pe_print(f"-> {etudiants.identites[etudid].nomprenom} :") + for nom_rcs, rcs in self.rcrcfs_suivis[etudid].items(): + if rcs: + pe_affichage.pe_print(f" > RCRCF {nom_rcs}: {rcs.get_repr()}") + else: + pe_affichage.pe_print(f" > RCRCF {nom_rcs}: !!! ") def get_rcs_etudiant( semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str diff --git a/app/pe/pe_rcstag.py b/app/pe/pe_rcstag.py index 9284ef6a..68f3b8f6 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/pe_rcstag.py @@ -145,7 +145,7 @@ class RCSTag(TableTag): notes_dfs = {} coeffs_dfs = {} - for sxtag_id, sxtag in sxstags_ids.item(): + for sxtag_id, sxtag in self.sxstags.items(): # Partant d'un dataframe vierge notes_df = pd.DataFrame(np.nan, index=etudids, columns=competences_sorted) coeffs_df = pd.DataFrame(np.nan, index=etudids, columns=competences_sorted) @@ -210,8 +210,8 @@ class RCSTag(TableTag): def do_complist(self): """Synthétise les compétences à partir des Sxtags aggrégés""" competences = [] - for sxtag_id, sxtag in self.sxstags: - comp = sxtag.moyennes_tags["but"].competences + for sxtag_id, sxtag in self.sxstags.items(): + comp = list(sxtag.moyennes_tags["but"].matrice_notes.columns) competences.extend(comp) return sorted(set(competences)) From d6a75b176e6e5a59f2b1d8f9e7df55b769cfe446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Mon, 19 Feb 2024 14:50:38 +0100 Subject: [PATCH 12/41] =?UTF-8?q?Am=C3=A9lioration=20structure=20codes=20+?= =?UTF-8?q?=20mise=20en=20place=20des=20capitalisations=20dans=20les=20SxT?= =?UTF-8?q?ag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_comp.py | 29 ++- app/pe/pe_interclasstag.py | 19 +- app/pe/pe_jury.py | 56 ++--- app/pe/pe_moytag.py | 4 +- app/pe/{pe_rcs.py => pe_rcss_jury.py} | 312 +++++--------------------- app/pe/pe_rcstag.py | 44 ++-- app/pe/pe_ressemtag.py | 108 +++++---- app/pe/pe_sxtag.py | 271 +++++++++++++++------- app/pe/pe_tabletags.py | 13 +- app/pe/rcss/__init__.py | 0 app/pe/rcss/pe_rcf.py | 57 +++++ app/pe/rcss/pe_rcrcf.py | 69 ++++++ app/pe/rcss/pe_rcs.py | 62 +++++ app/pe/rcss/rcss_constantes.py | 64 ++++++ 14 files changed, 658 insertions(+), 450 deletions(-) rename app/pe/{pe_rcs.py => pe_rcss_jury.py} (51%) create mode 100644 app/pe/rcss/__init__.py create mode 100644 app/pe/rcss/pe_rcf.py create mode 100644 app/pe/rcss/pe_rcrcf.py create mode 100644 app/pe/rcss/pe_rcs.py create mode 100644 app/pe/rcss/rcss_constantes.py diff --git a/app/pe/pe_comp.py b/app/pe/pe_comp.py index 24edf207..4f4ba8ed 100644 --- a/app/pe/pe_comp.py +++ b/app/pe/pe_comp.py @@ -41,13 +41,13 @@ import datetime import re import unicodedata - +import pandas as pd from flask import g import app.scodoc.sco_utils as scu from app.models import FormSemestre -from app.pe.pe_rcs import TYPES_RCS +from app.pe.rcss.rcss_constantes import TYPES_RCS from app.scodoc import sco_formsemestre from app.scodoc.sco_logos import find_logo @@ -291,6 +291,29 @@ def tri_semestres_par_rang(cosemestres: dict[int, FormSemestre]): dictionnaire {rang: [liste des semestres du dit rang]}""" cosemestres_tries = {} for sem in cosemestres.values(): - cosemestres_tries[sem.semestre_id] = cosemestres_tries.get(sem.semestre_id, []) + [sem] + cosemestres_tries[sem.semestre_id] = cosemestres_tries.get( + sem.semestre_id, [] + ) + [sem] return cosemestres_tries + +def find_index_and_columns_communs( + df1: pd.DataFrame, df2: pd.DataFrame +) -> (list, list): + """Partant de 2 DataFrames ``df1`` et ``df2``, renvoie les indices de lignes + et de colonnes, communes aux 2 dataframes + + Args: + df1: Un dataFrame + df2: Un dataFrame + Returns: + Le tuple formé par la liste des indices de lignes communs et la liste des indices + de colonnes communes entre les 2 dataFrames + """ + indices1 = df1.index + indices2 = df2.index + indices_communs = list(df1.index.intersection(df2.index)) + colonnes1 = df1.columns + colonnes2 = df2.columns + colonnes_communes = list(set(colonnes1) & set(colonnes2)) + return indices_communs, colonnes_communes diff --git a/app/pe/pe_interclasstag.py b/app/pe/pe_interclasstag.py index b015136b..91105066 100644 --- a/app/pe/pe_interclasstag.py +++ b/app/pe/pe_interclasstag.py @@ -38,9 +38,10 @@ import numpy as np 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_rcs import RCS, RCSsJuryPE -from app.pe.pe_rcstag import RCSTag +import app.pe.pe_etudiant as pe_etudiant +import app.pe.rcss.pe_rcs as pe_rcs +import app.pe.pe_rcss_jury as pe_rcss_jury +import app.pe.pe_rcstag as pe_rcstag class RCSInterclasseTag(TableTag): @@ -58,9 +59,9 @@ class RCSInterclasseTag(TableTag): def __init__( self, nom_rcs: str, - etudiants: EtudiantsJuryPE, - rcss_jury_pe: RCSsJuryPE, - rcss_tags: dict[tuple, RCSTag], + etudiants: pe_etudiant.EtudiantsJuryPE, + rcss_jury_pe: pe_rcss_jury.RCSsJuryPE, + rcss_tags: dict[tuple, pe_rcstag.RCSTag], ): TableTag.__init__(self) @@ -80,21 +81,21 @@ class RCSInterclasseTag(TableTag): # Les trajectoires (et leur version tagguées), en ne gardant que # celles associées à l'aggrégat - self.rcss: dict[int, RCS] = {} + self.rcss: dict[int, pe_rcs.RCS] = {} """Ensemble des trajectoires associées à l'aggrégat""" for trajectoire_id in rcss_jury_pe.rcss: trajectoire = rcss_jury_pe.rcss[trajectoire_id] if trajectoire_id[0] == nom_rcs: self.rcss[trajectoire_id] = trajectoire - self.trajectoires_taggues: dict[int, RCS] = {} + self.trajectoires_taggues: dict[int, pe_rcs.RCS] = {} """Ensemble des trajectoires tagguées associées à l'aggrégat""" for trajectoire_id in self.rcss: self.trajectoires_taggues[trajectoire_id] = rcss_tags[trajectoire_id] # Les trajectoires suivies par les étudiants du jury, en ne gardant que # celles associées aux diplomés - self.suivi: dict[int, RCS] = {} + self.suivi: dict[int, pe_rcs.RCS] = {} """Association entre chaque étudiant et la trajectoire tagguée à prendre en compte pour l'aggrégat""" for etudid in self.diplomes_ids: diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index c56f70c5..323080d3 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -50,15 +50,16 @@ from zipfile import ZipFile import numpy as np import pandas as pd +import app.pe.rcss.rcss_constantes as rcss_constants from app.pe import pe_sxtag from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE import app.pe.pe_affichage as pe_affichage import app.pe.pe_etudiant as pe_etudiant -import app.pe.pe_rcs as pe_rcs +import app.pe.rcss.pe_rcs as pe_rcs from app.pe.pe_rcstag import RCSTag from app.pe.pe_ressemtag import ResSemBUTTag from app.pe.pe_interclasstag import RCSInterclasseTag -import app.pe.pe_comp as pe_comp +import app.pe.pe_rcss_jury as pe_rcss_jury class JuryPE(object): @@ -94,7 +95,7 @@ class JuryPE(object): self.etudiants.find_etudiants() self.diplomes_ids = self.etudiants.diplomes_ids - self.rcss_jury = pe_rcs.RCSsJuryPE(self.diplome) + self.rcss_jury = pe_rcss_jury.RCSsJuryPE(self.diplome) """Les informations sur les regroupements de semestres""" self.zipdata = io.BytesIO() @@ -102,16 +103,18 @@ class JuryPE(object): if not self.diplomes_ids: pe_affichage.pe_print("*** Aucun étudiant diplômé") else: - self._gen_xls_diplomes(zipfile) - - self._gen_xls_ressembuttags(zipfile) - self._gen_rcss() - self._gen_xls_sxtags(zipfile) - self._gen_rcrcfs() - self._gen_xls_rcrcss_tags(zipfile) - # self._gen_xls_interclassements_rcss(zipfile) - # self._gen_xls_synthese_jury_par_tag(zipfile) - # self._gen_xls_synthese_par_etudiant(zipfile) + try: + self._gen_xls_diplomes(zipfile) + self._gen_xls_ressembuttags(zipfile) + self._gen_rcss() + self._gen_xls_sxtags(zipfile) + self._gen_rcrcfs() + self._gen_xls_rcrcss_tags(zipfile) + # self._gen_xls_interclassements_rcss(zipfile) + # self._gen_xls_synthese_jury_par_tag(zipfile) + # self._gen_xls_synthese_par_etudiant(zipfile) + except Exception as e: + raise e # et le log self._add_log_to_zip(zipfile) @@ -150,7 +153,7 @@ class JuryPE(object): formsemestres = get_formsemestres_etudiants(self.etudiants) pe_affichage.pe_print( - f" --> {len(formsemestres)} résultats de semestres à considérer" + f"--> {len(formsemestres)} résultats de semestres à considérer" ) self.ressembuttags = {} @@ -186,14 +189,15 @@ class JuryPE(object): ) self.rcss_jury.cree_rcss(self.etudiants) - self.rcss_jury.cree_rcfs(self.etudiants) def _gen_xls_sxtags(self, zipfile: ZipFile): """Génère les semestres taggués en s'appuyant sur les RCS de type Sx (pour identifier les redoublements impactant les semestres taggués). """ # Génère les regroupements de semestres de type Sx - pe_affichage.pe_print("*** Génère les RCF (RCS de type Sx)***") + pe_affichage.pe_print( + "*** Génère les RCSValid (RCS de même Sx donnant lieu à validation du semestre)" + ) self.rcss_jury.cree_rcfs(self.etudiants) # Génère les moyennes des RCS de type Sx @@ -204,11 +208,8 @@ class JuryPE(object): for rcf_id, rcf in self.rcss_jury.rcfs.items(): # SxTag traduisant le RCF sxtag_id = rcf_id - # Les resultats des semestres taggués à prendre en compte dans le RCF - ressemstags = { - fid: self.ressembuttags[fid] for fid in rcf.semestres_aggreges - } - self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, ressemstags) + + self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, rcf, self.ressembuttags) # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() @@ -232,10 +233,11 @@ class JuryPE(object): def _gen_rcrcfs(self): """Génère les regroupements cohérents de RCFs qu'ont suivi chaque étudiant""" - pe_affichage.pe_print("*** Génère les RCRCF (regroupements de RCF de type Sx) amenant du S1 à un semestre final***") + pe_affichage.pe_print( + "*** Génère les RCRCF (regroupements de RCF de type Sx) amenant du S1 à un semestre final***" + ) self.rcss_jury.cree_rcrcfs(self.etudiants) - def _gen_xls_rcrcss_tags(self, zipfile: ZipFile): """Génère les RCS taggués traduisant les moyennes (orientées compétences) de regroupements de semestre de type Sx, xA ou xS. @@ -268,7 +270,7 @@ class JuryPE(object): output, engine="openpyxl" ) as writer: for rcs_tag in self.rcss_tags.values(): - onglet = rcs_tag.get_repr(mode="short") + onglet = rcs_tag.get_repr(verbose=False) df = rcs_tag.df_moyennes_et_classements() # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) @@ -276,7 +278,7 @@ class JuryPE(object): self.add_file_to_zip( zipfile, - f"RCS_taggues_{self.diplome}.xlsx", + f"RCRCFs_{self.diplome}.xlsx", output.read(), path="details", ) @@ -431,7 +433,7 @@ class JuryPE(object): # Ajout des aggrégats for aggregat in pe_rcs.TOUS_LES_RCS: - descr = pe_rcs.TYPES_RCS[aggregat]["descr"] + descr = app.pe.rcss.constantes.TYPES_RCS[aggregat]["descr"] # Les trajectoires (tagguées) suivies par les étudiants pour l'aggrégat et le tag # considéré @@ -632,7 +634,7 @@ def get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict: def compute_interclassements( etudiants: pe_etudiant.EtudiantsJuryPE, - trajectoires_jury_pe: pe_rcs.RCSsJuryPE, + trajectoires_jury_pe: pe_rcss_jury.RCSsJuryPE, trajectoires_tagguees: dict[tuple, pe_rcs.RCS], ): """Interclasse les étudiants, (nom d') aggrégat par aggrégat, diff --git a/app/pe/pe_moytag.py b/app/pe/pe_moytag.py index 05d3882b..1df487ff 100644 --- a/app/pe/pe_moytag.py +++ b/app/pe/pe_moytag.py @@ -181,7 +181,7 @@ class MoyennesTag: self.matrice_notes: pd.DataFrame = matrice_notes """Les notes aux UEs ou aux compétences (DataFrame)""" - self.matrice_coeffs: pd.DataFrame = matrice_coeffs + self.matrice_coeffs_moy_gen: pd.DataFrame = matrice_coeffs """Les coeffs à appliquer pour le calcul des moyennes générales (toutes UE ou compétences confondues). NaN si étudiant non inscrit""" @@ -193,7 +193,7 @@ class MoyennesTag: self.moyennes[col] = Moyenne(notes) # Les moyennes générales - notes_gen = self.compute_moy_gen(self.matrice_notes, self.matrice_coeffs) + notes_gen = self.compute_moy_gen(self.matrice_notes, self.matrice_coeffs_moy_gen) self.notes_gen = notes_gen """Les notes générales (moyenne toutes UEs confonudes)""" self.moyenne_gen = Moyenne(notes_gen) diff --git a/app/pe/pe_rcs.py b/app/pe/pe_rcss_jury.py similarity index 51% rename from app/pe/pe_rcs.py rename to app/pe/pe_rcss_jury.py index 0233841f..e514e5bb 100644 --- a/app/pe/pe_rcs.py +++ b/app/pe/pe_rcss_jury.py @@ -1,248 +1,11 @@ -############################################################################## -# Module "Avis de poursuite d'étude" -# conçu et développé par Cléo Baras (IUT de Grenoble) -############################################################################## - -""" -Created on 01-2024 - -@author: barasc -""" +import app.pe.rcss.pe_rcf as pe_rcf +import app.pe.rcss.pe_rcrcf as pe_rcrcf +import app.pe.pe_etudiant as pe_etudiant import app.pe.pe_comp as pe_comp - +import app.pe.rcss.rcss_constantes as rcss_constantes from app.models import FormSemestre -from app.pe import pe_sxtag, pe_affichage -from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date - -TYPES_RCS = { - "S1": { - "aggregat": ["S1"], - "descr": "Semestre 1 (S1)", - }, - "S2": { - "aggregat": ["S2"], - "descr": "Semestre 2 (S2)", - }, - "1A": { - "aggregat": ["S1", "S2"], - "descr": "BUT1 (S1+S2)", - }, - "S3": { - "aggregat": ["S3"], - "descr": "Semestre 3 (S3)", - }, - "S4": { - "aggregat": ["S4"], - "descr": "Semestre 4 (S4)", - }, - "2A": { - "aggregat": ["S3", "S4"], - "descr": "BUT2 (S3+S4)", - }, - "3S": { - "aggregat": ["S1", "S2", "S3"], - "descr": "Moyenne du semestre 1 au semestre 3 (S1+S2+S3)", - }, - "4S": { - "aggregat": ["S1", "S2", "S3", "S4"], - "descr": "Moyenne du semestre 1 au semestre 4 (S1+S2+S3+S4)", - }, - "S5": { - "aggregat": ["S5"], - "descr": "Semestre 5 (S5)", - }, - "S6": { - "aggregat": ["S6"], - "descr": "Semestre 6 (S6)", - }, - "3A": { - "aggregat": ["S5", "S6"], - "descr": "3ème année (S5+S6)", - }, - "5S": { - "aggregat": ["S1", "S2", "S3", "S4", "S5"], - "descr": "Moyenne du semestre 1 au semestre 5 (S1+S2+S3+S4+S5)", - }, - "6S": { - "aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"], - "descr": "Moyenne globale (S1+S2+S3+S4+S5+S6)", - }, -} -"""Dictionnaire détaillant les différents regroupements cohérents -de semestres (RCS), en leur attribuant un nom et en détaillant -le nom des semestres qu'ils regroupent et l'affichage qui en sera fait -dans les tableurs de synthèse. -""" - -TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS if not cle.startswith("S")] -TOUS_LES_RCS = list(TYPES_RCS.keys()) -TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")] - - -def get_descr_rcs(nom_rcs: str) -> str: - """Renvoie la description pour les tableurs de synthèse - Excel d'un nom de RCS""" - return TYPES_RCS[nom_rcs]["descr"] - - -class RCS: - """Modélise un regroupement cohérent de semestres (formsemestre ou de Sx)""" - - def __init__(self, nom_rcs: str, semestre_final: FormSemestre): - self.nom: str = nom_rcs - """Nom du RCS""" - - self.formsemestre_final: FormSemestre = semestre_final - """FormSemestre terminal du RCS""" - - self.rang_final = self.formsemestre_final.semestre_id - """Le rang du formsemestre final""" - - self.rcs_id: (str, int) = (nom_rcs, semestre_final.formsemestre_id) - """Identifiant du RCS sous forme (nom_rcs, id du semestre_terminal)""" - - def get_formsemestre_id_final(self) -> int: - """Renvoie l'identifiant du formsemestre final du RCS - - Returns: - L'id du formsemestre final (marquant la fin) du RCS - """ - return self.formsemestre_final.formsemestre_id - - def __repr__(self): - """Représentation textuelle d'un RCS""" - return f"{self.nom} ({self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}" - - def get_repr(self): - return self.__repr__() - - def __eq(self, other): - """Egalité de RCS""" - return ( - self.nom == other.nom - and self.formsemestre_final == other.formsemestre_final - ) - - -class RCF(RCS): - """Modélise un ensemble de (form)semestres d'étudiants - associé à un type de regroupement cohérent de semestres - donné (par ex: 'S2', '3S', '2A'). - - Si le RCF est un semestre de type Si, stocke les - formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si - (en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants) - - Pour le RCF de type iS ou iA (par ex, 3A=S1+S2+S3), identifie - les semestres que les étudiants ont suivis pour les amener jusqu'au semestre - terminal du RCS (par ex: ici un S3). - - Ces semestres peuvent être : - - * des S1+S2+S1+S2+S3 si redoublement de la 1ère année - * des S1+S2+(année de césure)+S3 si césure, ... - - Args: - nom_rcs: Un nom du RCS (par ex: '5S') - semestre_final: Le formsemestre final du RCS - """ - - def __init__(self, nom_rcs: str, semestre_final: FormSemestre): - RCS.__init__(self, nom_rcs, semestre_final) - - self.semestres_aggreges: dict[int:FormSemestre] = {} - """Formsemestres regroupés dans le RCS""" - - def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]): - """Ajout de semestres aux semestres à regrouper - - Args: - semestres: Dictionnaire ``{fid: Formsemestre)`` - """ - self.semestres_aggreges = self.semestres_aggreges | semestres - - def get_repr(self, verbose=True) -> str: - """Représentation textuelle d'un RCS - basé 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) - title = f"""{str(self)}""" - if verbose and noms: - title += " - " + "+".join(noms) - return title - - - -def get_rcf_from_semestres_aggreges( - rcfs: dict[(str, int):RCF], semestres_a_aggreges: list[FormSemestre] - ) -> (str, int): - """Partant d'un dictionnaire de RCFs (de la forme - ``{ (nom_rcs, fid): RCF }, et connaissant une liste - de (form)semestres à aggréger, renvoie l'identifiant - (nom_rcs, fid) du RCFs qui lui correspond (c'est à dire celui dont - les semestres_aggregés par le RCF sont les même que les - semestres_a_aggreger. - - Returns: - rcf_id: L'identifiant du RCF trouvé - """ - fids_semestres_a_aggreger = set( - [frms.formsemestre_id for frms in semestres_a_aggreges] - ) - for rcf_id, rcf in rcfs.items(): - fids_rcf = set(rcf.semestres_aggreges) - if fids_rcf == fids_semestres_a_aggreger: - return rcf_id - return None - - -class RCRCF: - """Modélise les RCF d'étudiants suivis par un étudiant dans - le cadre d'un RCS donné (par ex: 3S=S1+S2+S3). - - Pour rappel : un RCF (par ex. S1) combine les semestres 1 qu'a suivi - l'étudiant pour valider son S1 (1 si étudiant standard, 2 si redoublant). - - Le RCRCF 3S est donc le regroupement du RCF S1 + RCF S2 + RCF S3. - - Il est identifié par le formsemestre de S3 marquant la fin du regroupement. - - - Args: - nom_rcs: Un nom du RCS (par ex: '5S') - semestre_final: Le semestre final du RCS - """ - - def __init__(self, nom_rcs: str, semestre_final: FormSemestre): - RCS.__init__(self, nom_rcs, semestre_final) - - self.rcfs_aggreges: dict[(str, int) : pe_sxtag.SxTag] = {} - """Les RCFs à aggréger""" - - def add_rcfs_a_aggreger(self, rcfs: dict[(str, int):RCF]): - """Ajout des RCFs aux RCFS à regrouper - - Args: - rcfs: Dictionnaire ``{(str,fid): RCF}`` à ajouter - """ - self.rcfs_aggreges = self.rcfs_aggreges | rcfs - - def get_repr(self, verbose=True) -> str: - """Représentation textuelle d'un RCSF - basé sur ses RCF aggrégés""" - - noms = [] - for rcf_id, rcf in self.rcfs_aggreges.items(): - noms.append(rcf.get_repr()) - title = f"""{str(self)}""" - if verbose and noms: - title += " : " + "+".join(noms) - return title +from app.pe import pe_affichage class RCSsJuryPE: @@ -257,28 +20,28 @@ class RCSsJuryPE: self.annee_diplome = annee_diplome """Année de diplômation""" - self.rcss: dict[tuple(int, str) : RCF] = {} + self.rcss: dict[tuple(int, str): pe_rcf.RCF] = {} """Ensemble des RCS recensés""" self.rcss_suivis: dict[int:dict] = {} """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, son RCS : {etudid: {nom_RCS: RCS}}""" - self.rcfs: dict[tuple(int, str) : RCF] = {} + self.rcfs: dict[tuple(int, str) : pe_rcf.RCF] = {} """Ensemble des RCF recensés : {(nom_RCS, fid_terminal): RCF}""" self.rcfs_suivis: dict[int:dict] = {} """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, son RCS : {etudid: {nom_RCS: RCF}}""" - self.rcrcfs: dict[tuple(int, str) : RCRCF] = {} + self.rcrcfs: dict[tuple(int, str) : pe_rcrcf.RCRCF] = {} """Ensemble des RCS recensés : {(nom_RCS, fid_terminal): RCRCF}""" self.rcrcfs_suivis: dict[int:str] = {} """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, son RCRCF : {etudid: {nom_RCS: RCSx}}""" - def cree_rcss(self, etudiants: EtudiantsJuryPE): + def cree_rcss(self, etudiants: pe_etudiant.EtudiantsJuryPE): """Créé tous les RCS, au regard du cursus des étudiants analysés + les mémorise dans les données de l'étudiant @@ -286,17 +49,17 @@ class RCSsJuryPE: etudiants: Les étudiants à prendre en compte dans le Jury PE """ + tous_les_aggregats = rcss_constantes.TOUS_LES_SEMESTRES + rcss_constantes.TOUS_LES_RCS_AVEC_PLUSIEURS_SEM for etudid in etudiants.trajectoires: self.rcss_suivis[etudid] = { aggregat: None - for aggregat in pe_comp.TOUS_LES_SEMESTRES - + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM + for aggregat in tous_les_aggregats } - for nom_rcs in pe_comp.TOUS_LES_SEMESTRES + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM: + for nom_rcs in 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 = TYPES_RCS[nom_rcs]["aggregat"] + noms_semestre_de_aggregat = rcss_constantes.TYPES_RCS[nom_rcs]["aggregat"] nom_semestre_terminal = noms_semestre_de_aggregat[-1] for etudid in etudiants.trajectoires: @@ -305,12 +68,12 @@ class RCSsJuryPE: # (par ex: son dernier S3 en date) trajectoire = etudiants.trajectoires[etudid][nom_semestre_terminal] if trajectoire: - formsemestre_final = get_dernier_semestre_en_date(trajectoire) + formsemestre_final = pe_etudiant.get_dernier_semestre_en_date(trajectoire) # Ajout ou récupération du RCS associé rcs_id = (nom_rcs, formsemestre_final.formsemestre_id) if rcs_id not in self.rcss: - self.rcss[rcs_id] = RCF(nom_rcs, formsemestre_final) + self.rcss[rcs_id] = pe_rcf.RCF(nom_rcs, formsemestre_final) rcs = self.rcss[rcs_id] # La liste des semestres de l'étudiant à prendre en compte @@ -328,23 +91,22 @@ class RCSsJuryPE: # Affichage pour debug jeunes = list(enumerate(self.rcss_suivis)) for no_etud, etudid in jeunes[:20]: - pe_affichage.pe_print(f"-> {etudiants.identites[etudid].nomprenom} :") + pe_affichage.pe_print(f"--> {etudiants.identites[etudid].nomprenom} (#{etudid}) :") for nom_rcs, rcs in self.rcss_suivis[etudid].items(): if rcs: pe_affichage.pe_print(f" > RCS {nom_rcs}: {rcs.get_repr()}") - def cree_rcfs(self, etudiants: EtudiantsJuryPE): + def cree_rcfs(self, etudiants: pe_etudiant.EtudiantsJuryPE): """Créé les RCFs en ne conservant dans les RCS que les regroupements de type Sx""" self.rcfs = {} for rcs_id, rcs in self.rcss.items(): - if rcs and rcs.nom in pe_comp.TOUS_LES_SEMESTRES: + if rcs and rcs.nom in rcss_constantes.TOUS_LES_SEMESTRES: self.rcfs[rcs_id] = rcs - print(self.rcfs) for etudid in self.rcss_suivis: for nom_rcs, rcs in self.rcss_suivis[etudid].items(): - if rcs and nom_rcs in pe_comp.TOUS_LES_SEMESTRES: + if rcs and nom_rcs in rcss_constantes.TOUS_LES_SEMESTRES: if etudid not in self.rcfs_suivis: self.rcfs_suivis[etudid] = {} self.rcfs_suivis[etudid][nom_rcs] = rcs @@ -355,11 +117,11 @@ class RCSsJuryPE: pe_affichage.pe_print(f"-> {etudiants.identites[etudid].nomprenom} :") for nom_rcs, rcs in self.rcfs_suivis[etudid].items(): if rcs: - pe_affichage.pe_print(f" > RCF {nom_rcs}: {rcs.get_repr()}") + pe_affichage.pe_print(f" > RCSValid {nom_rcs}: {rcs.get_repr()}") else: - pe_affichage.pe_print(f" > RCF {nom_rcs}: !!! ") + pe_affichage.pe_print(f" > RCSValid {nom_rcs}: ") - def cree_rcrcfs(self, etudiants: EtudiantsJuryPE): + def cree_rcrcfs(self, etudiants: pe_etudiant.EtudiantsJuryPE): """Créé tous les RCRCF, au regard du cursus des étudiants analysés (trajectoires traduisant son parcours dans les différents semestres) + les mémorise dans les données de l'étudiant @@ -390,7 +152,7 @@ class RCSsJuryPE: # Ajout du RCRCF if rcf_id not in self.rcrcfs: - self.rcrcfs[rcf_id] = RCRCF(rcf_id, rcf.formsemestre_final) + self.rcrcfs[rcf_id] = pe_rcrcf.RCRCF(rcf_id, rcf.formsemestre_final) rcrcf = self.rcrcfs[rcf_id] # Ajout des RCFs au RCRCF @@ -410,6 +172,8 @@ class RCSsJuryPE: else: pe_affichage.pe_print(f" > RCRCF {nom_rcs}: !!! ") + + def get_rcs_etudiant( semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str ) -> dict[int, FormSemestre]: @@ -439,7 +203,7 @@ def get_rcs_etudiant( numero_semestres_possibles = [numero_semestre_terminal] elif nom_rcs.endswith("A"): # les années numero_semestres_possibles = [ - int(sem[-1]) for sem in TYPES_RCS[nom_rcs]["aggregat"] + int(sem[-1]) for sem in rcss_constantes.TYPES_RCS[nom_rcs]["aggregat"] ] assert numero_semestre_terminal in numero_semestres_possibles else: # les xS = tous les semestres jusqu'à Sx (eg S1, S2, S3 pour un S3 terminal) @@ -454,3 +218,27 @@ def get_rcs_etudiant( ): semestres_aggreges[fid] = semestre return semestres_aggreges + + +def get_rcf_from_semestres_aggreges( + rcfs: dict[(str, int):pe_rcf.RCF], semestres_a_aggreges: list[FormSemestre] + ) -> (str, int): + """Partant d'un dictionnaire de RCFs (de la forme + ``{ (nom_rcs, fid): RCF }, et connaissant une liste + de (form)semestres à aggréger, renvoie l'identifiant + (nom_rcs, fid) du RCFs qui lui correspond (c'est à dire celui dont + les semestres_aggregés par le RCF sont les même que les + semestres_a_aggreger. + + Returns: + rcf_id: L'identifiant du RCF trouvé + """ + fids_semestres_a_aggreger = set( + [frms.formsemestre_id for frms in semestres_a_aggreges] + ) + for rcf_id, rcf in rcfs.items(): + fids_rcf = set(rcf.semestres_aggreges) + if fids_rcf == fids_semestres_a_aggreger: + return rcf_id + return None + diff --git a/app/pe/pe_rcstag.py b/app/pe/pe_rcstag.py index 68f3b8f6..70d11491 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/pe_rcstag.py @@ -38,18 +38,18 @@ Created on Fri Sep 9 09:15:05 2016 from app.comp.res_sem import load_formsemestre_results from app.pe import pe_affichage -from app.pe.pe_ressemtag import ResSemBUTTag import pandas as pd import numpy as np -from app.pe.pe_rcs import RCS, RCRCF -from app.pe.pe_sxtag import SxTag +import app.pe.rcss.pe_rcs as pe_rcs +import app.pe.rcss.pe_rcrcf as pe_rcrcf +import app.pe.pe_sxtag as pe_sxtag from app.pe.pe_tabletags import TableTag from app.pe.pe_moytag import MoyennesTag class RCSTag(TableTag): - def __init__(self, rcrcf: RCS, sxstags: dict[(str, int): SxTag]): + def __init__(self, rcrcf: pe_rcs.RCS, sxstags: dict[(str, int): pe_sxtag.SxTag]): """Calcule les moyennes par tag (orientées compétences) d'un regroupement de SxTag (RCRCF), pour extraire les classements par tag pour un @@ -65,7 +65,7 @@ class RCSTag(TableTag): self.rcs_id: tuple(str, int) = rcrcf.rcs_id """Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)""" - self.rcrcf: RCRCF = rcrcf + self.rcrcf: pe_rcrcf.RCRCF = rcrcf """RCRCF associé au RCS taggué""" self.nom = self.get_repr() @@ -74,6 +74,8 @@ class RCSTag(TableTag): self.formsemestre_terminal = rcrcf.formsemestre_final """Le formsemestre terminal""" + pe_affichage.pe_print(f"-> {self.get_repr(verbose=True)}") + # Les résultats du formsemestre terminal nt = load_formsemestre_results(self.formsemestre_terminal) @@ -95,10 +97,12 @@ class RCSTag(TableTag): # Les compétences (extraites de tous les Sxtags) self.competences_sorted = self.do_complist() """Compétences extraites de tous les SxTag aggrégés""" + pe_affichage.pe_print(f"* Compétences : {', '.join(self.competences_sorted)}") # Les tags self.tags_sorted = self.do_taglist() """Tags extraits de tous les SxTag aggrégés""" + pe_affichage.pe_print(f"* Tags : {', '.join(self.tags_sorted)}") # Les moyennes self.moyennes_tags: dict[str, MoyennesTag] = {} @@ -121,10 +125,13 @@ class RCSTag(TableTag): """Egalité de 2 RCS taggués sur la base de leur identifiant""" return self.rcs_id == other.sxtag_id - def get_repr(self, verbose=False) -> str: + def get_repr(self, verbose=True) -> str: """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle est basée)""" - return self.rcrcf.get_repr(verbose=verbose) + if verbose: + return self.rcrcf.get_repr(verbose=verbose) + else: + return f"{self.__class__.__name__} ({self.rcs_id})" def compute_notes_comps_cube(self, tag): """Pour un tag donné, construit : @@ -154,10 +161,11 @@ class RCSTag(TableTag): # Charge les notes et les coeffs du semestre tag notes = moys_tag.matrice_notes.copy() # avec une copie - coeffs = moys_tag.matrice_coeffs.copy() # les coeffs + coeffs = moys_tag.matrice_coeffs_moy_gen.copy() # les coeffs # Traduction des UE en compétences - association_ues_comp = moys_tag.competences + association_ues_comp = self.mapping_ue_competences() + print(association_ues_comp) ues_columns_df = notes.columns comp_associes_aux_ues = [association_ues_comp[ue] for ue in ues_columns_df] notes.columns = comp_associes_aux_ues @@ -204,16 +212,20 @@ class RCSTag(TableTag): tags = [] for frmsem_id in self.sxstags: tags.extend(self.sxstags[frmsem_id].tags_sorted) - pe_affichage.pe_print(f"* Tags : {', '.join(tags)}") return sorted(set(tags)) + def mapping_ue_competences(self): + """Dictionnaire {ue: competences} extrait des SxTags""" + dict_competences = {} + for sxtag_id, sxtag in self.sxstags.items(): + comp = sxtag.competences + dict_competences |= comp + return dict_competences + def do_complist(self): """Synthétise les compétences à partir des Sxtags aggrégés""" - competences = [] - for sxtag_id, sxtag in self.sxstags.items(): - comp = list(sxtag.moyennes_tags["but"].matrice_notes.columns) - competences.extend(comp) - return sorted(set(competences)) + dict_comptences = self.mapping_ue_competences() + return sorted(set(dict_comptences.values())) def compute_notes_competences( @@ -248,7 +260,7 @@ def compute_notes_competences( # Enlève les NaN du cube de notes pour les entrées manquantes set_cube_no_nan = np.nan_to_num(set_cube, nan=0.0) - coeffs_cube_no_nan = no.nan_to_num(coeff_cube, nan=0.0) + coeffs_cube_no_nan = np.nan_to_num(coeff_cube, nan=0.0) # Les moyennes par tag with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) diff --git a/app/pe/pe_ressemtag.py b/app/pe/pe_ressemtag.py index 0c4bc748..8b734855 100644 --- a/app/pe/pe_ressemtag.py +++ b/app/pe/pe_ressemtag.py @@ -48,7 +48,7 @@ import app.pe.pe_etudiant as pe_etudiant import app.pe.pe_tabletags as pe_tabletags from app.pe.pe_moytag import MoyennesTag from app.scodoc import sco_tag_module -from app.scodoc.codes_cursus import UE_SPORT +from app.scodoc import codes_cursus as sco_codes class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): @@ -66,44 +66,51 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): ResultatsSemestreBUT.__init__(self, formsemestre) pe_tabletags.TableTag.__init__(self) - # Le semestre - # self.formsemestre_id = self.formsemestre.formsemestre_id - # self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id) - # Le nom du res_semestre taggué self.nom = self.get_repr(verbose=True) 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 (etuds, états civils & etudis) ajouté self.add_etuds(self.etuds) + self.etudids_sorted = sorted(self.etudids) # Les UEs (et les dispenses d'UE) - ues_hors_sport = [ue for ue in self.ues if ue.type != UE_SPORT] + # self.ues + ues_standards = [ue for ue in self.ues if ue.type == sco_codes.UE_STANDARD] + + # Les UEs en fonction des parcours self.ues_inscr_parcours_df = self.load_ues_inscr_parcours() - # Les compétences associées aux UEs + # Les compétences associées aux UEs (définies par les acronymes) self.competences = {} + """L'association acronyme d'UEs -> compétence""" for ue in self.ues: - if ue.type != UE_SPORT: - assert ue.niveau_competence, ScoValueError("Des UEs ne sont pas rattachées à des compétences") + if ue.type == sco_codes.UE_STANDARD: + assert ue.niveau_competence, ScoValueError( + "Des UEs ne sont pas rattachées à des compétences" + ) nom = ue.niveau_competence.competence.titre - self.competences[ue.ue_id] = nom + self.competences[ue.acronyme] = nom + + # Les acronymes des UEs + self.ues_to_acronymes = {ue.id: ue.acronyme for ue in ues_standards} + self.acronymes_sorted = sorted(self.ues_to_acronymes.values()) + """Les acronymes de UE triés par ordre alphabétique""" # 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]] - # Les coefficients - matrice_coeffs = self.ues_inscr_parcours_df * [ - ue.ects - for ue in ues_hors_sport # if ue.type != UE_SPORT <= déjà supprimé + # Les coefficients pour le calcul de la moyenne générale + self.matrice_coeffs_moy_gen = self.ues_inscr_parcours_df * [ + ue.ects for ue in ues_standards # if ue.type != UE_SPORT <= déjà supprimé ] + # Les capitalisations (mask etuids x acronyme_ue valant True si capitalisée, False sinon) + self.capitalisations = self._get_capitalisations(ues_standards) + + # Calcul des moyennes & les classements de chaque étudiant à chaque tag self.moyennes_tags = {} @@ -111,46 +118,56 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): # pe_affichage.pe_print(f" -> Traitement du tag {tag}") infos_tag = tags_dict["personnalises"][tag] moy_ues_tag = self.compute_moy_ues_tag(infos_tag) - # moy_gen_tag = self.compute_moy_gen_tag(moy_ues_tag) - self.moyennes_tags[tag] = MoyennesTag( - tag, - moy_ues_tag, - matrice_coeffs + tag, moy_ues_tag, self.matrice_coeffs_moy_gen ) - # 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) + # Ajoute les moyennes par UEs + la moyenne générale (but) df_ues = pd.DataFrame( - {ue.id: self.etud_moy_ue[ue.id] for ue in ues_hors_sport}, + {ue.id: self.etud_moy_ue[ue.id] for ue in ues_standards}, index=self.etudids, ) + # Transforme les UEs en acronyme + colonnes = df_ues.columns + acronymes = [self.ues_to_acronymes[col] for col in colonnes] + df_ues.columns = acronymes + self.moyennes_tags["but"] = MoyennesTag( - "but", df_ues, matrice_coeffs # , moy_gen_but + "but", df_ues, self.matrice_coeffs_moy_gen # , 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, verbose=False): """Nom affiché pour le semestre taggué""" if verbose: - return f"{self.formsemestre} ({self.formsemestre.formsemestre_id})" + return f"{self.formsemestre} (#{self.formsemestre.formsemestre_id})" else: return pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True) + def _get_capitalisations(self, ues_hors_sport) -> pd.DataFrame: + """Renvoie un dataFrame résumant les UEs capitalisables par les + étudiants, d'après les décisions de jury + + Args: + ues_hors_sport: Liste des UEs autres que le sport + """ + capitalisations = pd.DataFrame(False, index=self.etudids_sorted, columns=self.acronymes_sorted) + self.get_formsemestre_validations() # charge les validations + res_jury = self.validations + if res_jury: + for etud in self.etuds: + etudid = etud.etudid + decisions = res_jury.decisions_jury_ues.get(etudid, {}) + for ue in ues_hors_sport: + if ue.id in decisions and decisions[ue.id]["code"] == sco_codes.ADM: + capitalisations.loc[etudid, ue.acronyme] = True + # pe_affichage.pe_print( + # f" ⚠ Capitalisation de {ue.acronyme} pour {etud.etat_civil}" + # ) + return capitalisations + 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. @@ -164,7 +181,7 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): # Adaptation du mask de calcul des moyennes au tag visé modimpls_mask = [ - modimpl.module.ue.type != UE_SPORT + modimpl.module.ue.type == sco_codes.UE_STANDARD for modimpl in self.formsemestre.modimpls_sorted ] @@ -216,9 +233,6 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): self.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 @@ -229,8 +243,11 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): dict_tags["auto"] = {"but": {}} noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp + + # Affichage pe_affichage.pe_print( - f"* Tags automatiquement ajoutés : {', '.join(noms_tags_auto)}" + f"* Tags du programme de formation : {', '.join(noms_tags_perso)} " + + f"Tags automatiques : {', '.join(noms_tags_auto)}" ) return dict_tags @@ -312,4 +329,3 @@ def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre): } return synthese_tags - diff --git a/app/pe/pe_sxtag.py b/app/pe/pe_sxtag.py index 00b41c07..132824df 100644 --- a/app/pe/pe_sxtag.py +++ b/app/pe/pe_sxtag.py @@ -36,19 +36,23 @@ Created on Fri Sep 9 09:15:05 2016 @author: barasc """ -from app.comp.res_sem import load_formsemestre_results -from app.models import UniteEns, FormSemestre -from app.pe import pe_affichage -from app.pe.pe_ressemtag import ResSemBUTTag +from app.pe import pe_affichage, pe_comp +import app.pe.pe_ressemtag as pe_ressemtag import pandas as pd import numpy as np from app.pe.pe_tabletags import TableTag from app.pe.pe_moytag import MoyennesTag +import app.pe.rcss.pe_rcf as pe_rcf class SxTag(TableTag): - def __init__(self, sxtag_id: (int, int), ressembuttags: dict[int, ResSemBUTTag]): + def __init__( + self, + sxtag_id: (str, int), + rcf: pe_rcf.RCF, + ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag], + ): """Calcule les moyennes/classements par tag d'un semestre de type 'Sx' (par ex. 'S1', 'S2', ...) avec une orientation par UE : @@ -76,10 +80,14 @@ class SxTag(TableTag): assert sxtag_id and len(sxtag_id) == 2 and sxtag_id[1] in ressembuttags - self.sxtag_id: (int, int) = sxtag_id - """Identifiant du SxTag de la forme (semestre_id, fid_semestre_final)""" + self.sxtag_id: (str, int) = sxtag_id + """Identifiant du SxTag de la forme (nom_Sx, fid_semestre_final)""" - self.ressembuttags = ressembuttags + self.rcf = rcf + """Le RCF sur lequel il s'appuie""" + + # Les resultats des semestres taggués à prendre en compte dans le RCF + self.ressembuttags = {fid: ressembuttags[fid] for fid in rcf.semestres_aggreges} """Les ResSemBUTTags à regrouper dans le SxTag""" # Les données du semestre final @@ -87,9 +95,13 @@ class SxTag(TableTag): self.ressembuttag_final = ressembuttags[self.fid_final] """Le ResSemBUTTag final""" - # Les étudiants (etuds, états civils & etudis) self.etuds = ressembuttags[self.fid_final].etuds + """Les étudiants du ReSemBUTTag final""" + + # Ajout les etudids et les états civils self.add_etuds(self.etuds) + self.etudids_sorted = sorted(self.etudids) + """Les etudids triés""" # Affichage pe_affichage.pe_print(f"--> {self.get_repr()}") @@ -103,50 +115,87 @@ class SxTag(TableTag): moy_sem_final = self.ressembuttag_final.moyennes_tags["but"] self.ues = list(moy_sem_final.matrice_notes.columns) + # L'association UE-compétences extraites du dernier semestre + self.competences = self.ressembuttag_final.competences + # Les acronymes des UE self.acronymes_ues_sorted = sorted(self.ues) # Les inscriptions des étudiants aux UEs # => ne conserve que les UEs du semestre final (pour les redoublants) - self.matrice_coeffs = self.ressembuttag_final.moyennes_tags[ - "but" - ].matrice_coeffs - self.ues_inscr_parcours = ~np.isnan(self.matrice_coeffs.to_numpy()) + self.ues_inscr_parcours_df = self.ressembuttag_final.ues_inscr_parcours_df + self.ues_inscr_parcours_df.sort_index() + # Les coeffs pour la moyenne générale + self.matrice_coeffs_moy_gen = self.ressembuttag_final.moyennes_tags[ + "but" + ].matrice_coeffs_moy_gen + self.matrice_coeffs_moy_gen.sort_index() # Trie les coeff par etudids # Les moyennes par tag self.moyennes_tags: dict[str, pd.DataFrame] = {} """Les notes aux UEs dans différents tags""" - # Masque des inscriptions - inscr_mask = self.ues_inscr_parcours + # Masque des inscriptions et des capitalisations + self.masque_df, masque_cube = compute_masques_ues_cube( + self.etudids_sorted, + self.acronymes_ues_sorted, + self.ressembuttags, + self.fid_final, + ) + # Affichage pour debug + for etud in self.etuds: + cap = [] + for frmsem_id in self.ressembuttags: + if frmsem_id != self.fid_final: + for accr in self.acronymes_ues_sorted: + if self.masque_df[frmsem_id].loc[etud.etudid, accr] > 0.0: + cap += [accr] + if cap: + pe_affichage.pe_print( + f" ⚠ Capitalisation de {etud.etat_civil} : {', '.join(cap)}" + ) + for tag in self.tags_sorted: # Cube de note etudids x UEs - notes_cube = self.compute_notes_ues_cube(tag, self.acronymes_ues_sorted) + notes_df, notes_cube = compute_notes_ues_cube( + tag, self.etudids_sorted, self.acronymes_ues_sorted, self.ressembuttags + ) - # Calcule des moyennes sous forme d'un dataframe""" - matrice_moys_ues = compute_notes_ues( + + + + + # self.ues_inscr_parcours = ~np.isnan(self.matrice_coeffs.to_numpy()) + # inscr_mask = self.ues_inscr_parcours + + # Calcule des moyennes sous forme d'un dataframe + inscr_mask = ~np.isnan(self.ues_inscr_parcours_df.to_numpy()) + matrice_moys_ues: pd.DataFrame = compute_notes_ues( notes_cube, - self.etudids, + masque_cube, + self.etudids_sorted, self.acronymes_ues_sorted, inscr_mask, ) # Les profils d'ects (pour debug) profils_ects = [] - for i in self.matrice_coeffs.index: - val = tuple(self.matrice_coeffs.loc[i].fillna("x")) + for i in self.matrice_coeffs_moy_gen.index: + val = tuple(self.matrice_coeffs_moy_gen.loc[i].fillna("x")) if tuple(val) not in profils_ects: profils_ects.append(tuple(val)) # Les moyennes - self.moyennes_tags[tag] = MoyennesTag(tag, - matrice_moys_ues, - self.matrice_coeffs) + self.moyennes_tags[tag] = MoyennesTag( + tag, matrice_moys_ues, self.matrice_coeffs_moy_gen + ) - pe_affichage.pe_print(f"> MoyTag pour {tag} avec") - pe_affichage.pe_print(f" - ues={self.acronymes_ues_sorted}") - pe_affichage.pe_print(f" - ects={profils_ects}") + pe_affichage.pe_print( + f"> MoyTag 🏷{tag} avec " + + f"ues={self.acronymes_ues_sorted} " + + f"ects={profils_ects}" + ) def __eq__(self, other): """Egalité de 2 SxTag sur la base de leur identifiant""" @@ -155,60 +204,123 @@ class SxTag(TableTag): def get_repr(self, verbose=False) -> str: """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle est basée)""" - affichage = [str(fid) for fid in self.ressembuttags] - return f"{self.sxtag_id[0]}Tag ({'+'.join(affichage)})" + if verbose: + return f"{self.sxtag_id[0]}Tag basé sur {self.rcf.get_repr()}" + else: + # affichage = [str(fid) for fid in self.ressembuttags] + return f"{self.sxtag_id[0]}Tag (#{self.fid_final})" - 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.ressembuttags.keys()) - dfs = {} +def compute_notes_ues_cube( + tag, etudids_sorted, acronymes_ues_sorted, ressembuttags +) -> (pd.DataFrame, np.array): + """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. + (Renvoie également le dataframe associé pour debug). - for frmsem_id in semestres_id: - # Partant d'un dataframe vierge - df = pd.DataFrame(np.nan, index=etudids, columns=acronymes_ues_sorted) + Args: + etudids_sorted: La liste des etudids triés par ordre croissant (dim 0) + acronymes_ues_sorted: La liste des acronymes de UEs triés par acronyme croissant (dim 1) + ressembuttags: Le dictionnaire des résultats de semestres BUT (tous tags confondus) + """ + # Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2) + # etudids_sorted = etudids_sorted + # acronymes_ues = sorted([ue.acronyme for ue in selMf.ues.values()]) + semestres_id = list(ressembuttags.keys()) - # Charge les notes du semestre tag - sem_tag = self.ressembuttags[frmsem_id] - moys_tag = sem_tag.moyennes_tags[tag] - notes = moys_tag.matrice_notes # dataframe etudids x ues - acronymes_ues_sem = list( - notes.columns - ) # les acronymes des UEs du semestre tag + dfs = {} - # UEs communes à celles du SemTag (celles du dernier semestre du RCS) - ues_communes = list(set(acronymes_ues_sorted) & set(acronymes_ues_sem)) + for frmsem_id in semestres_id: + # Partant d'un dataframe vierge + df = pd.DataFrame(np.nan, index=etudids_sorted, columns=acronymes_ues_sorted) - # Etudiants communs - etudids_communs = df.index.intersection(notes.index) + # Charge les notes du semestre tag + sem_tag = ressembuttags[frmsem_id] + moys_tag = sem_tag.moyennes_tags[tag] + notes = moys_tag.matrice_notes # dataframe etudids x ues - # Recopie - df.loc[etudids_communs, ues_communes] = notes.loc[ - etudids_communs, ues_communes + # les étudiants et les acronymes communs + etudids_communs, acronymes_communs = pe_comp.find_index_and_columns_communs( + df, notes + ) + + # Recopie + df.loc[etudids_communs, acronymes_communs] = notes.loc[ + etudids_communs, acronymes_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 + + """Réunit les notes sous forme d'un cube etudids x ues x semestres""" + 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 dfs, etudids_x_ues_x_semestres + + +def compute_masques_ues_cube( + etudids_sorted: list[int], + acronymes_ues_sorted: list[str], + ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag], + formsemestre_id_final: int, +) -> (pd.DataFrame, np.array): + """Construit le cube traduisant le masque des UEs à prendre en compte dans le calcul + des moyennes, en utilisant le df capitalisations de chaque ResSemBUTTag + + Ce masque contient : 1 si la note doit être prise en compte ; 0 sinon + + Args: + etudids_sorted: La liste des etudids triés par ordre croissant (dim 0) + acronymes_ues_sorted: La liste des acronymes de UEs triés par acronyme croissant (dim 1) + # ues_inscr_parcours_df: Le dataFrame des inscriptions au UE en fonction du parcours + ressembuttags: Le dictionnaire des résultats de semestres BUT (tous tags confondus) + formsemestre_id_final: L'identifiant du formsemestre_id_final (dont il faut forcément prendre en compte les coeffs) + """ + # Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2) + # etudids_sorted = etudids_sorted + # acronymes_ues = sorted([ue.acronyme for ue in selMf.ues.values()]) + semestres_id = list(ressembuttags.keys()) + + dfs = {} + + for frmsem_id in semestres_id: + # Partant d'un dataframe contenant des 1.0 + if frmsem_id == formsemestre_id_final: + df = pd.DataFrame(1.0, index=etudids_sorted, columns=acronymes_ues_sorted) + else: # semestres redoublés + df = pd.DataFrame(0.0, index=etudids_sorted, columns=acronymes_ues_sorted) + + # Traitement des capitalisations + capitalisations = ressembuttags[frmsem_id].capitalisations + capitalisations = capitalisations.replace(True, 1.0).replace(False, 0.0) + + # Met à 0 les coeffs des UEs non capitalisées : 1.0*False => 0.0 + etudids_communs, acronymes_communs = pe_comp.find_index_and_columns_communs( + df, capitalisations + ) + + df.loc[etudids_communs, acronymes_communs] = capitalisations.loc[ + etudids_communs, acronymes_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 - # Stocke le df - dfs[frmsem_id] = df - - """Réunit les notes sous forme d'un cube etudids x ues x semestres""" - 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 + """Réunit les notes sous forme d'un cube etudids x ues x semestres""" + 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 dfs, etudids_x_ues_x_semestres def compute_notes_ues( set_cube: np.array, - etudids: list, - acronymes_ues: list, + masque_cube: np.array, + etudids_sorted: list, + acronymes_ues_sorted: list, inscr_mask: np.array, ): """Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE @@ -217,8 +329,10 @@ def compute_notes_ues( Args: set_cube: notes moyennes aux modules ndarray (semestre_ids x etudids x UEs), des floats avec des NaN - etudids: liste des étudiants (dim. 0 du cube) - acronymes_ues: liste des acronymes des ues (dim. 1 du cube) + masque_cube: masque indiquant si la note doit être prise en compte ndarray + (semestre_ids x etudids x UEs), des 1.0 ou des 0.0 + etudids_sorted: liste des étudiants (dim. 0 du cube) trié par etudid + acronymes_ues_sorted: liste des acronymes des ues (dim. 1 du cube) trié par acronyme inscr_mask: masque etudids x UE traduisant les inscriptions des étudiants aux UE (du semestre terminal) Returns: @@ -227,18 +341,21 @@ def compute_notes_ues( """ nb_etuds, nb_ues, nb_semestres = set_cube.shape nb_etuds_mask, nb_ues_mask = inscr_mask.shape - assert nb_etuds == len(etudids) - assert nb_ues == len(acronymes_ues) + assert nb_etuds == len(etudids_sorted) + assert nb_ues == len(acronymes_ues_sorted) assert nb_etuds == nb_etuds_mask assert nb_ues == nb_ues_mask - # Quelles entrées du cube contiennent des notes ? - mask = ~np.isnan(set_cube) - - # Entrées à garder dans le cube en fonction du mask d'inscription + # Entrées à garder dans le cube en fonction du masque d'inscription aux UEs du parcours inscr_mask_3D = np.stack([inscr_mask] * nb_semestres, axis=-1) set_cube = set_cube * inscr_mask_3D + # Entrées à garder en fonction des UEs capitalisées ou non + set_cube = set_cube * masque_cube + + # Quelles entrées du cube contiennent des notes ? + mask = ~np.isnan(set_cube) + # Enlève les NaN du cube pour les entrées manquantes : NaN -> -1.0 set_cube_no_nan = np.nan_to_num(set_cube, nan=-1.0) @@ -252,8 +369,8 @@ def compute_notes_ues( # Le dataFrame etud_moy_tag_df = pd.DataFrame( etud_moy, - index=etudids, # les etudids - columns=acronymes_ues, # les tags + index=etudids_sorted, # les etudids + columns=acronymes_ues_sorted, # les tags ) etud_moy_tag_df.fillna(np.nan) diff --git a/app/pe/pe_tabletags.py b/app/pe/pe_tabletags.py index ea789ef5..a14f593b 100644 --- a/app/pe/pe_tabletags.py +++ b/app/pe/pe_tabletags.py @@ -52,9 +52,7 @@ class TableTag(object): # Les étudiants # self.etuds: list[Identite] = None # A venir """Les étudiants""" - # self.etats_civils: dict[int, Identite] = None - """Les états civils""" - # self.etudids: list[int] = None + # self.etudids: list[int] = {} """Les etudids""" def add_etuds(self, etuds: list[Identite]): @@ -64,8 +62,7 @@ class TableTag(object): etuds: la liste des identités de l'étudiant """ # self.etuds = etuds - self.etats_civils = {etud.etudid: etud.etat_civil for etud in self.etuds} - self.etudids = list(self.etats_civils.keys()) + self.etudids = list({etud.etudid for etud in etuds}) def get_all_tags(self): """Liste des tags de la table, triée par ordre alphabétique, @@ -88,8 +85,8 @@ class TableTag(object): Le dataframe des notes et des classements """ - etudiants = self.etats_civils - df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"]) + etudiants = {etud.etudid: [etud.nom, etud.prenom] for etud in self.etuds} + df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom", "prenom"]) tags_tries = self.get_all_tags() for tag in tags_tries: @@ -104,7 +101,7 @@ class TableTag(object): df = df.join(moy_gen.synthese["notes"].rename(f"Moy {tag} (gen)")) df = df.join(moy_gen.synthese["classements"].rename(f"Class {tag} (gen)")) - df.sort_values(by=['nom']) + df.sort_values(by=["nom", "prenom"]) return df diff --git a/app/pe/rcss/__init__.py b/app/pe/rcss/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/pe/rcss/pe_rcf.py b/app/pe/rcss/pe_rcf.py new file mode 100644 index 00000000..494a3505 --- /dev/null +++ b/app/pe/rcss/pe_rcf.py @@ -0,0 +1,57 @@ + +from app.models import FormSemestre +import app.pe.rcss.pe_rcs as pe_rcs + + +class RCF(pe_rcs.RCS): + """Modélise un ensemble de (form)semestres d'étudiants + associé à un type de regroupement cohérent de semestres + donné (par ex: 'S2', '3S', '2A'). + + Si le RCF est un semestre de type Si, stocke les + formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si + (en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants) + + Pour le RCF de type iS ou iA (par ex, 3A=S1+S2+S3), identifie + les semestres que les étudiants ont suivis pour les amener jusqu'au semestre + terminal du RCS (par ex: ici un S3). + + Ces semestres peuvent être : + + * des S1+S2+S1+S2+S3 si redoublement de la 1ère année + * des S1+S2+(année de césure)+S3 si césure, ... + + Args: + nom_rcs: Un nom du RCS (par ex: '5S') + semestre_final: Le formsemestre final du RCS + """ + + def __init__(self, nom_rcs: str, semestre_final: FormSemestre): + pe_rcs.RCS.__init__(self, nom_rcs, semestre_final) + + self.semestres_aggreges: dict[int:FormSemestre] = {} + """Formsemestres regroupés dans le RCS""" + + def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]): + """Ajout de semestres aux semestres à regrouper + + Args: + semestres: Dictionnaire ``{fid: Formsemestre)`` + """ + self.semestres_aggreges = self.semestres_aggreges | semestres + + def get_repr(self, verbose=True) -> str: + """Représentation textuelle d'un RCS + basé sur ses semestres aggrégés""" + title = f"""{self.__class__.__name__} {pe_rcs.RCS.__str__(self)}""" + if verbose: + noms = [] + for fid in self.semestres_aggreges: + semestre = self.semestres_aggreges[fid] + noms.append(f"S{semestre.semestre_id}(#{fid})") + noms = sorted(noms) + if noms: + title += " <" + "+".join(noms) + ">" + else: + title += " " + return title diff --git a/app/pe/rcss/pe_rcrcf.py b/app/pe/rcss/pe_rcrcf.py new file mode 100644 index 00000000..f44d9fb3 --- /dev/null +++ b/app/pe/rcss/pe_rcrcf.py @@ -0,0 +1,69 @@ +############################################################################## +# Module "Avis de poursuite d'étude" +# conçu et développé par Cléo Baras (IUT de Grenoble) +############################################################################## + +""" +Created on 01-2024 + +@author: barasc +""" + +import app.pe.pe_comp as pe_comp +import app.pe.rcss.pe_rcf +import app.pe.rcss.rcss_constantes + +from app.models import FormSemestre +from app.pe import pe_sxtag, pe_affichage +from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date +import app.pe.rcss.pe_rcs as pe_rcs +import app.pe.rcss.pe_rcf as pe_rcf + + + +class RCRCF: + """Modélise les RCF d'étudiants suivis par un étudiant dans + le cadre d'un RCS donné (par ex: 3S=S1+S2+S3). + + Pour rappel : un RCF (par ex. S1) combine les semestres 1 qu'a suivi + l'étudiant pour valider son S1 (1 si étudiant standard, 2 si redoublant). + + Le RCRCF 3S est donc le regroupement du RCF S1 + RCF S2 + RCF S3. + + Il est identifié par le formsemestre de S3 marquant la fin du regroupement. + + + Args: + nom_rcs: Un nom du RCS (par ex: '5S') + semestre_final: Le semestre final du RCS + """ + + def __init__(self, nom_rcs: str, semestre_final: FormSemestre): + pe_rcs.RCS.__init__(self, nom_rcs, semestre_final) + + self.rcfs_aggreges: dict[(str, int) : pe_sxtag.SxTag] = {} + """Les RCFs à aggréger""" + + def add_rcfs_a_aggreger(self, rcfs: dict[(str, int): app.pe.rcss.pe_rcf.RCF]): + """Ajout des RCFs aux RCFS à regrouper + + Args: + rcfs: Dictionnaire ``{(str,fid): RCF}`` à ajouter + """ + self.rcfs_aggreges = self.rcfs_aggreges | rcfs + + def get_repr(self, verbose=True) -> str: + """Représentation textuelle d'un RCSF + basé sur ses RCF aggrégés""" + title = f"""{self.__class__.__name__}{pe_rcs.RCS.__str__(self)}""" + if verbose: + noms = [] + for rcf_id, rcf in self.rcfs_aggreges.items(): + noms.append(rcf.get_repr(verbose=False)) + if noms: + title += " <<" + "+".join(noms) + ">>" + else: + title += " <>" + return title + + diff --git a/app/pe/rcss/pe_rcs.py b/app/pe/rcss/pe_rcs.py new file mode 100644 index 00000000..72a3669b --- /dev/null +++ b/app/pe/rcss/pe_rcs.py @@ -0,0 +1,62 @@ +############################################################################## +# Module "Avis de poursuite d'étude" +# conçu et développé par Cléo Baras (IUT de Grenoble) +############################################################################## + +""" +Created on 01-2024 + +@author: barasc +""" + +from app.models import FormSemestre +import app.pe.rcss.rcss_constantes as rcss_constantes + + + +def get_descr_rcs(nom_rcs: str) -> str: + """Renvoie la description pour les tableurs de synthèse + Excel d'un nom de RCS""" + return rcss_constantes.TYPES_RCS[nom_rcs]["descr"] + + +class RCS: + """Modélise un regroupement cohérent de semestres (formsemestre ou de Sx)""" + + def __init__(self, nom_rcs: str, semestre_final: FormSemestre): + self.nom: str = nom_rcs + """Nom du RCS""" + + self.formsemestre_final: FormSemestre = semestre_final + """FormSemestre terminal du RCS""" + + self.rang_final = self.formsemestre_final.semestre_id + """Le rang du formsemestre final""" + + self.rcs_id: (str, int) = (nom_rcs, semestre_final.formsemestre_id) + """Identifiant du RCS sous forme (nom_rcs, id du semestre_terminal)""" + + def get_formsemestre_id_final(self) -> int: + """Renvoie l'identifiant du formsemestre final du RCS + + Returns: + L'id du formsemestre final (marquant la fin) du RCS + """ + return self.formsemestre_final.formsemestre_id + + def __str__(self): + """Représentation textuelle d'un RCS""" + return f"{self.nom}[#{self.formsemestre_final.formsemestre_id}✟{self.formsemestre_final.date_fin.year}]" + + def get_repr(self, verbose=True): + """Représentation textuelle d'un RCS""" + return self.__str__() + + def __eq__(self, other): + """Egalité de RCS""" + return ( + self.nom == other.nom + and self.formsemestre_final == other.formsemestre_final + ) + + diff --git a/app/pe/rcss/rcss_constantes.py b/app/pe/rcss/rcss_constantes.py new file mode 100644 index 00000000..ee79de27 --- /dev/null +++ b/app/pe/rcss/rcss_constantes.py @@ -0,0 +1,64 @@ + +TYPES_RCS = { + "S1": { + "aggregat": ["S1"], + "descr": "Semestre 1 (S1)", + }, + "S2": { + "aggregat": ["S2"], + "descr": "Semestre 2 (S2)", + }, + "1A": { + "aggregat": ["S1", "S2"], + "descr": "BUT1 (S1+S2)", + }, + "S3": { + "aggregat": ["S3"], + "descr": "Semestre 3 (S3)", + }, + "S4": { + "aggregat": ["S4"], + "descr": "Semestre 4 (S4)", + }, + "2A": { + "aggregat": ["S3", "S4"], + "descr": "BUT2 (S3+S4)", + }, + "3S": { + "aggregat": ["S1", "S2", "S3"], + "descr": "Moyenne du semestre 1 au semestre 3 (S1+S2+S3)", + }, + "4S": { + "aggregat": ["S1", "S2", "S3", "S4"], + "descr": "Moyenne du semestre 1 au semestre 4 (S1+S2+S3+S4)", + }, + "S5": { + "aggregat": ["S5"], + "descr": "Semestre 5 (S5)", + }, + "S6": { + "aggregat": ["S6"], + "descr": "Semestre 6 (S6)", + }, + "3A": { + "aggregat": ["S5", "S6"], + "descr": "3ème année (S5+S6)", + }, + "5S": { + "aggregat": ["S1", "S2", "S3", "S4", "S5"], + "descr": "Moyenne du semestre 1 au semestre 5 (S1+S2+S3+S4+S5)", + }, + "6S": { + "aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"], + "descr": "Moyenne globale (S1+S2+S3+S4+S5+S6)", + }, +} +"""Dictionnaire détaillant les différents regroupements cohérents +de semestres (RCS), en leur attribuant un nom et en détaillant +le nom des semestres qu'ils regroupent et l'affichage qui en sera fait +dans les tableurs de synthèse. +""" + +TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS if not cle.startswith("S")] +TOUS_LES_RCS = list(TYPES_RCS.keys()) +TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")] \ No newline at end of file From 491d600bd44f29d46b9143da64ec400174365c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Mon, 19 Feb 2024 20:00:11 +0100 Subject: [PATCH 13/41] =?UTF-8?q?Finalisation=20des=20SxTags=20avec=20situ?= =?UTF-8?q?ation=20dans=20lesquels=20=C3=A9val=20du=20tag=20en=20cours?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_moytag.py | 27 +++++++--- app/pe/pe_rcstag.py | 14 ++++-- app/pe/pe_sxtag.py | 117 +++++++++++++++++++++++++++----------------- 3 files changed, 102 insertions(+), 56 deletions(-) diff --git a/app/pe/pe_moytag.py b/app/pe/pe_moytag.py index 1df487ff..d5e64a79 100644 --- a/app/pe/pe_moytag.py +++ b/app/pe/pe_moytag.py @@ -155,13 +155,12 @@ class Moyenne: return self.synthese["nb_inscrits"] > 0 - class MoyennesTag: def __init__( self, tag: str, - matrice_notes: pd.DataFrame, # etudids x colonnes - matrice_coeffs: pd.DataFrame # etudids x colonnes + matrice_notes: pd.DataFrame, # etudids x colonnes + matrice_coeffs: pd.DataFrame, # etudids x colonnes ): """Classe centralisant la synthèse des moyennes/classements d'une série d'étudiants à un tag donné, en différenciant les notes @@ -193,13 +192,30 @@ class MoyennesTag: self.moyennes[col] = Moyenne(notes) # Les moyennes générales - notes_gen = self.compute_moy_gen(self.matrice_notes, self.matrice_coeffs_moy_gen) - self.notes_gen = notes_gen + notes_gen = pd.Series(np.nan, index=self.matrice_notes.index) """Les notes générales (moyenne toutes UEs confonudes)""" + if self.has_notes(): + notes_gen = self.compute_moy_gen( + self.matrice_notes, self.matrice_coeffs_moy_gen + ) + self.notes_gen = notes_gen self.moyenne_gen = Moyenne(notes_gen) """Le dataframe retraçant les moyennes/classements/statistiques général""" + def has_notes(self): + """Détermine si les moyennes (aux UEs ou aux compétences) + ont des notes + Returns: + True si a des notes, False sinon + """ + notes = self.matrice_notes + nbre_nan = notes.isna().sum().sum() + nbre_notes_potentielles = len(notes.index) * len(notes.columns) + if nbre_nan == nbre_notes_potentielles: + return False + else: + return True def compute_moy_gen(self, moys: pd.DataFrame, coeffs: pd.DataFrame) -> pd.Series: """Calcule la moyenne générale (toutes UE/compétences confondus) @@ -225,4 +241,3 @@ class MoyennesTag: ) return moy_gen_tag - diff --git a/app/pe/pe_rcstag.py b/app/pe/pe_rcstag.py index 70d11491..94fe3051 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/pe_rcstag.py @@ -95,7 +95,13 @@ class RCSTag(TableTag): self.add_etuds(nt.etuds) # Les compétences (extraites de tous les Sxtags) + self.association_ues_comp = self.mapping_ue_competences() + print(self.association_ues_comp) + """Association indiquant pour chaque UE , quelle compétence lui correspond""" self.competences_sorted = self.do_complist() + """Liste des compétences triées""" + + """Compétences extraites de tous les SxTag aggrégés""" pe_affichage.pe_print(f"* Compétences : {', '.join(self.competences_sorted)}") @@ -164,10 +170,8 @@ class RCSTag(TableTag): coeffs = moys_tag.matrice_coeffs_moy_gen.copy() # les coeffs # Traduction des UE en compétences - association_ues_comp = self.mapping_ue_competences() - print(association_ues_comp) ues_columns_df = notes.columns - comp_associes_aux_ues = [association_ues_comp[ue] for ue in ues_columns_df] + comp_associes_aux_ues = [self.association_ues_comp[ue] for ue in ues_columns_df] notes.columns = comp_associes_aux_ues coeffs.columns = comp_associes_aux_ues @@ -224,8 +228,8 @@ class RCSTag(TableTag): def do_complist(self): """Synthétise les compétences à partir des Sxtags aggrégés""" - dict_comptences = self.mapping_ue_competences() - return sorted(set(dict_comptences.values())) + dict_competences = self.mapping_ue_competences() + return sorted(set(dict_competences.values())) def compute_notes_competences( diff --git a/app/pe/pe_sxtag.py b/app/pe/pe_sxtag.py index 132824df..208d9629 100644 --- a/app/pe/pe_sxtag.py +++ b/app/pe/pe_sxtag.py @@ -143,59 +143,72 @@ class SxTag(TableTag): self.ressembuttags, self.fid_final, ) - # Affichage pour debug - for etud in self.etuds: - cap = [] - for frmsem_id in self.ressembuttags: - if frmsem_id != self.fid_final: - for accr in self.acronymes_ues_sorted: - if self.masque_df[frmsem_id].loc[etud.etudid, accr] > 0.0: - cap += [accr] - if cap: - pe_affichage.pe_print( - f" ⚠ Capitalisation de {etud.etat_civil} : {', '.join(cap)}" - ) + self._aff_capitalisations() for tag in self.tags_sorted: - # Cube de note etudids x UEs - notes_df, notes_cube = compute_notes_ues_cube( - tag, self.etudids_sorted, self.acronymes_ues_sorted, self.ressembuttags - ) + # Y-a-t-il des notes ? + if not self.has_notes(tag): + pe_affichage.pe_print(f"> MoyTag 🏷{tag} actuellement sans ◯ notes") + matrice_moys_ues = pd.DataFrame(np.nan, index=self.etudids_sorted, columns=self.acronymes_ues_sorted) + else: + # Cube de note etudids x UEs + notes_df, notes_cube = compute_notes_ues_cube( + tag, + self.etudids_sorted, + self.acronymes_ues_sorted, + self.ressembuttags, + ) + # self.ues_inscr_parcours = ~np.isnan(self.matrice_coeffs.to_numpy()) + # inscr_mask = self.ues_inscr_parcours + # Calcule des moyennes sous forme d'un dataframe + inscr_mask = ~np.isnan(self.ues_inscr_parcours_df.to_numpy()) + matrice_moys_ues: pd.DataFrame = compute_notes_ues( + notes_cube, + masque_cube, + self.etudids_sorted, + self.acronymes_ues_sorted, + inscr_mask, + ) + # Les profils d'ects (pour debug) + profils_ects = [] + for i in self.matrice_coeffs_moy_gen.index: + val = tuple(self.matrice_coeffs_moy_gen.loc[i].fillna("x")) + if tuple(val) not in profils_ects: + profils_ects.append(tuple(val)) + pe_affichage.pe_print( + f"> MoyTag 🏷{tag} avec " + + f"ues={self.acronymes_ues_sorted} " + + f"ects={profils_ects}" + ) - # self.ues_inscr_parcours = ~np.isnan(self.matrice_coeffs.to_numpy()) - # inscr_mask = self.ues_inscr_parcours - - # Calcule des moyennes sous forme d'un dataframe - inscr_mask = ~np.isnan(self.ues_inscr_parcours_df.to_numpy()) - matrice_moys_ues: pd.DataFrame = compute_notes_ues( - notes_cube, - masque_cube, - self.etudids_sorted, - self.acronymes_ues_sorted, - inscr_mask, - ) - - # Les profils d'ects (pour debug) - profils_ects = [] - for i in self.matrice_coeffs_moy_gen.index: - val = tuple(self.matrice_coeffs_moy_gen.loc[i].fillna("x")) - if tuple(val) not in profils_ects: - profils_ects.append(tuple(val)) - - # Les moyennes + # Les moyennes au tag self.moyennes_tags[tag] = MoyennesTag( - tag, matrice_moys_ues, self.matrice_coeffs_moy_gen - ) + tag, matrice_moys_ues, self.matrice_coeffs_moy_gen + ) - pe_affichage.pe_print( - f"> MoyTag 🏷{tag} avec " - + f"ues={self.acronymes_ues_sorted} " - + f"ects={profils_ects}" - ) + + def has_notes(self, tag): + """Détermine si le SxTag, pour un tag donné, est en cours d'évaluation. + Si oui, n'a pas (encore) de notes dans le resformsemestre final. + + Args: + tag: Le tag visé + + Returns: + True si a des notes, False sinon + """ + moy_tag_dernier_sem = self.ressembuttag_final.moyennes_tags[tag] + notes = moy_tag_dernier_sem.matrice_notes + nbre_nan = notes.isna().sum().sum() + nbre_notes_potentielles = len(notes.index) * len(notes.columns) + if nbre_nan == nbre_notes_potentielles: + return False + else: + return True def __eq__(self, other): """Egalité de 2 SxTag sur la base de leur identifiant""" @@ -210,6 +223,20 @@ class SxTag(TableTag): # affichage = [str(fid) for fid in self.ressembuttags] return f"{self.sxtag_id[0]}Tag (#{self.fid_final})" + def _aff_capitalisations(self): + """Affichage des capitalisations du sxtag pour debug""" + for etud in self.etuds: + cap = [] + for frmsem_id in self.ressembuttags: + if frmsem_id != self.fid_final: + for accr in self.acronymes_ues_sorted: + if self.masque_df[frmsem_id].loc[etud.etudid, accr] > 0.0: + cap += [accr] + if cap: + pe_affichage.pe_print( + f" ⚠ Capitalisation de {etud.etat_civil} : {', '.join(cap)}" + ) + def compute_notes_ues_cube( tag, etudids_sorted, acronymes_ues_sorted, ressembuttags @@ -291,7 +318,7 @@ def compute_masques_ues_cube( # Partant d'un dataframe contenant des 1.0 if frmsem_id == formsemestre_id_final: df = pd.DataFrame(1.0, index=etudids_sorted, columns=acronymes_ues_sorted) - else: # semestres redoublés + else: # semestres redoublés df = pd.DataFrame(0.0, index=etudids_sorted, columns=acronymes_ues_sorted) # Traitement des capitalisations From 8de1a44583c4d455c52332e177a014f059269fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Mon, 19 Feb 2024 20:12:49 +0100 Subject: [PATCH 14/41] =?UTF-8?q?Corrige=20tri=20etuds/comp=C3=A9tences=20?= =?UTF-8?q?dans=20traduction=20SxTag=20->=20RSCTag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_rcstag.py | 54 +++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/app/pe/pe_rcstag.py b/app/pe/pe_rcstag.py index 94fe3051..882044a9 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/pe_rcstag.py @@ -43,7 +43,7 @@ import numpy as np import app.pe.rcss.pe_rcs as pe_rcs import app.pe.rcss.pe_rcrcf as pe_rcrcf import app.pe.pe_sxtag as pe_sxtag - +import app.pe.pe_comp as pe_comp from app.pe.pe_tabletags import TableTag from app.pe.pe_moytag import MoyennesTag @@ -93,16 +93,16 @@ class RCSTag(TableTag): # Les étudiants (etuds, états civils & etudis) self.etuds = nt.etuds self.add_etuds(nt.etuds) + self.etudids_sorted = sorted(self.etudids) + """Etudids triés""" # Les compétences (extraites de tous les Sxtags) self.association_ues_comp = self.mapping_ue_competences() - print(self.association_ues_comp) """Association indiquant pour chaque UE , quelle compétence lui correspond""" + pe_affichage.pe_print(f"* Association UEs -> compétences : {self.association_ues_comp}") + self.competences_sorted = self.do_complist() - """Liste des compétences triées""" - - - """Compétences extraites de tous les SxTag aggrégés""" + """Compétences (triées) extraites de tous les SxTag aggrégés""" pe_affichage.pe_print(f"* Compétences : {', '.join(self.competences_sorted)}") # Les tags @@ -116,11 +116,11 @@ class RCSTag(TableTag): for tag in self.tags_sorted: # Cube de note - notes_cube, coeffs_cube = self.compute_notes_comps_cube(tag) + notes_cube, coeffs_cube = self.compute_notes_comps_cube(tag, self.etudids_sorted, self.competences_sorted) # Calcule des moyennes/coeffs sous forme d'un dataframe""" moys_competences, coeffs_competences = compute_notes_competences( - notes_cube, coeffs_cube, self.etudids, self.competences_sorted + notes_cube, coeffs_cube, self.etudids_sorted, self.competences_sorted ) # Les moyennes @@ -139,20 +139,25 @@ class RCSTag(TableTag): else: return f"{self.__class__.__name__} ({self.rcs_id})" - def compute_notes_comps_cube(self, tag): + def compute_notes_comps_cube(self, tag, etudids_sorted: list[int], competences_sorted: list[str]): """Pour un tag donné, construit : * le cube de notes (etudid x competences x SxTag) nécessaire au calcul des moyennes, en remplaçant les données d'UE (obtenus du SxTag) par les compétences * le cube de coeffs (etudid x competences x SxTag) (traduisant les inscriptions) appliqué au calcul des différents SxTag + + Args: + tag: Le tag visé + etudids_sorted: Les etudis triés + competences_sorted: Les compétences triées """ # nb_tags = len(self.tags_sorted) # nb_etudiants = len(self.etuds) # nb_semestres = len(self.semestres_tags_aggreges) # Index du cube (etudids -> dim 0, tags -> dim 1) - etudids = [etud.etudid for etud in self.etuds] - competences_sorted = self.competences_sorted + # etudids = [etud.etudid for etud in self.etuds] + # competences_sorted = self.competences_sorted sxstags_ids = list(self.sxstags.keys()) notes_dfs = {} @@ -160,8 +165,8 @@ class RCSTag(TableTag): for sxtag_id, sxtag in self.sxstags.items(): # Partant d'un dataframe vierge - notes_df = pd.DataFrame(np.nan, index=etudids, columns=competences_sorted) - coeffs_df = pd.DataFrame(np.nan, index=etudids, columns=competences_sorted) + notes_df = pd.DataFrame(np.nan, index=etudids_sorted, columns=competences_sorted) + coeffs_df = pd.DataFrame(np.nan, index=etudids_sorted, columns=competences_sorted) moys_tag = sxtag.moyennes_tags[tag] @@ -175,11 +180,8 @@ class RCSTag(TableTag): notes.columns = comp_associes_aux_ues coeffs.columns = comp_associes_aux_ues - # Compétences communes - comp_communes = list(set(competences_sorted) & set(comp_associes_aux_ues)) - - # Etudiants communs - etudids_communs = notes_df.index.intersection(notes.index) + # Les étudiants et les compétences communes + etudids_communs, comp_communes = pe_comp.find_index_and_columns_communs(notes_df, notes) # Recopie des notes et des coeffs notes_df.loc[etudids_communs, comp_communes] = notes.loc[ @@ -233,7 +235,7 @@ class RCSTag(TableTag): def compute_notes_competences( - set_cube: np.array, coeff_cube: np.array, etudids: list, competences: list + set_cube: np.array, coeff_cube: np.array, etudids_sorted: list, competences_sorted: list ): """Calcule: * la moyenne par compétences à un tag donné sur plusieurs semestres (partant du set_cube). @@ -248,16 +250,16 @@ def compute_notes_competences( set_cube: notes moyennes aux modules ndarray (etuds x UEs|compétences x sxtags), des floats avec des NaN coeffs_cube: somme des coeffs impliqués dans la moyennes - etudids: liste des étudiants (dim. 0 du cube) - competences: list + etudids_sorted: liste des étudiants (dim. 0 du cube) + competences_sorted: list tags: liste des tags (dim. 1 du cube) Returns: Un DataFrame avec pour columns les moyennes par tags, et pour rows les etudid """ nb_etuds, nb_comps, nb_semestres = set_cube.shape - assert nb_etuds == len(etudids) - assert nb_comps == len(competences) + assert nb_etuds == len(etudids_sorted) + assert nb_comps == len(competences_sorted) # Quelles entrées du cube contiennent des notes ? mask = ~np.isnan(set_cube) @@ -275,12 +277,12 @@ def compute_notes_competences( # Le dataFrame des notes moyennes etud_moy_tag_df = pd.DataFrame( etud_moy_tag, - index=etudids, # les etudids - columns=competences, # les competences + index=etudids_sorted, # les etudids + columns=competences_sorted, # les competences ) etud_moy_tag_df.fillna(np.nan) - coeffs_df = pd.DataFrame(coeff_tag, index=etudids, columns=competences) + coeffs_df = pd.DataFrame(coeff_tag, index=etudids_sorted, columns=competences_sorted) coeffs_df.fillna(np.nan) return etud_moy_tag_df, coeffs_df From 83059cd9955795ccbd063a04da91873c26c8d6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 20 Feb 2024 09:13:19 +0100 Subject: [PATCH 15/41] =?UTF-8?q?Relecture=20+=20am=C3=A9liorations=20dive?= =?UTF-8?q?rses=20(dont=20tri=20syst=C3=A9matique=20par=20etudids=5Fsorted?= =?UTF-8?q?,=20acronymes=5Fsorted,=20competences=5Fsorted)=20des=20datafra?= =?UTF-8?q?mes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_jury.py | 37 ++++- app/pe/pe_moytag.py | 18 +-- app/pe/pe_rcss_jury.py | 3 +- app/pe/pe_rcstag.py | 290 ++++++++++++++++++++++++----------- app/pe/pe_ressemtag.py | 161 +++++++++++++------ app/pe/pe_sxtag.py | 172 +++++++++++---------- tests/unit/yaml_setup_but.py | 2 +- 7 files changed, 442 insertions(+), 241 deletions(-) diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 323080d3..168cdb9b 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -60,6 +60,7 @@ from app.pe.pe_rcstag import RCSTag from app.pe.pe_ressemtag import ResSemBUTTag from app.pe.pe_interclasstag import RCSInterclasseTag import app.pe.pe_rcss_jury as pe_rcss_jury +import app.pe.rcss.rcss_constantes as rcss_constantes class JuryPE(object): @@ -107,6 +108,7 @@ class JuryPE(object): self._gen_xls_diplomes(zipfile) self._gen_xls_ressembuttags(zipfile) self._gen_rcss() + self._gen_rcsf() self._gen_xls_sxtags(zipfile) self._gen_rcrcfs() self._gen_xls_rcrcss_tags(zipfile) @@ -166,11 +168,14 @@ class JuryPE(object): with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: + onglets = [] for res_sem_tag in self.ressembuttags.values(): - onglet = res_sem_tag.get_repr(verbose=False) + onglet = res_sem_tag.get_repr(verbose=True) + onglets += [] df = res_sem_tag.df_moyennes_et_classements() # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) + pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) self.add_file_to_zip( @@ -190,16 +195,19 @@ class JuryPE(object): self.rcss_jury.cree_rcss(self.etudiants) - def _gen_xls_sxtags(self, zipfile: ZipFile): - """Génère les semestres taggués en s'appuyant sur les RCS de type Sx (pour - identifier les redoublements impactant les semestres taggués). - """ + def _gen_rcsf(self): + """Génère les RCF, regroupement de semestres de type Sx pour préparer + le calcul des moyennes par Sx""" # Génère les regroupements de semestres de type Sx pe_affichage.pe_print( "*** Génère les RCSValid (RCS de même Sx donnant lieu à validation du semestre)" ) self.rcss_jury.cree_rcfs(self.etudiants) + def _gen_xls_sxtags(self, zipfile: ZipFile): + """Génère les semestres taggués en s'appuyant sur les RCF de type Sx (pour + identifier les redoublements impactant les semestres taggués). + """ # Génère les moyennes des RCS de type Sx pe_affichage.pe_print("*** Calcule les moyennes des SxTag") @@ -216,11 +224,15 @@ class JuryPE(object): with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: + onglets = [] for sxtag in self.sxtags.values(): onglet = sxtag.get_repr(verbose=False) + onglets += [onglet] df = sxtag.df_moyennes_et_classements() # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) + pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") + output.seek(0) self.add_file_to_zip( @@ -269,11 +281,14 @@ class JuryPE(object): with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: + onglets = [] for rcs_tag in self.rcss_tags.values(): onglet = rcs_tag.get_repr(verbose=False) + onglets += [onglet] df = rcs_tag.df_moyennes_et_classements() # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) + pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) self.add_file_to_zip( @@ -296,12 +311,16 @@ class JuryPE(object): with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: + onglets = [] for interclass_tag in self.interclassements_taggues.values(): if interclass_tag.significatif: # Avec des notes onglet = interclass_tag.get_repr() + onglets += [onglet] df = interclass_tag.df_moyennes_et_classements() # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) + pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") + output.seek(0) self.add_file_to_zip( @@ -322,9 +341,12 @@ class JuryPE(object): with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: + onglets = [] for onglet, df in self.synthese.items(): + onglets += [onglet] # écriture dans l'onglet: df.to_excel(writer, onglet, index=True, header=True) + pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) self.add_file_to_zip( @@ -342,9 +364,12 @@ class JuryPE(object): with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated output, engine="openpyxl" ) as writer: + onglets = [] for onglet, df in synthese.items(): + onglets += [onglet] # écriture dans l'onglet: df.to_excel(writer, onglet, index=True, header=True) + pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) self.add_file_to_zip( @@ -433,7 +458,7 @@ class JuryPE(object): # Ajout des aggrégats for aggregat in pe_rcs.TOUS_LES_RCS: - descr = app.pe.rcss.constantes.TYPES_RCS[aggregat]["descr"] + descr = rcss_constantes.TYPES_RCS[aggregat]["descr"] # Les trajectoires (tagguées) suivies par les étudiants pour l'aggrégat et le tag # considéré diff --git a/app/pe/pe_moytag.py b/app/pe/pe_moytag.py index d5e64a79..1029c66c 100644 --- a/app/pe/pe_moytag.py +++ b/app/pe/pe_moytag.py @@ -3,9 +3,7 @@ import pandas as pd from app import comp from app.comp.moy_sem import comp_ranks_series -from app.models import UniteEns from app.pe import pe_affichage -from app.scodoc.codes_cursus import UE_SPORT class Moyenne: @@ -24,12 +22,12 @@ class Moyenne: """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, + * 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""" @@ -171,7 +169,7 @@ class MoyennesTag: tag: Un tag matrice_notes: Les moyennes (etudid x acronymes_ues ou etudid x compétences) aux différentes UEs ou compétences matrice_coeffs: Les coeff à appliquer pour le calcul de la moyenne générale - # notes_gen: Une série de notes (moyenne) sous forme d'un pd.Series() (toutes UEs confondues) + # 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""" @@ -207,7 +205,7 @@ class MoyennesTag: ont des notes Returns: - True si a des notes, False sinon + True si la moytag a des notes, False sinon """ notes = self.matrice_notes nbre_nan = notes.isna().sum().sum() diff --git a/app/pe/pe_rcss_jury.py b/app/pe/pe_rcss_jury.py index e514e5bb..58697cb2 100644 --- a/app/pe/pe_rcss_jury.py +++ b/app/pe/pe_rcss_jury.py @@ -152,7 +152,8 @@ class RCSsJuryPE: # Ajout du RCRCF if rcf_id not in self.rcrcfs: - self.rcrcfs[rcf_id] = pe_rcrcf.RCRCF(rcf_id, rcf.formsemestre_final) + rcf_nom = rcf_id[0] + self.rcrcfs[rcf_id] = pe_rcrcf.RCRCF(rcf_nom, rcf.formsemestre_final) rcrcf = self.rcrcfs[rcf_id] # Ajout des RCFs au RCRCF diff --git a/app/pe/pe_rcstag.py b/app/pe/pe_rcstag.py index 882044a9..43425941 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/pe_rcstag.py @@ -37,6 +37,7 @@ Created on Fri Sep 9 09:15:05 2016 """ from app.comp.res_sem import load_formsemestre_results +from app.models import FormSemestre from app.pe import pe_affichage import pandas as pd import numpy as np @@ -49,7 +50,7 @@ from app.pe.pe_moytag import MoyennesTag class RCSTag(TableTag): - def __init__(self, rcrcf: pe_rcs.RCS, sxstags: dict[(str, int): pe_sxtag.SxTag]): + def __init__(self, rcrcf: pe_rcs.RCS, sxstags: dict[(str, int) : pe_sxtag.SxTag]): """Calcule les moyennes par tag (orientées compétences) d'un regroupement de SxTag (RCRCF), pour extraire les classements par tag pour un @@ -71,17 +72,18 @@ class RCSTag(TableTag): self.nom = self.get_repr() """Représentation textuelle du RCS taggué""" - self.formsemestre_terminal = rcrcf.formsemestre_final - """Le formsemestre terminal""" + # Les données du semestre final + self.formsemestre_terminal: FormSemestre = rcrcf.formsemestre_final + """Le semestre final""" + self.fid_final: int = rcrcf.formsemestre_final.formsemestre_id + """Le fid du semestre final""" + # Affichage pour debug pe_affichage.pe_print(f"-> {self.get_repr(verbose=True)}") - # Les résultats du formsemestre terminal - nt = load_formsemestre_results(self.formsemestre_terminal) - + # Les données aggrégés (RCRCF + SxTags self.rcfs_aggreges = rcrcf.rcfs_aggreges """Les RCFs aggrégés""" - self.sxstags = {} """Les SxTag associés aux RCF aggrégés""" try: @@ -91,41 +93,57 @@ class RCSTag(TableTag): raise ValueError("Semestres SxTag manquants") # Les étudiants (etuds, états civils & etudis) - self.etuds = nt.etuds - self.add_etuds(nt.etuds) + sxtag_final = self.sxstags[self.rcs_id] + self.etuds = sxtag_final.etuds + """Les étudiants (extraits du semestre final)""" + self.add_etuds(self.etuds) self.etudids_sorted = sorted(self.etudids) - """Etudids triés""" + """Les étudids triés""" # Les compétences (extraites de tous les Sxtags) - self.association_ues_comp = self.mapping_ue_competences() - """Association indiquant pour chaque UE , quelle compétence lui correspond""" - pe_affichage.pe_print(f"* Association UEs -> compétences : {self.association_ues_comp}") - - self.competences_sorted = self.do_complist() - """Compétences (triées) extraites de tous les SxTag aggrégés""" + self.acronymes_ues_to_competences = self._do_acronymes_to_competences() + """L'association acronyme d'UEs -> compétence (extraites des SxTag aggrégés)""" + pe_affichage.pe_print( + f"* Association UEs -> compétences : {self.acronymes_ues_to_competences}" + ) + self.competences_sorted = sorted(self.acronymes_ues_to_competences.values()) + """Compétences (triées par nom, extraites des SxTag aggrégés)""" pe_affichage.pe_print(f"* Compétences : {', '.join(self.competences_sorted)}") # Les tags - self.tags_sorted = self.do_taglist() + self.tags_sorted = self._do_taglist() """Tags extraits de tous les SxTag aggrégés""" pe_affichage.pe_print(f"* Tags : {', '.join(self.tags_sorted)}") # Les moyennes self.moyennes_tags: dict[str, MoyennesTag] = {} """Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)""" - for tag in self.tags_sorted: - # Cube de note - notes_cube, coeffs_cube = self.compute_notes_comps_cube(tag, self.etudids_sorted, self.competences_sorted) - - # Calcule des moyennes/coeffs sous forme d'un dataframe""" - moys_competences, coeffs_competences = compute_notes_competences( - notes_cube, coeffs_cube, self.etudids_sorted, self.competences_sorted + # Cube de notes (etudids_sorted x compétences_sorted x sxstags) + notes_df, notes_cube = self.compute_notes_comps_cube( + tag, self.etudids_sorted, self.competences_sorted, self.sxstags + ) + # Calcule des moyennes/coeffs sous forme d'un dataframe""" + moys_competences = compute_notes_competences( + notes_cube, self.etudids_sorted, self.competences_sorted + ) + # Cube de coeffs pour la moyenne générale, + # traduisant les inscriptions des étudiants aux UEs (etudids_sorted x compétences_sorted x sxstags) + coeffs_df, coeffs_cube = self.compute_coeffs_comps_cube( + tag, + self.etudids_sorted, + self.competences_sorted, + self.sxstags, + ) + # Calcule la synthèse des coefficients à prendre en compte pour la moyenne + # générale + matrice_coeffs_moy_gen = compute_coeffs_competences( + coeffs_cube, notes_cube, self.etudids_sorted, self.competences_sorted + ) + # Mémorise les moyennes et les coeff associés + self.moyennes_tags[tag] = MoyennesTag( + tag, moys_competences, matrice_coeffs_moy_gen ) - - # Les moyennes - self.moyennes_tags[tag] = MoyennesTag(tag, moys_competences, - coeffs_competences) def __eq__(self, other): """Egalité de 2 RCS taggués sur la base de leur identifiant""" @@ -139,107 +157,200 @@ class RCSTag(TableTag): else: return f"{self.__class__.__name__} ({self.rcs_id})" - def compute_notes_comps_cube(self, tag, etudids_sorted: list[int], competences_sorted: list[str]): - """Pour un tag donné, construit : - * le cube de notes (etudid x competences x SxTag) nécessaire au calcul des moyennes, - en remplaçant les données d'UE (obtenus du SxTag) par les compétences - * le cube de coeffs (etudid x competences x SxTag) (traduisant les inscriptions) - appliqué au calcul des différents SxTag + def compute_notes_comps_cube( + self, + tag, + etudids_sorted: list[int], + competences_sorted: list[str], + sxstags: dict[(str, int) : pe_sxtag.SxTag], + ): + """Pour un tag donné, construit le cube de notes (etudid x competences x SxTag) + nécessaire au calcul des moyennes, + en remplaçant les données d'UE (obtenus du SxTag) par les compétences Args: tag: Le tag visé - etudids_sorted: Les etudis triés - competences_sorted: Les compétences triées + etudids_sorted: Les etudis triés (dim 0) + competences_sorted: Les compétences triées (dim 1) + sxstags: Les SxTag à réunir """ - # nb_tags = len(self.tags_sorted) - # nb_etudiants = len(self.etuds) - # nb_semestres = len(self.semestres_tags_aggreges) - - # Index du cube (etudids -> dim 0, tags -> dim 1) - # etudids = [etud.etudid for etud in self.etuds] - # competences_sorted = self.competences_sorted - sxstags_ids = list(self.sxstags.keys()) - notes_dfs = {} - coeffs_dfs = {} - for sxtag_id, sxtag in self.sxstags.items(): + for sxtag_id, sxtag in sxstags.items(): # Partant d'un dataframe vierge - notes_df = pd.DataFrame(np.nan, index=etudids_sorted, columns=competences_sorted) - coeffs_df = pd.DataFrame(np.nan, index=etudids_sorted, columns=competences_sorted) - + notes_df = pd.DataFrame( + np.nan, index=etudids_sorted, columns=competences_sorted + ) + # Charge les notes du semestre tag (copie car changement de nom de colonnes à venir) moys_tag = sxtag.moyennes_tags[tag] - - # Charge les notes et les coeffs du semestre tag notes = moys_tag.matrice_notes.copy() # avec une copie - coeffs = moys_tag.matrice_coeffs_moy_gen.copy() # les coeffs - # Traduction des UE en compétences - ues_columns_df = notes.columns - comp_associes_aux_ues = [self.association_ues_comp[ue] for ue in ues_columns_df] - notes.columns = comp_associes_aux_ues - coeffs.columns = comp_associes_aux_ues + # Traduction des acronymes d'UE en compétences + acronymes_ues_columns = notes.columns + acronymes_to_comps = [ + self.acronymes_ues_to_competences[acro] + for acro in acronymes_ues_columns + ] + notes.columns = acronymes_to_comps # Les étudiants et les compétences communes - etudids_communs, comp_communes = pe_comp.find_index_and_columns_communs(notes_df, notes) + etudids_communs, comp_communes = pe_comp.find_index_and_columns_communs( + notes_df, notes + ) # Recopie des notes et des coeffs notes_df.loc[etudids_communs, comp_communes] = notes.loc[ etudids_communs, comp_communes ] + + # Supprime tout ce qui n'est pas numérique + # for col in notes_df.columns: + # notes_df[col] = pd.to_numeric(notes_df[col], errors="coerce") + + # Stocke les dfs + notes_dfs[sxtag_id] = notes_df + + """Réunit les notes sous forme d'un cube etudids x competences x semestres""" + sxtag_x_etudids_x_comps = [notes_dfs[sxtag_id] for sxtag_id in sxstags] + notes_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1) + + return notes_dfs, notes_etudids_x_comps_x_sxtag + + def compute_coeffs_comps_cube( + self, + tag, + etudids_sorted: list[int], + competences_sorted: list[str], + sxstags: dict[(str, int) : pe_sxtag.SxTag], + ): + """Pour un tag donné, construit + le cube de coeffs (etudid x competences x SxTag) (traduisant les inscriptions + des étudiants aux UEs en fonction de leur parcours) + qui s'applique aux différents SxTag + en remplaçant les données d'UE (obtenus du SxTag) par les compétences + + Args: + tag: Le tag visé + etudids_sorted: Les etudis triés + competences_sorted: Les compétences triées + sxstags: Les SxTag à réunir + """ + coeffs_dfs = {} + + for sxtag_id, sxtag in sxstags.items(): + # Partant d'un dataframe vierge + coeffs_df = pd.DataFrame( + np.nan, index=etudids_sorted, columns=competences_sorted + ) + + moys_tag = sxtag.moyennes_tags[tag] + + # Charge les notes et les coeffs du semestre tag + coeffs = moys_tag.matrice_coeffs_moy_gen.copy() # les coeffs + + # Traduction des acronymes d'UE en compétences + acronymes_ues_columns = coeffs.columns + acronymes_to_comps = [ + self.acronymes_ues_to_competences[acro] + for acro in acronymes_ues_columns + ] + coeffs.columns = acronymes_to_comps + + # Les étudiants et les compétences communes + etudids_communs, comp_communes = pe_comp.find_index_and_columns_communs( + coeffs_df, coeffs + ) + + # Recopie des notes et des coeffs coeffs_df.loc[etudids_communs, comp_communes] = coeffs.loc[ etudids_communs, comp_communes ] - # Supprime tout ce qui n'est pas numérique - for col in notes_df.columns: - notes_df[col] = pd.to_numeric(notes_df[col], errors="coerce") - # Stocke les dfs - notes_dfs[sxtag_id] = notes_df coeffs_dfs[sxtag_id] = coeffs_df - """Réunit les notes sous forme d'un cube etudids x competences x semestres""" - sxtag_x_etudids_x_comps = [notes_dfs[fid].values for fid in notes_dfs] - notes_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1) - """Réunit les coeffs sous forme d'un cube etudids x competences x semestres""" - sxtag_x_etudids_x_comps = [coeffs_dfs[fid].values for fid in notes_dfs] + sxtag_x_etudids_x_comps = [coeffs_dfs[sxtag_id] for sxtag_id in sxstags] coeffs_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1) - return notes_etudids_x_comps_x_sxtag, coeffs_etudids_x_comps_x_sxtag + return coeffs_dfs, coeffs_etudids_x_comps_x_sxtag - def do_taglist(self): - """Synthétise les tags à partir des Sxtags aggrégés + def _do_taglist(self) -> list[str]: + """Synthétise les tags à partir des Sxtags aggrégés. Returns: - Une liste de tags triés par ordre alphabétique + Liste de tags triés par ordre alphabétique """ tags = [] for frmsem_id in self.sxstags: tags.extend(self.sxstags[frmsem_id].tags_sorted) return sorted(set(tags)) - def mapping_ue_competences(self): - """Dictionnaire {ue: competences} extrait des SxTags""" + def _do_acronymes_to_competences(self) -> dict[str:str]: + """Synthétise l'association complète {acronyme_ue: competences} + extraite de toutes les données/associations des SxTags + aggrégés. + + Returns: + Un dictionnaire {'acronyme_ue' : 'compétences'} + """ dict_competences = {} for sxtag_id, sxtag in self.sxstags.items(): - comp = sxtag.competences - dict_competences |= comp + dict_competences |= sxtag.acronymes_ues_to_competences return dict_competences - def do_complist(self): - """Synthétise les compétences à partir des Sxtags aggrégés""" - dict_competences = self.mapping_ue_competences() - return sorted(set(dict_competences.values())) + +def compute_coeffs_competences( + coeff_cube: np.array, + set_cube: np.array, + etudids_sorted: list, + competences_sorted: list, +): + """Calcule les coeffs à utiliser pour la moyenne générale (toutes compétences + confondues), en fonction des notes (set_cube) aggrégées. + + Args: + coeffs_cube: coeffs impliqués dans la moyenne générale (semestres par semestres) + set_cube: notes moyennes aux modules ndarray + (etuds x UEs|compétences x sxtags), des floats avec des NaN + etudids_sorted: liste des étudiants (dim. 0 du cube) + competences_sorted: list + + Returns: + Un DataFrame de coefficients (etudids_sorted x compétences_sorted) + """ + nb_etuds, nb_comps, nb_semestres = set_cube.shape + assert nb_etuds == len(etudids_sorted) + assert nb_comps == len(competences_sorted) + + # Quelles entrées du cube contiennent des notes ? + mask = ~np.isnan(set_cube) + + # Enlève les NaN du cube de notes pour les entrées manquantes + coeffs_cube_no_nan = np.nan_to_num(coeff_cube, nan=0.0) + + # Retire les coefficients associées à des données sans notes + coeffs_cube_no_nan = coeffs_cube_no_nan * mask + + # Somme les coefficients (correspondant à des notes) + coeff_tag = np.sum(coeffs_cube_no_nan, axis=2) + + # Le dataFrame des coeffs + coeffs_df = pd.DataFrame( + coeff_tag, index=etudids_sorted, columns=competences_sorted + ) + # Remet à Nan les coeffs à 0 + coeffs_df.fillna(np.nan) + + return coeffs_df def compute_notes_competences( - set_cube: np.array, coeff_cube: np.array, etudids_sorted: list, competences_sorted: list + set_cube: np.array, + etudids_sorted: list, + competences_sorted: list, ): - """Calcule: - * la moyenne par compétences à un tag donné sur plusieurs semestres (partant du set_cube). - * la somme des coeffs à utiliser pour la moyenne générale. + """Calcule la moyenne par compétences (à un tag donné) sur plusieurs semestres (partant du set_cube). La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles @@ -249,7 +360,6 @@ def compute_notes_competences( Args: set_cube: notes moyennes aux modules ndarray (etuds x UEs|compétences x sxtags), des floats avec des NaN - coeffs_cube: somme des coeffs impliqués dans la moyennes etudids_sorted: liste des étudiants (dim. 0 du cube) competences_sorted: list tags: liste des tags (dim. 1 du cube) @@ -266,13 +376,10 @@ def compute_notes_competences( # Enlève les NaN du cube de notes pour les entrées manquantes set_cube_no_nan = np.nan_to_num(set_cube, nan=0.0) - coeffs_cube_no_nan = np.nan_to_num(coeff_cube, nan=0.0) # Les moyennes par tag with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) etud_moy_tag = np.sum(set_cube_no_nan, axis=2) / np.sum(mask, axis=2) - # La somme des coeffs - coeff_tag = np.sum(coeffs_cube_no_nan, axis=2) # Le dataFrame des notes moyennes etud_moy_tag_df = pd.DataFrame( @@ -282,7 +389,4 @@ def compute_notes_competences( ) etud_moy_tag_df.fillna(np.nan) - coeffs_df = pd.DataFrame(coeff_tag, index=etudids_sorted, columns=competences_sorted) - coeffs_df.fillna(np.nan) - - return etud_moy_tag_df, coeffs_df + return etud_moy_tag_df diff --git a/app/pe/pe_ressemtag.py b/app/pe/pe_ressemtag.py index 8b734855..09b88ab7 100644 --- a/app/pe/pe_ressemtag.py +++ b/app/pe/pe_ressemtag.py @@ -41,7 +41,7 @@ from app import db, ScoValueError from app import comp from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_sem import load_formsemestre_results -from app.models import FormSemestre +from app.models import FormSemestre, UniteEns from app.models.moduleimpls import ModuleImpl import app.pe.pe_affichage as pe_affichage import app.pe.pe_etudiant as pe_etudiant @@ -55,7 +55,7 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): """ Un ResSemBUTTag 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. + Il s'appuie principalement sur un ResultatsSemestreBUT. """ def __init__(self, formsemestre: FormSemestre): @@ -69,51 +69,59 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): # Le nom du res_semestre taggué self.nom = self.get_repr(verbose=True) - pe_affichage.pe_print(f"--> Résultats de semestre taggués {self.nom}") + pe_affichage.pe_print(f"--> ResultatsSemestreBUT taggués {self.nom}") # Les étudiants (etuds, états civils & etudis) ajouté self.add_etuds(self.etuds) self.etudids_sorted = sorted(self.etudids) + """Les etudids des étudiants du ResultatsSemestreBUT triés""" # Les UEs (et les dispenses d'UE) - # self.ues - ues_standards = [ue for ue in self.ues if ue.type == sco_codes.UE_STANDARD] + self.ues_standards: list[UniteEns] = [ + ue for ue in self.ues if ue.type == sco_codes.UE_STANDARD + ] + """Liste des UEs standards du ResultatsSemestreBUT""" # Les UEs en fonction des parcours self.ues_inscr_parcours_df = self.load_ues_inscr_parcours() - - # Les compétences associées aux UEs (définies par les acronymes) - self.competences = {} - """L'association acronyme d'UEs -> compétence""" - for ue in self.ues: - if ue.type == sco_codes.UE_STANDARD: - assert ue.niveau_competence, ScoValueError( - "Des UEs ne sont pas rattachées à des compétences" - ) - nom = ue.niveau_competence.competence.titre - self.competences[ue.acronyme] = nom - + """Les inscriptions des étudiants aux UEs du parcours""" # Les acronymes des UEs - self.ues_to_acronymes = {ue.id: ue.acronyme for ue in ues_standards} + self.ues_to_acronymes = {ue.id: ue.acronyme for ue in self.ues_standards} self.acronymes_sorted = sorted(self.ues_to_acronymes.values()) """Les acronymes de UE triés par ordre alphabétique""" + # Les compétences associées aux UEs (définies par les acronymes) + self.acronymes_ues_to_competences = {} + """L'association acronyme d'UEs -> compétence""" + for ue in self.ues_standards: + assert ue.niveau_competence, ScoValueError( + "Des UEs ne sont pas rattachées à des compétences" + ) + nom = ue.niveau_competence.competence.titre + self.acronymes_ues_to_competences[ue.acronyme] = nom + self.competences_sorted = sorted( + list(set(self.acronymes_ues_to_competences.values())) + ) + """Les compétences triées par nom""" + # Les tags personnalisés et auto: tags_dict = self._get_tags_dict() self._check_tags(tags_dict) - # Les coefficients pour le calcul de la moyenne générale - self.matrice_coeffs_moy_gen = self.ues_inscr_parcours_df * [ - ue.ects for ue in ues_standards # if ue.type != UE_SPORT <= déjà supprimé - ] + # Les coefficients pour le calcul de la moyenne générale, donnés par + # acronymes d'UE + self.matrice_coeffs_moy_gen = self._get_matrice_coeffs( + self.ues_inscr_parcours_df, self.ues_standards + ) + """DataFrame indiquant les coeffs des UEs par ordre alphabétique d'acronyme""" # Les capitalisations (mask etuids x acronyme_ue valant True si capitalisée, False sinon) - self.capitalisations = self._get_capitalisations(ues_standards) - + self.capitalisations = self._get_capitalisations(self.ues_standards) + """DataFrame indiquant les UEs capitalisables d'un étudiant (etudids x )""" # Calcul des moyennes & les classements de chaque étudiant à chaque tag self.moyennes_tags = {} - + """Les moyennes par tags (personnalisés ou 'but')""" for tag in tags_dict["personnalises"]: # pe_affichage.pe_print(f" -> Traitement du tag {tag}") infos_tag = tags_dict["personnalises"][tag] @@ -123,49 +131,76 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): ) # Ajoute les moyennes par UEs + la moyenne générale (but) - df_ues = pd.DataFrame( - {ue.id: self.etud_moy_ue[ue.id] for ue in ues_standards}, - index=self.etudids, - ) - # Transforme les UEs en acronyme - colonnes = df_ues.columns - acronymes = [self.ues_to_acronymes[col] for col in colonnes] - df_ues.columns = acronymes - + moy_gen = self.compute_moy_gen() self.moyennes_tags["but"] = MoyennesTag( - "but", df_ues, self.matrice_coeffs_moy_gen # , moy_gen_but + "but", moy_gen, self.matrice_coeffs_moy_gen # , moy_gen_but ) self.tags_sorted = self.get_all_tags() """Tags (personnalisés+compétences) par ordre alphabétique""" - def get_repr(self, verbose=False): - """Nom affiché pour le semestre taggué""" - if verbose: - return f"{self.formsemestre} (#{self.formsemestre.formsemestre_id})" + def get_repr(self, verbose=False) -> str: + """Nom affiché pour le semestre taggué, de la forme (par ex.): + + * S1#69 si verbose est False + * S1 FI 2023 si verbose est True + """ + if not verbose: + return f"{self.formsemestre}#{self.formsemestre.formsemestre_id}" else: return pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True) - def _get_capitalisations(self, ues_hors_sport) -> pd.DataFrame: - """Renvoie un dataFrame résumant les UEs capitalisables par les - étudiants, d'après les décisions de jury + def _get_matrice_coeffs( + self, ues_inscr_parcours_df: pd.DataFrame, ues_standards: list[UniteEns] + ) -> pd.DataFrame: + """Renvoie un dataFrame donnant les coefficients à appliquer aux UEs + dans le calcul de la moyenne générale (toutes UEs confondues). + Prend en compte l'inscription des étudiants aux UEs en fonction de leur parcours + (cf. ues_inscr_parcours_df). Args: - ues_hors_sport: Liste des UEs autres que le sport + ues_inscr_parcours_df: Les inscriptions des étudiants aux UEs + ues_standards: Les UEs standards à prendre en compte + + Returns: + Un dataFrame etudids x acronymes_UEs avec les coeffs des UEs """ - capitalisations = pd.DataFrame(False, index=self.etudids_sorted, columns=self.acronymes_sorted) + matrice_coeffs_moy_gen = ues_inscr_parcours_df * [ + ue.ects for ue in ues_standards # if ue.type != UE_SPORT <= déjà supprimé + ] + matrice_coeffs_moy_gen.columns = [ + self.ues_to_acronymes[ue.id] for ue in ues_standards + ] + # Tri par etudids (dim 0) et par acronymes (dim 1) + matrice_coeffs_moy_gen = matrice_coeffs_moy_gen.sort_index() + matrice_coeffs_moy_gen = matrice_coeffs_moy_gen.sort_index(axis=1) + return matrice_coeffs_moy_gen + + def _get_capitalisations(self, ues_standards) -> pd.DataFrame: + """Renvoie un dataFrame résumant les UEs capitalisables par les + étudiants, d'après les décisions de jury (sous réserve qu'elles existent). + + Args: + ues_standards: Liste des UEs standards (notamment autres que le sport) + Returns: + Un dataFrame etudids x acronymes_UEs dont les valeurs sont ``True`` si l'UE + est capitalisable, ``False`` sinon + """ + capitalisations = pd.DataFrame( + False, index=self.etudids_sorted, columns=self.acronymes_sorted + ) self.get_formsemestre_validations() # charge les validations res_jury = self.validations if res_jury: for etud in self.etuds: etudid = etud.etudid decisions = res_jury.decisions_jury_ues.get(etudid, {}) - for ue in ues_hors_sport: + for ue in ues_standards: if ue.id in decisions and decisions[ue.id]["code"] == sco_codes.ADM: capitalisations.loc[etudid, ue.acronyme] = True - # pe_affichage.pe_print( - # f" ⚠ Capitalisation de {ue.acronyme} pour {etud.etat_civil}" - # ) + # Tri par etudis et par accronyme d'UE + capitalisations = capitalisations.sort_index() + capitalisations = capitalisations.sort_index(axis=1) return capitalisations def compute_moy_ues_tag(self, info_tag: dict[int, dict]) -> pd.DataFrame: @@ -208,14 +243,38 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): block=self.formsemestre.block_moyennes, ) + # Ne conserve que les UEs standards + colonnes = [ue.id for ue in self.ues_standards] + moyennes_ues_tag = moyennes_ues_tag[colonnes] + # Transforme les UEs en acronyme - colonnes = moyennes_ues_tag.columns - ue_to_acro = {ue.id: ue.acronyme for ue in self.ues} - acronymes = [ue_to_acro[col] for col in colonnes] + acronymes = [self.ues_to_acronymes[ue.id] for ue in self.ues_standards] moyennes_ues_tag.columns = acronymes + # Tri par etudids et par ordre alphabétique d'acronyme + moyennes_ues_tag = moyennes_ues_tag.sort_index() + moyennes_ues_tag = moyennes_ues_tag.sort_index(axis=1) + return moyennes_ues_tag + def compute_moy_gen(self): + """Récupère les moyennes des UEs pour le calcul de la moyenne générale, + en associant à chaque UE.id son acronyme (toutes UEs confondues) + """ + df_ues = pd.DataFrame( + {ue.id: self.etud_moy_ue[ue.id] for ue in self.ues_standards}, + index=self.etudids, + ) + # Transforme les UEs en acronyme + colonnes = df_ues.columns + acronymes = [self.ues_to_acronymes[col] for col in colonnes] + df_ues.columns = acronymes + + # Tri par ordre aphabétique de colonnes + df_ues.sort_index(axis=1) + + return df_ues + 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, diff --git a/app/pe/pe_sxtag.py b/app/pe/pe_sxtag.py index 208d9629..f96b7807 100644 --- a/app/pe/pe_sxtag.py +++ b/app/pe/pe_sxtag.py @@ -54,20 +54,24 @@ class SxTag(TableTag): ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag], ): """Calcule les moyennes/classements par tag d'un semestre de type 'Sx' - (par ex. 'S1', 'S2', ...) avec une orientation par UE : + (par ex. 'S1', 'S2', ...) représentés par acronyme d'UE. - * pour les étudiants non redoublants, ce sont les moyennes/classements + Il représente : + + * pour les étudiants *non redoublants* : moyennes/classements du semestre suivi - * pour les étudiants redoublants, c'est une fusion des moyennes/classements - dans les (2) 'Sx' qu'il a suivi + * pour les étudiants *redoublants* : une fusion des moyennes/classements + dans les (2) 'Sx' qu'il a suivi, en exploitant les informations de capitalisation : + meilleure moyenne entre l'UE capitalisée et l'UE refaite (la notion de meilleure + s'appliquant à la moyenne d'UE) - Un SxTag peut donc regrouper plusieurs semestres. + Un SxTag (regroupant potentiellement plusieurs semestres) est identifié + par un tuple ``(Sx, fid)`` où : - Un SxTag est identifié par un tuple (x, fid) où x est le numéro (semestre_id) - du semestre et fid le formsemestre_id du semestre final (le plus récent) du - regrouprement. + * ``x`` est le rang (semestre_id) du semestre + * ``fid`` le formsemestre_id du semestre final (le plus récent) du regroupement. - Les **tags**, les **UE** et les inscriptions aux UEs (pour les etudiants) + Les **tags**, les **UE** et les inscriptions aux UEs (pour les étudiants) considérés sont uniquement ceux du semestre final. Args: @@ -85,6 +89,7 @@ class SxTag(TableTag): self.rcf = rcf """Le RCF sur lequel il s'appuie""" + assert rcf.rcs_id == sxtag_id, "Problème de correspondance SxTag/RCF" # Les resultats des semestres taggués à prendre en compte dans le RCF self.ressembuttags = {fid: ressembuttags[fid] for fid in rcf.semestres_aggreges} @@ -95,10 +100,9 @@ class SxTag(TableTag): self.ressembuttag_final = ressembuttags[self.fid_final] """Le ResSemBUTTag final""" - self.etuds = ressembuttags[self.fid_final].etuds - """Les étudiants du ReSemBUTTag final""" - - # Ajout les etudids et les états civils + # Ajoute les etudids et les états civils + self.etuds = self.ressembuttag_final.etuds + """Les étudiants (extraits du ReSemBUTTag final)""" self.add_etuds(self.etuds) self.etudids_sorted = sorted(self.etudids) """Les etudids triés""" @@ -108,88 +112,93 @@ class SxTag(TableTag): # Les tags self.tags_sorted = self.ressembuttag_final.tags_sorted - """Tags (extraits uniquement du semestre final)""" + """Tags (extraits du ReSemBUTTag final)""" pe_affichage.pe_print(f"* Tags : {', '.join(self.tags_sorted)}") - # Les UE - moy_sem_final = self.ressembuttag_final.moyennes_tags["but"] - self.ues = list(moy_sem_final.matrice_notes.columns) + # Les UE données par leur acronyme + self.acronymes_sorted = self.ressembuttag_final.acronymes_sorted + """Les acronymes des UEs (extraits du ResSemBUTTag final)""" # L'association UE-compétences extraites du dernier semestre - self.competences = self.ressembuttag_final.competences + self.acronymes_ues_to_competences = ( + self.ressembuttag_final.acronymes_ues_to_competences + ) + """L'association acronyme d'UEs -> compétence""" + self.competences_sorted = sorted(self.acronymes_ues_to_competences.values()) + """Les compétences triées par nom""" - # Les acronymes des UE - self.acronymes_ues_sorted = sorted(self.ues) - - # Les inscriptions des étudiants aux UEs - # => ne conserve que les UEs du semestre final (pour les redoublants) - self.ues_inscr_parcours_df = self.ressembuttag_final.ues_inscr_parcours_df - self.ues_inscr_parcours_df.sort_index() - - # Les coeffs pour la moyenne générale - self.matrice_coeffs_moy_gen = self.ressembuttag_final.moyennes_tags[ - "but" - ].matrice_coeffs_moy_gen - self.matrice_coeffs_moy_gen.sort_index() # Trie les coeff par etudids - - # Les moyennes par tag - self.moyennes_tags: dict[str, pd.DataFrame] = {} - """Les notes aux UEs dans différents tags""" + # Les coeffs pour la moyenne générale (traduisant également l'inscription + # des étudiants aux UEs) (etudids_sorted x acronymes_ues_sorted) + self.matrice_coeffs_moy_gen = self.ressembuttag_final.matrice_coeffs_moy_gen # Masque des inscriptions et des capitalisations - self.masque_df, masque_cube = compute_masques_ues_cube( + self.masque_df = None + """Le DataFrame traduisant les capitalisations des différents semestres""" + self.masque_df, masque_cube = compute_masques_capitalisation_cube( self.etudids_sorted, - self.acronymes_ues_sorted, + self.acronymes_sorted, self.ressembuttags, self.fid_final, ) self._aff_capitalisations() + # Les moyennes par tag + self.moyennes_tags: dict[str, pd.DataFrame] = {} + """Moyennes aux UEs (identifiées par leur acronyme) des différents tags""" for tag in self.tags_sorted: # Y-a-t-il des notes ? if not self.has_notes(tag): pe_affichage.pe_print(f"> MoyTag 🏷{tag} actuellement sans ◯ notes") - matrice_moys_ues = pd.DataFrame(np.nan, index=self.etudids_sorted, columns=self.acronymes_ues_sorted) - + matrice_moys_ues = pd.DataFrame( + np.nan, index=self.etudids_sorted, columns=self.acronymes_sorted + ) else: # Cube de note etudids x UEs notes_df, notes_cube = compute_notes_ues_cube( tag, self.etudids_sorted, - self.acronymes_ues_sorted, + self.acronymes_sorted, self.ressembuttags, ) - # self.ues_inscr_parcours = ~np.isnan(self.matrice_coeffs.to_numpy()) - # inscr_mask = self.ues_inscr_parcours + # Masque des inscriptions aux UEs (extraits de la matrice de coefficients) + inscr_mask: np.array = ~np.isnan(self.matrice_coeffs_moy_gen.to_numpy()) - # Calcule des moyennes sous forme d'un dataframe - inscr_mask = ~np.isnan(self.ues_inscr_parcours_df.to_numpy()) + # Matrice des moyennes matrice_moys_ues: pd.DataFrame = compute_notes_ues( notes_cube, masque_cube, self.etudids_sorted, - self.acronymes_ues_sorted, + self.acronymes_sorted, inscr_mask, ) - # Les profils d'ects (pour debug) - profils_ects = [] - for i in self.matrice_coeffs_moy_gen.index: - val = tuple(self.matrice_coeffs_moy_gen.loc[i].fillna("x")) - if tuple(val) not in profils_ects: - profils_ects.append(tuple(val)) - pe_affichage.pe_print( - f"> MoyTag 🏷{tag} avec " - + f"ues={self.acronymes_ues_sorted} " - + f"ects={profils_ects}" - ) + # Affichage de debug + self.__aff_profil_coeff_ects(tag) - # Les moyennes au tag + # Mémorise les infos pour la moyennes au tag self.moyennes_tags[tag] = MoyennesTag( - tag, matrice_moys_ues, self.matrice_coeffs_moy_gen - ) + tag, matrice_moys_ues, self.matrice_coeffs_moy_gen + ) + def __aff_profil_coeff_ects(self, tag): + """Extrait de la matrice des coeffs, les différents types d'inscription + et de coefficients (appelés profil) des étudiants et les affiche + (pour debug) + """ + + # Les profils d'ects (pour debug) + profils_ects = [] + for i in self.matrice_coeffs_moy_gen.index: + val = tuple(self.matrice_coeffs_moy_gen.loc[i].fillna("x")) + if tuple(val) not in profils_ects: + profils_ects.append(tuple(val)) + + # L'affichage + ues = ", ".join(self.acronymes_sorted) + pe_affichage.pe_print( + f"> MoyTag 🏷{tag} avec " + f"ues={ues} " + f"inscr/ects={profils_ects}" + ) def has_notes(self, tag): """Détermine si le SxTag, pour un tag donné, est en cours d'évaluation. @@ -229,7 +238,7 @@ class SxTag(TableTag): cap = [] for frmsem_id in self.ressembuttags: if frmsem_id != self.fid_final: - for accr in self.acronymes_ues_sorted: + for accr in self.acronymes_sorted: if self.masque_df[frmsem_id].loc[etud.etudid, accr] > 0.0: cap += [accr] if cap: @@ -239,7 +248,7 @@ class SxTag(TableTag): def compute_notes_ues_cube( - tag, etudids_sorted, acronymes_ues_sorted, ressembuttags + tag, etudids_sorted, acronymes_sorted, ressembuttags ) -> (pd.DataFrame, np.array): """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. @@ -247,7 +256,7 @@ def compute_notes_ues_cube( Args: etudids_sorted: La liste des etudids triés par ordre croissant (dim 0) - acronymes_ues_sorted: La liste des acronymes de UEs triés par acronyme croissant (dim 1) + acronymes_sorted: La liste des acronymes de UEs triés par acronyme croissant (dim 1) ressembuttags: Le dictionnaire des résultats de semestres BUT (tous tags confondus) """ # Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2) @@ -259,7 +268,7 @@ def compute_notes_ues_cube( for frmsem_id in semestres_id: # Partant d'un dataframe vierge - df = pd.DataFrame(np.nan, index=etudids_sorted, columns=acronymes_ues_sorted) + df = pd.DataFrame(np.nan, index=etudids_sorted, columns=acronymes_sorted) # Charge les notes du semestre tag sem_tag = ressembuttags[frmsem_id] @@ -289,23 +298,27 @@ def compute_notes_ues_cube( return dfs, etudids_x_ues_x_semestres -def compute_masques_ues_cube( +def compute_masques_capitalisation_cube( etudids_sorted: list[int], - acronymes_ues_sorted: list[str], + acronymes_sorted: list[str], ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag], formsemestre_id_final: int, ) -> (pd.DataFrame, np.array): - """Construit le cube traduisant le masque des UEs à prendre en compte dans le calcul - des moyennes, en utilisant le df capitalisations de chaque ResSemBUTTag + """Construit le cube traduisant les masques des UEs à prendre en compte dans le calcul + des moyennes, en utilisant le dataFrame de capitalisations de chaque ResSemBUTTag - Ce masque contient : 1 si la note doit être prise en compte ; 0 sinon + Ces masques contiennent : 1 si la note doit être prise en compte, 0 sinon + + Le masque des UEs à prendre en compte correspondant au semestre final (identifié par + son formsemestre_id_final) est systématiquement à 1 (puisque les résultats + de ce semestre doivent systématiquement + être pris en compte notamment pour les étudiants non redoublant). Args: etudids_sorted: La liste des etudids triés par ordre croissant (dim 0) - acronymes_ues_sorted: La liste des acronymes de UEs triés par acronyme croissant (dim 1) - # ues_inscr_parcours_df: Le dataFrame des inscriptions au UE en fonction du parcours + acronymes_sorted: La liste des acronymes de UEs triés par acronyme croissant (dim 1) ressembuttags: Le dictionnaire des résultats de semestres BUT (tous tags confondus) - formsemestre_id_final: L'identifiant du formsemestre_id_final (dont il faut forcément prendre en compte les coeffs) + formsemestre_id_final: L'identifiant du formsemestre_id_final """ # Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2) # etudids_sorted = etudids_sorted @@ -317,15 +330,16 @@ def compute_masques_ues_cube( for frmsem_id in semestres_id: # Partant d'un dataframe contenant des 1.0 if frmsem_id == formsemestre_id_final: - df = pd.DataFrame(1.0, index=etudids_sorted, columns=acronymes_ues_sorted) + df = pd.DataFrame(1.0, index=etudids_sorted, columns=acronymes_sorted) else: # semestres redoublés - df = pd.DataFrame(0.0, index=etudids_sorted, columns=acronymes_ues_sorted) + df = pd.DataFrame(0.0, index=etudids_sorted, columns=acronymes_sorted) - # Traitement des capitalisations + # Traitement des capitalisations : remplace les infos de capitalisations par les coeff 1 ou 0 capitalisations = ressembuttags[frmsem_id].capitalisations capitalisations = capitalisations.replace(True, 1.0).replace(False, 0.0) - # Met à 0 les coeffs des UEs non capitalisées : 1.0*False => 0.0 + # Met à 0 les coeffs des UEs non capitalisées pour les étudiants + # inscrits dans les 2 semestres: 1.0*False => 0.0 etudids_communs, acronymes_communs = pe_comp.find_index_and_columns_communs( df, capitalisations ) @@ -347,7 +361,7 @@ def compute_notes_ues( set_cube: np.array, masque_cube: np.array, etudids_sorted: list, - acronymes_ues_sorted: list, + acronymes_sorted: list, inscr_mask: np.array, ): """Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE @@ -359,7 +373,7 @@ def compute_notes_ues( masque_cube: masque indiquant si la note doit être prise en compte ndarray (semestre_ids x etudids x UEs), des 1.0 ou des 0.0 etudids_sorted: liste des étudiants (dim. 0 du cube) trié par etudid - acronymes_ues_sorted: liste des acronymes des ues (dim. 1 du cube) trié par acronyme + acronymes_sorted: liste des acronymes des ues (dim. 1 du cube) trié par acronyme inscr_mask: masque etudids x UE traduisant les inscriptions des étudiants aux UE (du semestre terminal) Returns: @@ -369,7 +383,7 @@ def compute_notes_ues( nb_etuds, nb_ues, nb_semestres = set_cube.shape nb_etuds_mask, nb_ues_mask = inscr_mask.shape assert nb_etuds == len(etudids_sorted) - assert nb_ues == len(acronymes_ues_sorted) + assert nb_ues == len(acronymes_sorted) assert nb_etuds == nb_etuds_mask assert nb_ues == nb_ues_mask @@ -397,7 +411,7 @@ def compute_notes_ues( etud_moy_tag_df = pd.DataFrame( etud_moy, index=etudids_sorted, # les etudids - columns=acronymes_ues_sorted, # les tags + columns=acronymes_sorted, # les acronymes d'UEs ) etud_moy_tag_df.fillna(np.nan) diff --git a/tests/unit/yaml_setup_but.py b/tests/unit/yaml_setup_but.py index c4ab421d..b7026c26 100644 --- a/tests/unit/yaml_setup_but.py +++ b/tests/unit/yaml_setup_but.py @@ -105,7 +105,7 @@ def associe_ues_et_parcours(formation: Formation, formation_infos: dict): # Niveaux compétences: if ue_infos.get("competence"): - competence = referentiel_competence.competences.filter_by( + competence = referentiel_competence.acronymes_ues_to_competences.filter_by( titre=ue_infos["competence"] ).first() assert competence is not None # La compétence de titre indiqué doit exister From 5f656b431b014caefbb6cb4746ce14a35e99020f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 20 Feb 2024 09:18:03 +0100 Subject: [PATCH 16/41] Corrige modif non voulue dans tests/unit/yaml_setud_but.py --- tests/unit/yaml_setup_but.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/yaml_setup_but.py b/tests/unit/yaml_setup_but.py index b7026c26..c4ab421d 100644 --- a/tests/unit/yaml_setup_but.py +++ b/tests/unit/yaml_setup_but.py @@ -105,7 +105,7 @@ def associe_ues_et_parcours(formation: Formation, formation_infos: dict): # Niveaux compétences: if ue_infos.get("competence"): - competence = referentiel_competence.acronymes_ues_to_competences.filter_by( + competence = referentiel_competence.competences.filter_by( titre=ue_infos["competence"] ).first() assert competence is not None # La compétence de titre indiqué doit exister From 0f446fe0d379669b70770b1b505d70fb77515eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 20 Feb 2024 16:22:22 +0100 Subject: [PATCH 17/41] =?UTF-8?q?Renomme=20RCs=20pour=20faciliter=20interp?= =?UTF-8?q?r=C3=A9tation=20+=20corrige=20d=C3=A9tection=20des=20RCSemX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_comp.py | 22 ++- app/pe/pe_etudiant.py | 61 ++----- app/pe/pe_interclasstag.py | 6 +- app/pe/pe_jury.py | 61 +++---- app/pe/pe_rcss_jury.py | 286 ++++++++++++++++++++------------- app/pe/pe_rcstag.py | 8 +- app/pe/pe_sxtag.py | 29 ++-- app/pe/rcss/pe_rcf.py | 57 ------- app/pe/rcss/pe_rcrcf.py | 69 -------- app/pe/rcss/pe_rcs.py | 80 ++++++++- app/pe/rcss/pe_rcsemx.py | 59 +++++++ app/pe/rcss/pe_trajectoires.py | 87 ++++++++++ app/pe/rcss/rcss_constantes.py | 64 -------- 13 files changed, 491 insertions(+), 398 deletions(-) delete mode 100644 app/pe/rcss/pe_rcf.py delete mode 100644 app/pe/rcss/pe_rcrcf.py create mode 100644 app/pe/rcss/pe_rcsemx.py create mode 100644 app/pe/rcss/pe_trajectoires.py delete mode 100644 app/pe/rcss/rcss_constantes.py diff --git a/app/pe/pe_comp.py b/app/pe/pe_comp.py index 4f4ba8ed..0ab973f4 100644 --- a/app/pe/pe_comp.py +++ b/app/pe/pe_comp.py @@ -47,7 +47,7 @@ from flask import g import app.scodoc.sco_utils as scu from app.models import FormSemestre -from app.pe.rcss.rcss_constantes import TYPES_RCS +from app.pe.rcss.pe_rcs import TYPES_RCS from app.scodoc import sco_formsemestre from app.scodoc.sco_logos import find_logo @@ -317,3 +317,23 @@ def find_index_and_columns_communs( colonnes2 = df2.columns colonnes_communes = list(set(colonnes1) & set(colonnes2)) return indices_communs, colonnes_communes + + +def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSemestre: + """Renvoie le dernier semestre en **date de fin** d'un dictionnaire + de semestres (potentiellement non trié) de la forme ``{fid: FormSemestre(fid)}``. + + Args: + semestres: Un dictionnaire de semestres + + Return: + Le FormSemestre du semestre le plus récent + """ + if semestres: + fid_dernier_semestre = list(semestres.keys())[0] + dernier_semestre: FormSemestre = semestres[fid_dernier_semestre] + for fid in semestres: + if semestres[fid].date_fin > dernier_semestre.date_fin: + dernier_semestre = semestres[fid] + return dernier_semestre + return None diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 35e9f942..94e17e43 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -59,10 +59,10 @@ class EtudiantsJuryPE: self.identites: dict[int, Identite] = {} # ex. ETUDINFO_DICT "Les identités des étudiants traités pour le jury" - self.trajectoires: dict[int, dict] = {} + self.cursus: dict[int, dict] = {} "Les cursus (semestres suivis, abandons) des étudiants" - self.trajectoires = {} + self.cursus = {} """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)""" @@ -164,7 +164,7 @@ class EtudiantsJuryPE: """ etudids = [ etudid - for etudid, cursus_etud in self.trajectoires.items() + for etudid, cursus_etud in self.cursus.items() if cursus_etud["diplome"] == self.annee_diplome and cursus_etud["abandon"] is False ] @@ -181,18 +181,14 @@ class EtudiantsJuryPE: """ etudids = [ etudid - for etudid, cursus_etud in self.trajectoires.items() + for etudid, cursus_etud in self.cursus.items() if cursus_etud["diplome"] != self.annee_diplome or cursus_etud["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] - ): + def analyse_etat_etudiant(self, etudid: int, cosemestres: dict[int, FormSemestre]): """Analyse le cursus d'un étudiant pouvant être : * l'un de ceux sur lesquels le jury va statuer (année de diplômation du jury considéré) @@ -225,7 +221,7 @@ class EtudiantsJuryPE: if formsemestre.formation.is_apc() } - self.trajectoires[etudid] = { + self.cursus[etudid] = { "etudid": etudid, # les infos sur l'étudiant "etat_civil": identite.etat_civil, # Ajout à la table jury "nom": identite.nom, @@ -241,16 +237,16 @@ class EtudiantsJuryPE: } # Si l'étudiant est succeptible d'être diplomé - if self.trajectoires[etudid]["diplome"] == self.annee_diplome: + if self.cursus[etudid]["diplome"] == self.annee_diplome: # Est-il démissionnaire : charge son dernier semestre pour connaitre son état ? dernier_semes_etudiant = formsemestres[0] res = load_formsemestre_results(dernier_semes_etudiant) etud_etat = res.get_etud_etat(etudid) if etud_etat == scu.DEMISSION: - self.trajectoires[etudid]["abandon"] = True + self.cursus[etudid]["abandon"] = True else: # Est-il réorienté ou a-t-il arrêté (volontairement) sa formation ? - self.trajectoires[etudid]["abandon"] = arret_de_formation( + self.cursus[etudid]["abandon"] = arret_de_formation( identite, cosemestres ) @@ -270,7 +266,7 @@ class EtudiantsJuryPE: Un dictionnaire ``{fid: FormSemestre(fid)}`` dans lequel les semestres amènent à une diplômation antérieur à celle de la diplômation visée par le jury jury """ - semestres_etudiant = self.trajectoires[etudid]["formsemestres"] + semestres_etudiant = self.cursus[etudid]["formsemestres"] semestres_significatifs = {} for fid in semestres_etudiant: semestre = semestres_etudiant[fid] @@ -297,11 +293,9 @@ class EtudiantsJuryPE: for fid, sem_sig in semestres_significatifs.items() if sem_sig.semestre_id == i } - self.trajectoires[etudid][f"S{i}"] = semestres_i + self.cursus[etudid][f"S{i}"] = semestres_i - def get_formsemestres_finals_des_rcs( - self, nom_rcs: str - ) -> dict[int, FormSemestre]: + def get_formsemestres_finals_des_rcs(self, nom_rcs: str) -> dict[int, FormSemestre]: """Pour un nom de RCS donné, ensemble des formsemestres finals possibles pour les RCS. Par ex. un RCS '3S' incluant S1+S2+S3 a pour semestre final un S3. Les formsemestres finals obtenus traduisent : @@ -321,7 +315,7 @@ class EtudiantsJuryPE: Un dictionnaire ``{fid: FormSemestre(fid)}`` """ formsemestres_terminaux = {} - for trajectoire_aggr in self.trajectoires.values(): + for trajectoire_aggr in self.cursus.values(): trajectoire = trajectoire_aggr[nom_rcs] if trajectoire: # Le semestre terminal de l'étudiant de l'aggrégat @@ -338,7 +332,7 @@ class EtudiantsJuryPE: """ nbres_semestres = [] for etudid in etudids: - nbres_semestres.append(self.trajectoires[etudid]["nb_semestres"]) + nbres_semestres.append(self.cursus[etudid]["nb_semestres"]) if not nbres_semestres: return 0 return max(nbres_semestres) @@ -359,7 +353,7 @@ class EtudiantsJuryPE: for etudid in etudids: etudiant = self.identites[etudid] - cursus = self.trajectoires[etudid] + cursus = self.cursus[etudid] formsemestres = cursus["formsemestres"] if cursus["diplome"]: @@ -549,9 +543,9 @@ def arret_de_formation(etud: Identite, cosemestres: dict[int, FormSemestre]) -> non_inscrit_a = [ rang for rang in etat_inscriptions if not etat_inscriptions[rang] ] - affichage = ",".join([f"S{val}" for val in non_inscrit_a]) + affichage = ", ".join([f"S{val}" for val in non_inscrit_a]) pe_affichage.pe_print( - f"{etud.etat_civil} ({etud.etudid}) considéré en abandon car non inscrit dans un (ou des) semestre(s) {affichage} amenant à diplômation" + f"--> ⛔ {etud.etat_civil} ({etud.etudid}), non inscrit dans {affichage} amenant à diplômation" ) return est_demissionnaire @@ -593,26 +587,6 @@ def arret_de_formation(etud: Identite, cosemestres: dict[int, FormSemestre]) -> # return False -def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSemestre: - """Renvoie le dernier semestre en **date de fin** d'un dictionnaire - de semestres (potentiellement non trié) de la forme ``{fid: FormSemestre(fid)}``. - - Args: - semestres: Un dictionnaire de semestres - - Return: - Le FormSemestre du semestre le plus récent - """ - if semestres: - fid_dernier_semestre = list(semestres.keys())[0] - dernier_semestre: FormSemestre = semestres[fid_dernier_semestre] - for fid in semestres: - if semestres[fid].date_fin > dernier_semestre.date_fin: - dernier_semestre = semestres[fid] - return dernier_semestre - return None - - def etapes_du_cursus( semestres: dict[int, FormSemestre], nbre_etapes_max: int ) -> list[str]: @@ -679,4 +653,3 @@ def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str: description.append(f"({semestre.formsemestre_id})") return " ".join(description) - diff --git a/app/pe/pe_interclasstag.py b/app/pe/pe_interclasstag.py index 91105066..9d5b998d 100644 --- a/app/pe/pe_interclasstag.py +++ b/app/pe/pe_interclasstag.py @@ -83,8 +83,8 @@ class RCSInterclasseTag(TableTag): # celles associées à l'aggrégat self.rcss: dict[int, pe_rcs.RCS] = {} """Ensemble des trajectoires associées à l'aggrégat""" - for trajectoire_id in rcss_jury_pe.rcss: - trajectoire = rcss_jury_pe.rcss[trajectoire_id] + for trajectoire_id in rcss_jury_pe.trajectoires: + trajectoire = rcss_jury_pe.trajectoires[trajectoire_id] if trajectoire_id[0] == nom_rcs: self.rcss[trajectoire_id] = trajectoire @@ -99,7 +99,7 @@ class RCSInterclasseTag(TableTag): """Association entre chaque étudiant et la trajectoire tagguée à prendre en compte pour l'aggrégat""" for etudid in self.diplomes_ids: - self.suivi[etudid] = rcss_jury_pe.rcss_suivis[etudid][nom_rcs] + self.suivi[etudid] = rcss_jury_pe.trajectoires_suivies[etudid][nom_rcs] self.tags_sorted = self.do_taglist() """Liste des tags (triés par ordre alphabétique)""" diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 168cdb9b..030e3ea7 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -50,17 +50,15 @@ from zipfile import ZipFile import numpy as np import pandas as pd -import app.pe.rcss.rcss_constantes as rcss_constants +from app.pe.rcss import pe_rcs from app.pe import pe_sxtag from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE import app.pe.pe_affichage as pe_affichage import app.pe.pe_etudiant as pe_etudiant -import app.pe.rcss.pe_rcs as pe_rcs from app.pe.pe_rcstag import RCSTag from app.pe.pe_ressemtag import ResSemBUTTag from app.pe.pe_interclasstag import RCSInterclasseTag import app.pe.pe_rcss_jury as pe_rcss_jury -import app.pe.rcss.rcss_constantes as rcss_constantes class JuryPE(object): @@ -107,11 +105,11 @@ class JuryPE(object): try: self._gen_xls_diplomes(zipfile) self._gen_xls_ressembuttags(zipfile) - self._gen_rcss() - self._gen_rcsf() + self._gen_trajectoires() + self._gen_semXs() self._gen_xls_sxtags(zipfile) - self._gen_rcrcfs() - self._gen_xls_rcrcss_tags(zipfile) + # self._gen_rcsemxs() + # self._gen_xls_rcrcss_tags(zipfile) # self._gen_xls_interclassements_rcss(zipfile) # self._gen_xls_synthese_jury_par_tag(zipfile) # self._gen_xls_synthese_par_etudiant(zipfile) @@ -185,35 +183,40 @@ class JuryPE(object): path="details", ) - def _gen_rcss(self): - """Génère les RCS (attribut `rcss_jury`), combinaisons de semestres - suivis par les étudiants au sens d'un nom de RCS (par ex: 'S2' ou '3S'). + def _gen_trajectoires(self): + """Génère l'ensemble des trajectoires (RCS), qui traduisent les différents + chemins au sein des (form)semestres pour atteindre la cible d'un + RCS (par ex: 'S2' ou '3S'). """ pe_affichage.pe_print( - "*** Génère les RCS (différentes combinaisons de semestres) des étudiants" + "*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants" ) - self.rcss_jury.cree_rcss(self.etudiants) + self.rcss_jury.cree_trajectoires(self.etudiants) - def _gen_rcsf(self): - """Génère les RCF, regroupement de semestres de type Sx pour préparer - le calcul des moyennes par Sx""" + def _gen_semXs(self): + """Génère les SemXs (trajectoires/combinaisons de semestre de même rang x) + qui traduisent les différents chemins des étudiants pour valider un semestre Sx. + """ # Génère les regroupements de semestres de type Sx pe_affichage.pe_print( - "*** Génère les RCSValid (RCS de même Sx donnant lieu à validation du semestre)" + "*** Génère les SemXs (RCS de même Sx donnant lieu à validation du semestre)" ) - self.rcss_jury.cree_rcfs(self.etudiants) + self.rcss_jury.cree_semxs(self.etudiants) + self.rcss_jury._aff_semxs_suivis(self.etudiants) def _gen_xls_sxtags(self, zipfile: ZipFile): """Génère les semestres taggués en s'appuyant sur les RCF de type Sx (pour identifier les redoublements impactant les semestres taggués). """ # Génère les moyennes des RCS de type Sx - pe_affichage.pe_print("*** Calcule les moyennes des SxTag") + pe_affichage.pe_print( + "*** Calcule les moyennes des SxTag (moyennes d'un SemX/RCS de type Sx)" + ) # Les SxTag (moyenne de Sx par UE) self.sxtags = {} - for rcf_id, rcf in self.rcss_jury.rcfs.items(): + for rcf_id, rcf in self.rcss_jury.semXs.items(): # SxTag traduisant le RCF sxtag_id = rcf_id @@ -242,13 +245,15 @@ class JuryPE(object): path="details", ) - def _gen_rcrcfs(self): + def _gen_rcsemxs(self): """Génère les regroupements cohérents de RCFs qu'ont suivi chaque étudiant""" pe_affichage.pe_print( - "*** Génère les RCRCF (regroupements de RCF de type Sx) amenant du S1 à un semestre final***" + "*** Génère les RCSemX (regroupements cohérents de données" + " extraites des SemX) amenant du S1 à un semestre final***" ) - self.rcss_jury.cree_rcrcfs(self.etudiants) + self.rcss_jury.cree_rcsemxs(self.etudiants) + self.rcss_jury._aff_rcsemxs_suivis(self.etudiants) def _gen_xls_rcrcss_tags(self, zipfile: ZipFile): """Génère les RCS taggués traduisant les moyennes (orientées compétences) @@ -273,7 +278,7 @@ class JuryPE(object): pe_affichage.pe_print("*** Calcule les moyennes des RC de RCFS") self.rcss_tags = {} - for rcs_id, rcrcf in self.rcss_jury.rcrcfs.items(): + for rcs_id, rcrcf in self.rcss_jury.rcsemxs.items(): self.rcss_tags[rcs_id] = RCSTag(rcrcf, self.sxtags) # Intègre le bilan des trajectoires tagguées au zip final @@ -458,13 +463,13 @@ class JuryPE(object): # Ajout des aggrégats for aggregat in pe_rcs.TOUS_LES_RCS: - descr = rcss_constantes.TYPES_RCS[aggregat]["descr"] + descr = app.pe.rcss.pe_rcs.TYPES_RCS[aggregat]["descr"] # Les trajectoires (tagguées) suivies par les étudiants pour l'aggrégat et le tag # considéré trajectoires_tagguees = [] for etudid in etudids: - trajectoire = self.rcss_jury.rcss_suivis[etudid][aggregat] + trajectoire = self.rcss_jury.trajectoires_suivies[etudid][aggregat] if trajectoire: tid = trajectoire.sxtag_id trajectoire_tagguee = self.rcss_tags[tid] @@ -610,7 +615,7 @@ class JuryPE(object): ) # La trajectoire de l'étudiant sur l'aggrégat - trajectoire = self.rcss_jury.rcss_suivis[etudid][aggregat] + trajectoire = self.rcss_jury.trajectoires_suivies[etudid][aggregat] if trajectoire: trajectoire_tagguee = self.rcss_tags[trajectoire.sxtag_id] if tag in trajectoire_tagguee.moyennes_tags: @@ -651,9 +656,9 @@ def get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict: """ semestres = {} for etudid in etudiants.etudiants_ids: - for cle in etudiants.trajectoires[etudid]: + for cle in etudiants.cursus[etudid]: if cle.startswith("S"): - semestres = semestres | etudiants.trajectoires[etudid][cle] + semestres = semestres | etudiants.cursus[etudid][cle] return semestres diff --git a/app/pe/pe_rcss_jury.py b/app/pe/pe_rcss_jury.py index 58697cb2..65e423b0 100644 --- a/app/pe/pe_rcss_jury.py +++ b/app/pe/pe_rcss_jury.py @@ -1,9 +1,7 @@ - -import app.pe.rcss.pe_rcf as pe_rcf -import app.pe.rcss.pe_rcrcf as pe_rcrcf +import app.pe.pe_comp +from app.pe.rcss import pe_rcs, pe_trajectoires, pe_rcsemx import app.pe.pe_etudiant as pe_etudiant import app.pe.pe_comp as pe_comp -import app.pe.rcss.rcss_constantes as rcss_constantes from app.models import FormSemestre from app.pe import pe_affichage @@ -20,159 +18,198 @@ class RCSsJuryPE: self.annee_diplome = annee_diplome """Année de diplômation""" - self.rcss: dict[tuple(int, str): pe_rcf.RCF] = {} - """Ensemble des RCS recensés""" + self.trajectoires: dict[tuple(int, str) : pe_trajectoires.Trajectoire] = {} + """Ensemble des trajectoires recensées (regroupement de (form)semestres BUT)""" - self.rcss_suivis: dict[int:dict] = {} + self.trajectoires_suivies: dict[int:dict] = {} """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, - son RCS : {etudid: {nom_RCS: RCS}}""" + sa Trajectoire : {etudid: {nom_RCS: Trajectoire}}""" - self.rcfs: dict[tuple(int, str) : pe_rcf.RCF] = {} - """Ensemble des RCF recensés : {(nom_RCS, fid_terminal): RCF}""" + self.semXs: dict[tuple(int, str) : pe_trajectoires.SemX] = {} + """Ensemble des SemX recensés (regroupement de (form)semestre BUT de rang x) : + {(nom_RCS, fid_terminal): SemX}""" - self.rcfs_suivis: dict[int:dict] = {} + self.semXs_suivis: dict[int:dict] = {} + """Dictionnaire associant, pour chaque étudiant et pour chaque RCS de type Sx, + son SemX : {etudid: {nom_RCS_de_type_Sx: SemX}}""" + + self.rcsemxs: dict[tuple(int, str) : pe_rcsemx.RCSemX] = {} + """Ensemble des RCSemX (regroupement de SemX donnant les résultats aux sems de rang x) + recensés : {(nom_RCS, fid_terminal): RCSemX}""" + + self.rcsemxs_suivis: dict[int:str] = {} """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, - son RCS : {etudid: {nom_RCS: RCF}}""" + son RCSemX : {etudid: {nom_RCS: RCSemX}}""" - self.rcrcfs: dict[tuple(int, str) : pe_rcrcf.RCRCF] = {} - """Ensemble des RCS recensés : {(nom_RCS, fid_terminal): RCRCF}""" - - self.rcrcfs_suivis: dict[int:str] = {} - """Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS, - son RCRCF : {etudid: {nom_RCS: RCSx}}""" - - def cree_rcss(self, etudiants: pe_etudiant.EtudiantsJuryPE): - """Créé tous les RCS, au regard du cursus des étudiants + def cree_trajectoires(self, etudiants: pe_etudiant.EtudiantsJuryPE): + """Créé toutes les trajectoires, au regard du cursus des étudiants analysés + les mémorise dans les données de l'étudiant Args: etudiants: Les étudiants à prendre en compte dans le Jury PE """ - tous_les_aggregats = rcss_constantes.TOUS_LES_SEMESTRES + rcss_constantes.TOUS_LES_RCS_AVEC_PLUSIEURS_SEM - for etudid in etudiants.trajectoires: - self.rcss_suivis[etudid] = { - aggregat: None - for aggregat in tous_les_aggregats + tous_les_aggregats = ( + pe_rcs.TOUS_LES_SEMESTRES + pe_rcs.TOUS_LES_RCS_AVEC_PLUSIEURS_SEM + ) + for etudid in etudiants.cursus: + self.trajectoires_suivies[etudid] = { + aggregat: None for aggregat in tous_les_aggregats } for nom_rcs in 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 = rcss_constantes.TYPES_RCS[nom_rcs]["aggregat"] + noms_semestre_de_aggregat = pe_rcs.TYPES_RCS[nom_rcs]["aggregat"] nom_semestre_terminal = noms_semestre_de_aggregat[-1] - for etudid in etudiants.trajectoires: + for etudid in etudiants.cursus: # Le formsemestre terminal (dernier en date) associé au # semestre marquant la fin de l'aggrégat # (par ex: son dernier S3 en date) - trajectoire = etudiants.trajectoires[etudid][nom_semestre_terminal] + trajectoire = etudiants.cursus[etudid][nom_semestre_terminal] if trajectoire: - formsemestre_final = pe_etudiant.get_dernier_semestre_en_date(trajectoire) + formsemestre_final = app.pe.pe_comp.get_dernier_semestre_en_date( + trajectoire + ) # Ajout ou récupération du RCS associé rcs_id = (nom_rcs, formsemestre_final.formsemestre_id) - if rcs_id not in self.rcss: - self.rcss[rcs_id] = pe_rcf.RCF(nom_rcs, formsemestre_final) - rcs = self.rcss[rcs_id] + if rcs_id not in self.trajectoires: + self.trajectoires[rcs_id] = pe_trajectoires.Trajectoire( + nom_rcs, formsemestre_final + ) + rcs = self.trajectoires[rcs_id] # La liste des semestres de l'étudiant à prendre en compte # pour cette trajectoire semestres_a_aggreger = get_rcs_etudiant( - etudiants.trajectoires[etudid], formsemestre_final, nom_rcs + etudiants.cursus[etudid], formsemestre_final, nom_rcs ) # Ajout des semestres au RCS - rcs.add_semestres_a_aggreger(semestres_a_aggreger) + rcs.add_semestres(semestres_a_aggreger) # Mémorise le RCS suivi par l'étudiant - self.rcss_suivis[etudid][nom_rcs] = rcs + self.trajectoires_suivies[etudid][nom_rcs] = rcs # Affichage pour debug - jeunes = list(enumerate(self.rcss_suivis)) + jeunes = list(enumerate(self.trajectoires_suivies)) for no_etud, etudid in jeunes[:20]: - pe_affichage.pe_print(f"--> {etudiants.identites[etudid].nomprenom} (#{etudid}) :") - for nom_rcs, rcs in self.rcss_suivis[etudid].items(): + pe_affichage.pe_print( + f"--> {etudiants.identites[etudid].nomprenom} (#{etudid}) :" + ) + for nom_rcs, rcs in self.trajectoires_suivies[etudid].items(): if rcs: pe_affichage.pe_print(f" > RCS {nom_rcs}: {rcs.get_repr()}") - def cree_rcfs(self, etudiants: pe_etudiant.EtudiantsJuryPE): - """Créé les RCFs en ne conservant dans les RCS que les regroupements + def cree_semxs(self, etudiants: pe_etudiant.EtudiantsJuryPE): + """Créé les les SemXs (trajectoires/combinaisons de semestre de même rang x), + en ne conservant dans les trajectoires que les regroupements de type Sx""" - self.rcfs = {} - for rcs_id, rcs in self.rcss.items(): - if rcs and rcs.nom in rcss_constantes.TOUS_LES_SEMESTRES: - self.rcfs[rcs_id] = rcs + self.semXs = {} + for rcs_id, trajectoire in self.trajectoires.items(): + if trajectoire and trajectoire.nom in pe_rcs.TOUS_LES_SEMESTRES: + self.semXs[rcs_id] = pe_trajectoires.SemX(trajectoire) - for etudid in self.rcss_suivis: - for nom_rcs, rcs in self.rcss_suivis[etudid].items(): - if rcs and nom_rcs in rcss_constantes.TOUS_LES_SEMESTRES: - if etudid not in self.rcfs_suivis: - self.rcfs_suivis[etudid] = {} - self.rcfs_suivis[etudid][nom_rcs] = rcs + self.semXs_suivis = {} + for etudid in self.trajectoires_suivies: + self.semXs_suivis[etudid] = { + nom_rcs: None for nom_rcs in pe_rcs.TOUS_LES_SEMESTRES + } - # Affichage pour debug - jeunes = list(enumerate(self.rcfs_suivis)) + for nom_rcs, trajectoire in self.trajectoires_suivies[etudid].items(): + if trajectoire and nom_rcs in pe_rcs.TOUS_LES_SEMESTRES: + rcs_id = trajectoire.rcs_id + self.semXs_suivis[etudid][nom_rcs] = self.semXs[rcs_id] + + def _aff_semxs_suivis(self, etudiants: pe_etudiant.EtudiantsJuryPE): + """Affichage des SemX pour debug""" + jeunes = list(enumerate(self.semXs_suivis)) + vides = [] for no_etud, etudid in jeunes[:20]: pe_affichage.pe_print(f"-> {etudiants.identites[etudid].nomprenom} :") - for nom_rcs, rcs in self.rcfs_suivis[etudid].items(): + for nom_rcs, rcs in self.semXs_suivis[etudid].items(): if rcs: - pe_affichage.pe_print(f" > RCSValid {nom_rcs}: {rcs.get_repr()}") + pe_affichage.pe_print(f" > SemX {nom_rcs}: {rcs.get_repr()}") else: - pe_affichage.pe_print(f" > RCSValid {nom_rcs}: ") + vides += [nom_rcs] + vides = sorted(list(set(vides))) + pe_affichage.pe_print(f"-> ⚠ SemX vides : {', '.join(vides)}") - def cree_rcrcfs(self, etudiants: pe_etudiant.EtudiantsJuryPE): - """Créé tous les RCRCF, au regard du cursus des étudiants + def cree_rcsemxs(self, etudiants: pe_etudiant.EtudiantsJuryPE): + """Créé tous les RCSemXs, au regard du cursus des étudiants analysés (trajectoires traduisant son parcours dans les différents semestres) + les mémorise dans les données de l'étudiant """ + self.rcsemxs_suivis = {nom_rcs: None for nom_rcs in pe_rcs.TOUS_LES_RCS} + self.rcsemxs = {} # Pour tous les étudiants du jury - for etudid in self.rcss_suivis: - self.rcrcfs_suivis[etudid] = {} + for etudid in self.trajectoires_suivies: + self.rcsemxs_suivis[etudid] = {} - for nom_rcs, rcf in self.rcfs_suivis[etudid].items(): # Pour chaque RCS - semestres_a_aggreger = rcf.semestres_aggreges + # Recopie des SemX & des suivis associés + for nom_rcs in pe_rcs.TOUS_LES_SEMESTRES: + trajectoire = self.semXs_suivis[etudid][nom_rcs] + if trajectoire: + self.rcsemxs[trajectoire.rcs_id] = trajectoire + self.rcsemxs_suivis[etudid][nom_rcs] = trajectoire - # Tri des semestres par rang - semestres_tries = pe_comp.tri_semestres_par_rang(semestres_a_aggreger) - - # Récupére les RCFs de type Sx traduisant sa trajectoire - rcfs_a_aggreger = {} - for semestres_du_rang in semestres_tries.values(): - if semestres_du_rang: - rcf_id = get_rcf_from_semestres_aggreges( - self.rcfs, semestres_du_rang + # Pour chaque aggréggat de type xA ou Sx + for nom_rcs in pe_rcs.TOUS_LES_RCS_AVEC_PLUSIEURS_SEM: + trajectoire = self.trajectoires_suivies[etudid][nom_rcs] + if not trajectoire: + self.rcsemxs_suivis[etudid][nom_rcs] = None + else: + # Identifiant de la trajectoire => donnera ceux du RCSemX + tid = trajectoire.rcs_id + # Ajout du RCSemX + if tid not in self.rcsemxs: + self.rcsemxs[tid] = pe_rcsemx.RCSemX( + trajectoire.nom, trajectoire.formsemestre_final ) - if not rcf_id: - raise ValueError( - "Il manque un RCF pour créer les RCRCFs dans cree_rcrcfs" + + # Récupére les SemX (RC de type Sx) associés aux semestres de son cursus + # Par ex: dans S1+S2+S1+S2+S3 => les 2 S1 devient le SemX('S1'), les 2 S2 le SemX('S2'), etc.. + + # Les Sx pris en compte dans l'aggrégat + noms_sems_aggregat = pe_rcs.TYPES_RCS[nom_rcs]["aggregat"] + + semxs_a_aggreger = {} + for Sx in noms_sems_aggregat: + semestres_etudiants = etudiants.cursus[etudid][Sx] + + semx_id = get_semx_from_semestres_aggreges( + self.semXs, semestres_etudiants + ) + if not semx_id: + raise ( + "Il manque un SemX pour créer les RCSemX dans cree_rcsemxs" ) - rcfs_a_aggreger[rcf_id] = self.rcfs[rcf_id] + # Les SemX à ajouter au RCSemX + semxs_a_aggreger[semx_id] = self.semXs[semx_id] - # Ajout du RCRCF - if rcf_id not in self.rcrcfs: - rcf_nom = rcf_id[0] - self.rcrcfs[rcf_id] = pe_rcrcf.RCRCF(rcf_nom, rcf.formsemestre_final) - rcrcf = self.rcrcfs[rcf_id] + # Ajout des SemX à ceux à aggréger dans le RCSemX + rcsemx = self.rcsemxs[tid] + rcsemx.add_semXs(semxs_a_aggreger) - # Ajout des RCFs au RCRCF - rcrcf.add_rcfs_a_aggreger(rcfs_a_aggreger) - - # Mémoire la trajectoire RCRCF suivie par l'étudiant - nom_rcs = rcrcf.nom - self.rcrcfs_suivis[etudid][nom_rcs] = rcrcf + # Mémoire du RCSemX aux informations de suivi de l'étudiant + self.rcsemxs_suivis[etudid][nom_rcs] = rcsemx + def _aff_rcsemxs_suivis(self, etudiants): + """Affiche les RCSemX suivis par les étudiants""" # Affichage pour debug - jeunes = list(enumerate(self.rcrcfs_suivis)) + jeunes = list(enumerate(self.rcsemxs_suivis)) + vides = [] for no_etud, etudid in jeunes[:20]: pe_affichage.pe_print(f"-> {etudiants.identites[etudid].nomprenom} :") - for nom_rcs, rcs in self.rcrcfs_suivis[etudid].items(): + for nom_rcs, rcs in self.rcsemxs_suivis[etudid].items(): if rcs: - pe_affichage.pe_print(f" > RCRCF {nom_rcs}: {rcs.get_repr()}") + pe_affichage.pe_print(f" > RCSemX {nom_rcs}: {rcs.get_repr()}") else: - pe_affichage.pe_print(f" > RCRCF {nom_rcs}: !!! ") - + vides += [f"{nom_rcs}"] + pe_affichage.pe_print(f"-> ⚠ RCSemX vides : {', '.join(list(set(vides)))}") def get_rcs_etudiant( @@ -204,7 +241,7 @@ def get_rcs_etudiant( numero_semestres_possibles = [numero_semestre_terminal] elif nom_rcs.endswith("A"): # les années numero_semestres_possibles = [ - int(sem[-1]) for sem in rcss_constantes.TYPES_RCS[nom_rcs]["aggregat"] + int(sem[-1]) for sem in pe_rcs.TYPES_RCS[nom_rcs]["aggregat"] ] assert numero_semestre_terminal in numero_semestres_possibles else: # les xS = tous les semestres jusqu'à Sx (eg S1, S2, S3 pour un S3 terminal) @@ -221,25 +258,52 @@ def get_rcs_etudiant( return semestres_aggreges -def get_rcf_from_semestres_aggreges( - rcfs: dict[(str, int):pe_rcf.RCF], semestres_a_aggreges: list[FormSemestre] - ) -> (str, int): - """Partant d'un dictionnaire de RCFs (de la forme - ``{ (nom_rcs, fid): RCF }, et connaissant une liste - de (form)semestres à aggréger, renvoie l'identifiant - (nom_rcs, fid) du RCFs qui lui correspond (c'est à dire celui dont - les semestres_aggregés par le RCF sont les même que les - semestres_a_aggreger. +def get_semx_from_semestres_aggreges( + semXs: dict[(str, int) : pe_trajectoires.SemX], + semestres_a_aggreger: dict[(str, int):FormSemestre], +) -> (str, int): + """Partant d'un dictionnaire de SemX (de la forme + ``{ (nom_rcs, fid): SemX }, et connaissant une liste + de (form)semestres suivis, renvoie l'identifiant + (nom_rcs, fid) du SemX qui lui correspond. - Returns: - rcf_id: L'identifiant du RCF trouvé + Le SemX qui correspond est tel que : + + * le semestre final du SemX correspond au dernier semestre en date des + semestres_a_aggreger + * le rang du SemX est le même que celui des semestres_aggreges + * les semestres_a_aggreger (plus large, car contenant plusieurs + parcours), matchent avec les semestres aggrégés + par le SemX + + + Returns: + rcf_id: L'identifiant du RCF trouvé """ - fids_semestres_a_aggreger = set( - [frms.formsemestre_id for frms in semestres_a_aggreges] - ) - for rcf_id, rcf in rcfs.items(): - fids_rcf = set(rcf.semestres_aggreges) - if fids_rcf == fids_semestres_a_aggreger: - return rcf_id - return None + assert semestres_a_aggreger, "Pas de semestres à aggréger" + rangs_a_aggreger = [sem.semestre_id for fid, sem in semestres_a_aggreger.items()] + assert ( + len(set(rangs_a_aggreger)) == 1 + ), "Tous les sem à aggréger doivent être de même rang" + # Le dernier semestre des semestres à regrouper + dernier_sem_a_aggreger = pe_comp.get_dernier_semestre_en_date(semestres_a_aggreger) + + semxs_ids = [] # Au cas où il y ait plusieurs solutions + for semx_id, semx in semXs.items(): + # Même semestre final ? + if semx.get_formsemestre_id_final() == dernier_sem_a_aggreger.formsemestre_id: + # Les fids + fids_a_aggreger = set(semestres_a_aggreger.keys()) + # Ceux du semx + fids_semx = set(semx.semestres_aggreges.keys()) + if fids_a_aggreger.issubset( + fids_semx + ): # tous les semestres du semx correspond à des sems de la trajectoire + semxs_ids += [semx_id] + if len(semxs_ids) == 0: + return None # rien trouvé + elif len(semxs_ids) == 1: + return semxs_ids[0] + else: + raise "Plusieurs solutions :)" diff --git a/app/pe/pe_rcstag.py b/app/pe/pe_rcstag.py index 43425941..d675483f 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/pe_rcstag.py @@ -42,7 +42,7 @@ from app.pe import pe_affichage import pandas as pd import numpy as np import app.pe.rcss.pe_rcs as pe_rcs -import app.pe.rcss.pe_rcrcf as pe_rcrcf +import app.pe.rcss.pe_rcsemx as pe_rcrcf import app.pe.pe_sxtag as pe_sxtag import app.pe.pe_comp as pe_comp from app.pe.pe_tabletags import TableTag @@ -66,7 +66,7 @@ class RCSTag(TableTag): self.rcs_id: tuple(str, int) = rcrcf.rcs_id """Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)""" - self.rcrcf: pe_rcrcf.RCRCF = rcrcf + self.rcrcf: pe_rcrcf.RCSemX = rcrcf """RCRCF associé au RCS taggué""" self.nom = self.get_repr() @@ -82,12 +82,12 @@ class RCSTag(TableTag): pe_affichage.pe_print(f"-> {self.get_repr(verbose=True)}") # Les données aggrégés (RCRCF + SxTags - self.rcfs_aggreges = rcrcf.rcfs_aggreges + self.rcsemxs_aggreges = rcrcf.rcsemxs_aggreges """Les RCFs aggrégés""" self.sxstags = {} """Les SxTag associés aux RCF aggrégés""" try: - for rcf_id in self.rcfs_aggreges: + for rcf_id in self.rcsemxs_aggreges: self.sxstags[rcf_id] = sxstags[rcf_id] except: raise ValueError("Semestres SxTag manquants") diff --git a/app/pe/pe_sxtag.py b/app/pe/pe_sxtag.py index f96b7807..b7600bca 100644 --- a/app/pe/pe_sxtag.py +++ b/app/pe/pe_sxtag.py @@ -43,14 +43,14 @@ import numpy as np from app.pe.pe_tabletags import TableTag from app.pe.pe_moytag import MoyennesTag -import app.pe.rcss.pe_rcf as pe_rcf +import app.pe.rcss.pe_trajectoires as pe_trajectoires class SxTag(TableTag): def __init__( self, sxtag_id: (str, int), - rcf: pe_rcf.RCF, + semx: pe_trajectoires.SemX, ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag], ): """Calcule les moyennes/classements par tag d'un semestre de type 'Sx' @@ -86,13 +86,22 @@ class SxTag(TableTag): self.sxtag_id: (str, int) = sxtag_id """Identifiant du SxTag de la forme (nom_Sx, fid_semestre_final)""" + assert ( + len(self.sxtag_id) == 2 + and isinstance(self.sxtag_id[0], str) + and isinstance(self.sxtag_id[1], int) + ), "Format de l'identifiant du SxTag non respecté" - self.rcf = rcf - """Le RCF sur lequel il s'appuie""" - assert rcf.rcs_id == sxtag_id, "Problème de correspondance SxTag/RCF" + self.nom_rcs = sxtag_id[0] - # Les resultats des semestres taggués à prendre en compte dans le RCF - self.ressembuttags = {fid: ressembuttags[fid] for fid in rcf.semestres_aggreges} + self.semx = semx + """Le SemX sur lequel il s'appuie""" + assert semx.rcs_id == sxtag_id, "Problème de correspondance SxTag/SemX" + + # Les resultats des semestres taggués à prendre en compte dans le SemX + self.ressembuttags = { + fid: ressembuttags[fid] for fid in semx.semestres_aggreges + } """Les ResSemBUTTags à regrouper dans le SxTag""" # Les données du semestre final @@ -108,7 +117,7 @@ class SxTag(TableTag): """Les etudids triés""" # Affichage - pe_affichage.pe_print(f"--> {self.get_repr()}") + pe_affichage.pe_print(f"--> {self.get_repr(verbose=True)}") # Les tags self.tags_sorted = self.ressembuttag_final.tags_sorted @@ -227,10 +236,10 @@ class SxTag(TableTag): """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle est basée)""" if verbose: - return f"{self.sxtag_id[0]}Tag basé sur {self.rcf.get_repr()}" + return f"SXTag basé sur {self.semx.get_repr()}" else: # affichage = [str(fid) for fid in self.ressembuttags] - return f"{self.sxtag_id[0]}Tag (#{self.fid_final})" + return f"SXTag {self.nom_rcs}#{self.fid_final}" def _aff_capitalisations(self): """Affichage des capitalisations du sxtag pour debug""" diff --git a/app/pe/rcss/pe_rcf.py b/app/pe/rcss/pe_rcf.py deleted file mode 100644 index 494a3505..00000000 --- a/app/pe/rcss/pe_rcf.py +++ /dev/null @@ -1,57 +0,0 @@ - -from app.models import FormSemestre -import app.pe.rcss.pe_rcs as pe_rcs - - -class RCF(pe_rcs.RCS): - """Modélise un ensemble de (form)semestres d'étudiants - associé à un type de regroupement cohérent de semestres - donné (par ex: 'S2', '3S', '2A'). - - Si le RCF est un semestre de type Si, stocke les - formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si - (en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants) - - Pour le RCF de type iS ou iA (par ex, 3A=S1+S2+S3), identifie - les semestres que les étudiants ont suivis pour les amener jusqu'au semestre - terminal du RCS (par ex: ici un S3). - - Ces semestres peuvent être : - - * des S1+S2+S1+S2+S3 si redoublement de la 1ère année - * des S1+S2+(année de césure)+S3 si césure, ... - - Args: - nom_rcs: Un nom du RCS (par ex: '5S') - semestre_final: Le formsemestre final du RCS - """ - - def __init__(self, nom_rcs: str, semestre_final: FormSemestre): - pe_rcs.RCS.__init__(self, nom_rcs, semestre_final) - - self.semestres_aggreges: dict[int:FormSemestre] = {} - """Formsemestres regroupés dans le RCS""" - - def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]): - """Ajout de semestres aux semestres à regrouper - - Args: - semestres: Dictionnaire ``{fid: Formsemestre)`` - """ - self.semestres_aggreges = self.semestres_aggreges | semestres - - def get_repr(self, verbose=True) -> str: - """Représentation textuelle d'un RCS - basé sur ses semestres aggrégés""" - title = f"""{self.__class__.__name__} {pe_rcs.RCS.__str__(self)}""" - if verbose: - noms = [] - for fid in self.semestres_aggreges: - semestre = self.semestres_aggreges[fid] - noms.append(f"S{semestre.semestre_id}(#{fid})") - noms = sorted(noms) - if noms: - title += " <" + "+".join(noms) + ">" - else: - title += " " - return title diff --git a/app/pe/rcss/pe_rcrcf.py b/app/pe/rcss/pe_rcrcf.py deleted file mode 100644 index f44d9fb3..00000000 --- a/app/pe/rcss/pe_rcrcf.py +++ /dev/null @@ -1,69 +0,0 @@ -############################################################################## -# Module "Avis de poursuite d'étude" -# conçu et développé par Cléo Baras (IUT de Grenoble) -############################################################################## - -""" -Created on 01-2024 - -@author: barasc -""" - -import app.pe.pe_comp as pe_comp -import app.pe.rcss.pe_rcf -import app.pe.rcss.rcss_constantes - -from app.models import FormSemestre -from app.pe import pe_sxtag, pe_affichage -from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date -import app.pe.rcss.pe_rcs as pe_rcs -import app.pe.rcss.pe_rcf as pe_rcf - - - -class RCRCF: - """Modélise les RCF d'étudiants suivis par un étudiant dans - le cadre d'un RCS donné (par ex: 3S=S1+S2+S3). - - Pour rappel : un RCF (par ex. S1) combine les semestres 1 qu'a suivi - l'étudiant pour valider son S1 (1 si étudiant standard, 2 si redoublant). - - Le RCRCF 3S est donc le regroupement du RCF S1 + RCF S2 + RCF S3. - - Il est identifié par le formsemestre de S3 marquant la fin du regroupement. - - - Args: - nom_rcs: Un nom du RCS (par ex: '5S') - semestre_final: Le semestre final du RCS - """ - - def __init__(self, nom_rcs: str, semestre_final: FormSemestre): - pe_rcs.RCS.__init__(self, nom_rcs, semestre_final) - - self.rcfs_aggreges: dict[(str, int) : pe_sxtag.SxTag] = {} - """Les RCFs à aggréger""" - - def add_rcfs_a_aggreger(self, rcfs: dict[(str, int): app.pe.rcss.pe_rcf.RCF]): - """Ajout des RCFs aux RCFS à regrouper - - Args: - rcfs: Dictionnaire ``{(str,fid): RCF}`` à ajouter - """ - self.rcfs_aggreges = self.rcfs_aggreges | rcfs - - def get_repr(self, verbose=True) -> str: - """Représentation textuelle d'un RCSF - basé sur ses RCF aggrégés""" - title = f"""{self.__class__.__name__}{pe_rcs.RCS.__str__(self)}""" - if verbose: - noms = [] - for rcf_id, rcf in self.rcfs_aggreges.items(): - noms.append(rcf.get_repr(verbose=False)) - if noms: - title += " <<" + "+".join(noms) + ">>" - else: - title += " <>" - return title - - diff --git a/app/pe/rcss/pe_rcs.py b/app/pe/rcss/pe_rcs.py index 72a3669b..f9babeee 100644 --- a/app/pe/rcss/pe_rcs.py +++ b/app/pe/rcss/pe_rcs.py @@ -10,32 +10,100 @@ Created on 01-2024 """ from app.models import FormSemestre -import app.pe.rcss.rcss_constantes as rcss_constantes +TYPES_RCS = { + "S1": { + "aggregat": ["S1"], + "descr": "Semestre 1 (S1)", + }, + "S2": { + "aggregat": ["S2"], + "descr": "Semestre 2 (S2)", + }, + "1A": { + "aggregat": ["S1", "S2"], + "descr": "BUT1 (S1+S2)", + }, + "S3": { + "aggregat": ["S3"], + "descr": "Semestre 3 (S3)", + }, + "S4": { + "aggregat": ["S4"], + "descr": "Semestre 4 (S4)", + }, + "2A": { + "aggregat": ["S3", "S4"], + "descr": "BUT2 (S3+S4)", + }, + "3S": { + "aggregat": ["S1", "S2", "S3"], + "descr": "Moyenne du semestre 1 au semestre 3 (S1+S2+S3)", + }, + "4S": { + "aggregat": ["S1", "S2", "S3", "S4"], + "descr": "Moyenne du semestre 1 au semestre 4 (S1+S2+S3+S4)", + }, + "S5": { + "aggregat": ["S5"], + "descr": "Semestre 5 (S5)", + }, + "S6": { + "aggregat": ["S6"], + "descr": "Semestre 6 (S6)", + }, + "3A": { + "aggregat": ["S5", "S6"], + "descr": "3ème année (S5+S6)", + }, + "5S": { + "aggregat": ["S1", "S2", "S3", "S4", "S5"], + "descr": "Moyenne du semestre 1 au semestre 5 (S1+S2+S3+S4+S5)", + }, + "6S": { + "aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"], + "descr": "Moyenne globale (S1+S2+S3+S4+S5+S6)", + }, +} +"""Dictionnaire détaillant les différents regroupements cohérents +de semestres (RCS), en leur attribuant un nom et en détaillant +le nom des semestres qu'ils regroupent et l'affichage qui en sera fait +dans les tableurs de synthèse. +""" + +TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS if not cle.startswith("S")] +TOUS_LES_RCS = list(TYPES_RCS.keys()) +TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")] def get_descr_rcs(nom_rcs: str) -> str: """Renvoie la description pour les tableurs de synthèse Excel d'un nom de RCS""" - return rcss_constantes.TYPES_RCS[nom_rcs]["descr"] + return TYPES_RCS[nom_rcs]["descr"] class RCS: - """Modélise un regroupement cohérent de semestres (formsemestre ou de Sx)""" + """Modélise un regroupement cohérent de semestres, + tous se terminant par un (form)semestre final. + """ def __init__(self, nom_rcs: str, semestre_final: FormSemestre): self.nom: str = nom_rcs """Nom du RCS""" + assert self.nom in TOUS_LES_RCS, "Le nom d'un RCS doit être un aggrégat" self.formsemestre_final: FormSemestre = semestre_final - """FormSemestre terminal du RCS""" + """(Form)Semestre final du RCS""" self.rang_final = self.formsemestre_final.semestre_id - """Le rang du formsemestre final""" + """Rang du formsemestre final""" self.rcs_id: (str, int) = (nom_rcs, semestre_final.formsemestre_id) """Identifiant du RCS sous forme (nom_rcs, id du semestre_terminal)""" + self.fid_final: int = self.formsemestre_final.formsemestre_id + """Identifiant du (Form)Semestre final""" + def get_formsemestre_id_final(self) -> int: """Renvoie l'identifiant du formsemestre final du RCS @@ -58,5 +126,3 @@ class RCS: self.nom == other.nom and self.formsemestre_final == other.formsemestre_final ) - - diff --git a/app/pe/rcss/pe_rcsemx.py b/app/pe/rcss/pe_rcsemx.py new file mode 100644 index 00000000..8c6bb0b0 --- /dev/null +++ b/app/pe/rcss/pe_rcsemx.py @@ -0,0 +1,59 @@ +############################################################################## +# Module "Avis de poursuite d'étude" +# conçu et développé par Cléo Baras (IUT de Grenoble) +############################################################################## + +""" +Created on 01-2024 + +@author: barasc +""" + +from app.models import FormSemestre +from app.pe import pe_sxtag, pe_affichage +from app.pe.rcss import pe_rcs, pe_trajectoires + + +class RCSemX(pe_rcs.RCS): + """Modélise un regroupement cohérent de SemX (en même regroupant + des semestres Sx combinés pour former les résultats des étudiants + au semestre de rang x) dans le but de synthétiser les résultats + du S1 jusqu'au semestre final ciblé par le RCSemX (dépendant de l'aggrégat + visé). + + Par ex: Si l'aggrégat du RCSemX est '3S' (=S1+S2+S3), + regroupement le SemX du S1 + le SemX du S2 + le SemX du S3 (chacun + incluant des infos sur les redoublements). + + Args: + nom_rcs: Un nom du RCS (par ex: '5S') + semestre_final: Le semestre final du RCS + """ + + def __init__(self, nom_rcs: str, semestre_final: FormSemestre): + pe_rcs.RCS.__init__(self, nom_rcs, semestre_final) + + self.semXs_aggreges: dict[(str, int) : pe_sxtag.SxTag] = {} + """Les semX à aggréger""" + + def add_semXs(self, semXs: dict[(str, int) : pe_trajectoires.SemX]): + """Ajoute des semXs aux semXs à regrouper dans le RCSemX + + Args: + semXs: Dictionnaire ``{(str,fid): RCF}`` à ajouter + """ + self.semXs_aggreges = self.semXs_aggreges | semXs + + def get_repr(self, verbose=True) -> str: + """Représentation textuelle d'un RCSF + basé sur ses RCF aggrégés""" + title = f"""{self.__class__.__name__} {pe_rcs.RCS.__str__(self)}""" + if verbose: + noms = [] + for semx_id, semx in self.semXs_aggreges.items(): + noms.append(semx.get_repr(verbose=False)) + if noms: + title += " <<" + "+".join(noms) + ">>" + else: + title += " <>" + return title diff --git a/app/pe/rcss/pe_trajectoires.py b/app/pe/rcss/pe_trajectoires.py new file mode 100644 index 00000000..ed13b17f --- /dev/null +++ b/app/pe/rcss/pe_trajectoires.py @@ -0,0 +1,87 @@ +from app.models import FormSemestre +import app.pe.rcss.pe_rcs as pe_rcs + + +class Trajectoire(pe_rcs.RCS): + """Regroupement Cohérent de Semestres ciblant un type d'aggrégat (par ex. + 'S2', '3S', '1A') et un semestre final, et dont les données regroupées + sont des **FormSemestres** suivis par les étudiants. + + Une *Trajectoire* traduit la succession de semestres + qu'ont pu suivre des étudiants pour aller d'un semestre S1 jusqu'au semestre final + de l'aggrégat. + + Une *Trajectoire* peut être : + + * un RCS de semestre de type "Sx" (cf. classe "SemX"), qui stocke les + formsemestres de rang x qu'ont suivi l'étudiant pour valider le Sx + (en général 1 formsemestre pour les non-redoublants et 2 pour les redoublants) + + * un RCS de type iS ou iA (par ex, 3A=S1+S2+S3), qui identifie + les formsemestres que des étudiants ont suivis pour les amener jusqu'au semestre + terminal du RCS. Par ex: si le RCS est un 3S: + + * des S1+S2+S1+S2+S3 si redoublement de la 1ère année + * des S1+S2+(année de césure)+S3 si césure, ... + + Args: + nom_rcs: Un nom du RCS (par ex: '5S') + semestre_final: Le formsemestre final du RCS + """ + + def __init__(self, nom_rcs: str, semestre_final: FormSemestre): + pe_rcs.RCS.__init__(self, nom_rcs, semestre_final) + + self.semestres_aggreges: dict[int:FormSemestre] = {} + """Formsemestres regroupés dans le RCS""" + + def add_semestres(self, semestres: dict[int:FormSemestre]): + """Ajout de semestres aux semestres à regrouper + + Args: + semestres: Dictionnaire ``{fid: Formsemestre)`` + """ + for sem in semestres.values(): + assert isinstance( + sem, FormSemestre + ), "Les données aggrégées d'une Trajectoire doivent être des FormSemestres" + self.semestres_aggreges = self.semestres_aggreges | semestres + + def get_repr(self, verbose=True) -> str: + """Représentation textuelle d'un RCS + basé sur ses semestres aggrégés""" + title = f"""{self.__class__.__name__} {pe_rcs.RCS.__str__(self)}""" + if verbose: + noms = [] + for fid in self.semestres_aggreges: + semestre = self.semestres_aggreges[fid] + noms.append(f"S{semestre.semestre_id}#{fid}") + noms = sorted(noms) + if noms: + title += " <" + "+".join(noms) + ">" + else: + title += " " + return title + + +class SemX(Trajectoire): + """Trajectoire (regroupement cohérent de (form)semestres + dans laquelle tous les semestres regroupés sont de même rang `x`. + + Les SemX stocke les + formsemestres de rang x qu'ont suivi l'étudiant pour valider le Sx + (en général 1 formsemestre pour les non-redoublants et 2 pour les redoublants). + + Ils servent à calculer les SemXTag (moyennes par tag des RCS de type `Sx`). + """ + + def __init__(self, trajectoire: Trajectoire): + Trajectoire.__init__(self, trajectoire.nom, trajectoire.formsemestre_final) + + semestres_aggreges = trajectoire.semestres_aggreges + for sem in semestres_aggreges.values(): + assert ( + sem.semestre_id == trajectoire.rang_final + ), "Tous les semestres aggrégés d'un SemX doivent être de même rang" + + self.semestres_aggreges = trajectoire.semestres_aggreges diff --git a/app/pe/rcss/rcss_constantes.py b/app/pe/rcss/rcss_constantes.py deleted file mode 100644 index ee79de27..00000000 --- a/app/pe/rcss/rcss_constantes.py +++ /dev/null @@ -1,64 +0,0 @@ - -TYPES_RCS = { - "S1": { - "aggregat": ["S1"], - "descr": "Semestre 1 (S1)", - }, - "S2": { - "aggregat": ["S2"], - "descr": "Semestre 2 (S2)", - }, - "1A": { - "aggregat": ["S1", "S2"], - "descr": "BUT1 (S1+S2)", - }, - "S3": { - "aggregat": ["S3"], - "descr": "Semestre 3 (S3)", - }, - "S4": { - "aggregat": ["S4"], - "descr": "Semestre 4 (S4)", - }, - "2A": { - "aggregat": ["S3", "S4"], - "descr": "BUT2 (S3+S4)", - }, - "3S": { - "aggregat": ["S1", "S2", "S3"], - "descr": "Moyenne du semestre 1 au semestre 3 (S1+S2+S3)", - }, - "4S": { - "aggregat": ["S1", "S2", "S3", "S4"], - "descr": "Moyenne du semestre 1 au semestre 4 (S1+S2+S3+S4)", - }, - "S5": { - "aggregat": ["S5"], - "descr": "Semestre 5 (S5)", - }, - "S6": { - "aggregat": ["S6"], - "descr": "Semestre 6 (S6)", - }, - "3A": { - "aggregat": ["S5", "S6"], - "descr": "3ème année (S5+S6)", - }, - "5S": { - "aggregat": ["S1", "S2", "S3", "S4", "S5"], - "descr": "Moyenne du semestre 1 au semestre 5 (S1+S2+S3+S4+S5)", - }, - "6S": { - "aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"], - "descr": "Moyenne globale (S1+S2+S3+S4+S5+S6)", - }, -} -"""Dictionnaire détaillant les différents regroupements cohérents -de semestres (RCS), en leur attribuant un nom et en détaillant -le nom des semestres qu'ils regroupent et l'affichage qui en sera fait -dans les tableurs de synthèse. -""" - -TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS if not cle.startswith("S")] -TOUS_LES_RCS = list(TYPES_RCS.keys()) -TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")] \ No newline at end of file From b5125fa3d752e565642256c7da712689d8d7a62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 20 Feb 2024 20:52:44 +0100 Subject: [PATCH 18/41] =?UTF-8?q?G=C3=A9n=C3=A8re=20les=20RCSTag=20(mais?= =?UTF-8?q?=20sont-ils=20bons=20=3F=3F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_jury.py | 10 ++-- app/pe/pe_rcss_jury.py | 60 ++++++++++++--------- app/pe/pe_rcstag.py | 119 ++++++++++++++++++++++------------------- 3 files changed, 103 insertions(+), 86 deletions(-) diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 030e3ea7..7bfbf8ec 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -108,8 +108,8 @@ class JuryPE(object): self._gen_trajectoires() self._gen_semXs() self._gen_xls_sxtags(zipfile) - # self._gen_rcsemxs() - # self._gen_xls_rcrcss_tags(zipfile) + self._gen_rcsemxs() + self._gen_xls_rcstags(zipfile) # self._gen_xls_interclassements_rcss(zipfile) # self._gen_xls_synthese_jury_par_tag(zipfile) # self._gen_xls_synthese_par_etudiant(zipfile) @@ -255,7 +255,7 @@ class JuryPE(object): self.rcss_jury.cree_rcsemxs(self.etudiants) self.rcss_jury._aff_rcsemxs_suivis(self.etudiants) - def _gen_xls_rcrcss_tags(self, zipfile: ZipFile): + def _gen_xls_rcstags(self, zipfile: ZipFile): """Génère les RCS taggués traduisant les moyennes (orientées compétences) de regroupements de semestre de type Sx, xA ou xS. @@ -278,8 +278,8 @@ class JuryPE(object): pe_affichage.pe_print("*** Calcule les moyennes des RC de RCFS") self.rcss_tags = {} - for rcs_id, rcrcf in self.rcss_jury.rcsemxs.items(): - self.rcss_tags[rcs_id] = RCSTag(rcrcf, self.sxtags) + for rcs_id, rcsemx in self.rcss_jury.rcsemxs.items(): + self.rcss_tags[rcs_id] = RCSTag(rcsemx, self.sxtags) # Intègre le bilan des trajectoires tagguées au zip final output = io.BytesIO() diff --git a/app/pe/pe_rcss_jury.py b/app/pe/pe_rcss_jury.py index 65e423b0..6b0fe5f6 100644 --- a/app/pe/pe_rcss_jury.py +++ b/app/pe/pe_rcss_jury.py @@ -142,22 +142,25 @@ class RCSsJuryPE: analysés (trajectoires traduisant son parcours dans les différents semestres) + les mémorise dans les données de l'étudiant """ - self.rcsemxs_suivis = {nom_rcs: None for nom_rcs in pe_rcs.TOUS_LES_RCS} + self.rcsemxs_suivis = {} self.rcsemxs = {} # Pour tous les étudiants du jury for etudid in self.trajectoires_suivies: - self.rcsemxs_suivis[etudid] = {} + self.rcsemxs_suivis[etudid] = { + nom_rcs: None for nom_rcs in pe_rcs.TOUS_LES_RCS_AVEC_PLUSIEURS_SEM + } - # Recopie des SemX & des suivis associés - for nom_rcs in pe_rcs.TOUS_LES_SEMESTRES: - trajectoire = self.semXs_suivis[etudid][nom_rcs] - if trajectoire: - self.rcsemxs[trajectoire.rcs_id] = trajectoire - self.rcsemxs_suivis[etudid][nom_rcs] = trajectoire + # Recopie des SemX & des suivis associés => est-ce utile ? + # for nom_rcs in pe_rcs.TOUS_LES_SEMESTRES: + # trajectoire = self.semXs_suivis[etudid][nom_rcs] + # if trajectoire: + # self.rcsemxs[trajectoire.rcs_id] = trajectoire + # self.rcsemxs_suivis[etudid][nom_rcs] = trajectoire # Pour chaque aggréggat de type xA ou Sx - for nom_rcs in pe_rcs.TOUS_LES_RCS_AVEC_PLUSIEURS_SEM: + tous_les_agregats = pe_rcs.TOUS_LES_RCS + for nom_rcs in tous_les_agregats: trajectoire = self.trajectoires_suivies[etudid][nom_rcs] if not trajectoire: self.rcsemxs_suivis[etudid][nom_rcs] = None @@ -179,16 +182,20 @@ class RCSsJuryPE: semxs_a_aggreger = {} for Sx in noms_sems_aggregat: semestres_etudiants = etudiants.cursus[etudid][Sx] - - semx_id = get_semx_from_semestres_aggreges( - self.semXs, semestres_etudiants - ) - if not semx_id: - raise ( - "Il manque un SemX pour créer les RCSemX dans cree_rcsemxs" + if not semestres_etudiants: + pe_affichage.pe_print( + f"-> ⚠ Pas de semestres {Sx} pour {etudiants.identites[etudid].etat_civil}" ) - # Les SemX à ajouter au RCSemX - semxs_a_aggreger[semx_id] = self.semXs[semx_id] + else: + semx_id = get_semx_from_semestres_aggreges( + self.semXs, semestres_etudiants + ) + if not semx_id: + raise ( + "Il manque un SemX pour créer les RCSemX dans cree_rcsemxs" + ) + # Les SemX à ajouter au RCSemX + semxs_a_aggreger[semx_id] = self.semXs[semx_id] # Ajout des SemX à ceux à aggréger dans le RCSemX rcsemx = self.rcsemxs[tid] @@ -200,15 +207,16 @@ class RCSsJuryPE: def _aff_rcsemxs_suivis(self, etudiants): """Affiche les RCSemX suivis par les étudiants""" # Affichage pour debug - jeunes = list(enumerate(self.rcsemxs_suivis)) + jeunes = list(enumerate(self.rcsemxs_suivis.keys())) vides = [] - for no_etud, etudid in jeunes[:20]: - pe_affichage.pe_print(f"-> {etudiants.identites[etudid].nomprenom} :") - for nom_rcs, rcs in self.rcsemxs_suivis[etudid].items(): - if rcs: - pe_affichage.pe_print(f" > RCSemX {nom_rcs}: {rcs.get_repr()}") - else: - vides += [f"{nom_rcs}"] + for no_etud, etudid in jeunes: + if etudid not in etudiants.abandons_ids: + pe_affichage.pe_print(f"-> {etudiants.identites[etudid].nomprenom} :") + for nom_rcs, rcs in self.rcsemxs_suivis[etudid].items(): + if rcs: + pe_affichage.pe_print(f" > RCSemX {nom_rcs}: {rcs.get_repr()}") + else: + vides += [f"{nom_rcs}"] pe_affichage.pe_print(f"-> ⚠ RCSemX vides : {', '.join(list(set(vides)))}") diff --git a/app/pe/pe_rcstag.py b/app/pe/pe_rcstag.py index d675483f..f8f6b9dc 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/pe_rcstag.py @@ -41,8 +41,7 @@ from app.models import FormSemestre from app.pe import pe_affichage import pandas as pd import numpy as np -import app.pe.rcss.pe_rcs as pe_rcs -import app.pe.rcss.pe_rcsemx as pe_rcrcf +from app.pe.rcss import pe_rcs, pe_rcsemx import app.pe.pe_sxtag as pe_sxtag import app.pe.pe_comp as pe_comp from app.pe.pe_tabletags import TableTag @@ -50,7 +49,9 @@ from app.pe.pe_moytag import MoyennesTag class RCSTag(TableTag): - def __init__(self, rcrcf: pe_rcs.RCS, sxstags: dict[(str, int) : pe_sxtag.SxTag]): + def __init__( + self, rcsemx: pe_rcsemx.RCSemX, sxstags: dict[(str, int) : pe_sxtag.SxTag] + ): """Calcule les moyennes par tag (orientées compétences) d'un regroupement de SxTag (RCRCF), pour extraire les classements par tag pour un @@ -58,42 +59,43 @@ class RCSTag(TableTag): participé au même semestre terminal. Args: - rcs: Un RCS (identifié par un nom et l'id de son semestre terminal) - sxstags: Les données sur les RCF taggués + rcsemx: Le RCSemX (identifié par un nom et l'id de son semestre terminal) + sxstags: Les données sur les SemX taggués """ TableTag.__init__(self) - self.rcs_id: tuple(str, int) = rcrcf.rcs_id - """Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)""" + self.rcs_id: tuple(str, int) = rcsemx.rcs_id + """Identifiant du RCSTag (identique au RCSemX sur lequel il s'appuie)""" - self.rcrcf: pe_rcrcf.RCSemX = rcrcf - """RCRCF associé au RCS taggué""" + self.rcsemx: pe_rcsemx.RCSemX = rcsemx + """RCSemX associé au RCSTag""" self.nom = self.get_repr() - """Représentation textuelle du RCS taggué""" + """Représentation textuelle du RSCtag""" # Les données du semestre final - self.formsemestre_terminal: FormSemestre = rcrcf.formsemestre_final + self.formsemestre_final: FormSemestre = rcsemx.formsemestre_final """Le semestre final""" - self.fid_final: int = rcrcf.formsemestre_final.formsemestre_id + self.fid_final: int = rcsemx.formsemestre_final.formsemestre_id """Le fid du semestre final""" # Affichage pour debug pe_affichage.pe_print(f"-> {self.get_repr(verbose=True)}") # Les données aggrégés (RCRCF + SxTags - self.rcsemxs_aggreges = rcrcf.rcsemxs_aggreges - """Les RCFs aggrégés""" + self.semXs_aggreges: dict[(str, int) : pe_rcsemx.RCSemX] = rcsemx.semXs_aggreges + """Les SemX aggrégés""" self.sxstags = {} - """Les SxTag associés aux RCF aggrégés""" + """Les SxTag associés aux SemX aggrégés""" try: - for rcf_id in self.rcsemxs_aggreges: + for rcf_id in self.semXs_aggreges: self.sxstags[rcf_id] = sxstags[rcf_id] except: raise ValueError("Semestres SxTag manquants") # Les étudiants (etuds, états civils & etudis) - sxtag_final = self.sxstags[self.rcs_id] + sems_dans_aggregat = pe_rcs.TYPES_RCS[self.rcs_id[0]]["aggregat"] + sxtag_final = self.sxstags[(sems_dans_aggregat[-1], self.rcs_id[1])] self.etuds = sxtag_final.etuds """Les étudiants (extraits du semestre final)""" self.add_etuds(self.etuds) @@ -106,7 +108,10 @@ class RCSTag(TableTag): pe_affichage.pe_print( f"* Association UEs -> compétences : {self.acronymes_ues_to_competences}" ) - self.competences_sorted = sorted(self.acronymes_ues_to_competences.values()) + self.competences_sorted = sorted( + set(self.acronymes_ues_to_competences.values()) + ) + """Compétences (triées par nom, extraites des SxTag aggrégés)""" pe_affichage.pe_print(f"* Compétences : {', '.join(self.competences_sorted)}") @@ -119,6 +124,7 @@ class RCSTag(TableTag): self.moyennes_tags: dict[str, MoyennesTag] = {} """Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)""" for tag in self.tags_sorted: + print(tag) # Cube de notes (etudids_sorted x compétences_sorted x sxstags) notes_df, notes_cube = self.compute_notes_comps_cube( tag, self.etudids_sorted, self.competences_sorted, self.sxstags @@ -153,7 +159,7 @@ class RCSTag(TableTag): """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle est basée)""" if verbose: - return self.rcrcf.get_repr(verbose=verbose) + return self.rcsemx.get_repr(verbose=verbose) else: return f"{self.__class__.__name__} ({self.rcs_id})" @@ -182,26 +188,29 @@ class RCSTag(TableTag): np.nan, index=etudids_sorted, columns=competences_sorted ) # Charge les notes du semestre tag (copie car changement de nom de colonnes à venir) - moys_tag = sxtag.moyennes_tags[tag] - notes = moys_tag.matrice_notes.copy() # avec une copie + if tag in sxtag.moyennes_tags: # si le tag est présent dans le semestre + moys_tag = sxtag.moyennes_tags[tag] - # Traduction des acronymes d'UE en compétences - acronymes_ues_columns = notes.columns - acronymes_to_comps = [ - self.acronymes_ues_to_competences[acro] - for acro in acronymes_ues_columns - ] - notes.columns = acronymes_to_comps + notes = moys_tag.matrice_notes.copy() # avec une copie - # Les étudiants et les compétences communes - etudids_communs, comp_communes = pe_comp.find_index_and_columns_communs( - notes_df, notes - ) + # Traduction des acronymes d'UE en compétences + acronymes_ues_columns = notes.columns + acronymes_to_comps = [ + self.acronymes_ues_to_competences[acro] + for acro in acronymes_ues_columns + ] + notes.columns = acronymes_to_comps - # Recopie des notes et des coeffs - notes_df.loc[etudids_communs, comp_communes] = notes.loc[ - etudids_communs, comp_communes - ] + # Les étudiants et les compétences communes + ( + etudids_communs, + comp_communes, + ) = pe_comp.find_index_and_columns_communs(notes_df, notes) + + # Recopie des notes et des coeffs + notes_df.loc[etudids_communs, comp_communes] = notes.loc[ + etudids_communs, comp_communes + ] # Supprime tout ce qui n'est pas numérique # for col in notes_df.columns: @@ -242,29 +251,29 @@ class RCSTag(TableTag): coeffs_df = pd.DataFrame( np.nan, index=etudids_sorted, columns=competences_sorted ) + if tag in sxtag.moyennes_tags: + moys_tag = sxtag.moyennes_tags[tag] - moys_tag = sxtag.moyennes_tags[tag] + # Charge les notes et les coeffs du semestre tag + coeffs = moys_tag.matrice_coeffs_moy_gen.copy() # les coeffs - # Charge les notes et les coeffs du semestre tag - coeffs = moys_tag.matrice_coeffs_moy_gen.copy() # les coeffs + # Traduction des acronymes d'UE en compétences + acronymes_ues_columns = coeffs.columns + acronymes_to_comps = [ + self.acronymes_ues_to_competences[acro] + for acro in acronymes_ues_columns + ] + coeffs.columns = acronymes_to_comps - # Traduction des acronymes d'UE en compétences - acronymes_ues_columns = coeffs.columns - acronymes_to_comps = [ - self.acronymes_ues_to_competences[acro] - for acro in acronymes_ues_columns - ] - coeffs.columns = acronymes_to_comps + # Les étudiants et les compétences communes + etudids_communs, comp_communes = pe_comp.find_index_and_columns_communs( + coeffs_df, coeffs + ) - # Les étudiants et les compétences communes - etudids_communs, comp_communes = pe_comp.find_index_and_columns_communs( - coeffs_df, coeffs - ) - - # Recopie des notes et des coeffs - coeffs_df.loc[etudids_communs, comp_communes] = coeffs.loc[ - etudids_communs, comp_communes - ] + # Recopie des notes et des coeffs + coeffs_df.loc[etudids_communs, comp_communes] = coeffs.loc[ + etudids_communs, comp_communes + ] # Stocke les dfs coeffs_dfs[sxtag_id] = coeffs_df From 40a57a9b86c5243f49edfedae2cd7f9bfc3c7e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Tue, 20 Feb 2024 21:12:18 +0100 Subject: [PATCH 19/41] =?UTF-8?q?Etat=20interm=C3=A9diaire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_interclasstag.py | 14 ++++++-------- app/pe/pe_jury.py | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/pe/pe_interclasstag.py b/app/pe/pe_interclasstag.py index 9d5b998d..3de44d23 100644 --- a/app/pe/pe_interclasstag.py +++ b/app/pe/pe_interclasstag.py @@ -69,17 +69,15 @@ class RCSInterclasseTag(TableTag): """Le nom du RCS interclassé""" self.nom = self.get_repr() + """Représentation textuelle de l'interclassement""" - """Les étudiants diplômés et leurs rcss""" # 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 - } + """Identité des étudiants diplômés""" - # Les trajectoires (et leur version tagguées), en ne gardant que + self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids} + """Etudids des étudiants diplômés""" + + # Les RCSemX et leur versions taggués SxTag, en ne gardant que # celles associées à l'aggrégat self.rcss: dict[int, pe_rcs.RCS] = {} """Ensemble des trajectoires associées à l'aggrégat""" diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 7bfbf8ec..92fb6f95 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -306,7 +306,7 @@ class JuryPE(object): def _gen_xls_interclassements_rcss(self, zipfile: ZipFile): """Intègre le bilan des RCS (interclassé par promo) au zip""" # Génère les interclassements (par promo et) par (nom d') aggrégat - pe_affichage.pe_print("*** Génère les interclassements par aggrégat") + pe_affichage.pe_print("*** Génère les interclassements") self.interclassements_taggues = compute_interclassements( self.etudiants, self.rcss_jury, self.rcss_tags ) From 746314b2fbfd7cd8975e4d8311e4c7f5af4358c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Wed, 21 Feb 2024 20:02:38 +0100 Subject: [PATCH 20/41] =?UTF-8?q?Etat=20interm=C3=A9diaire=20sur=20les=20i?= =?UTF-8?q?nterclassements=20et=20la=20synth=C3=A8se=20du=20jury=20(donn?= =?UTF-8?q?=C3=A9es=20sans=20notes=20ou=20tags=20=C3=A0=20revoir)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/moys/__init__.py | 0 app/pe/moys/pe_interclasstag.py | 325 +++++++++++++++++++++ app/pe/{pe_moytag.py => moys/pe_moy.py} | 108 +------ app/pe/moys/pe_moytag.py | 155 ++++++++++ app/pe/{ => moys}/pe_rcstag.py | 20 +- app/pe/{ => moys}/pe_ressemtag.py | 27 +- app/pe/{ => moys}/pe_sxtag.py | 16 +- app/pe/moys/pe_tabletags.py | 204 +++++++++++++ app/pe/pe_interclasstag.py | 160 ---------- app/pe/pe_jury.py | 372 +++++++++++------------- app/pe/pe_tabletags.py | 122 -------- app/pe/rcss/pe_rcsemx.py | 2 +- 12 files changed, 897 insertions(+), 614 deletions(-) create mode 100644 app/pe/moys/__init__.py create mode 100644 app/pe/moys/pe_interclasstag.py rename app/pe/{pe_moytag.py => moys/pe_moy.py} (58%) create mode 100644 app/pe/moys/pe_moytag.py rename app/pe/{ => moys}/pe_rcstag.py (97%) rename app/pe/{ => moys}/pe_ressemtag.py (95%) rename app/pe/{ => moys}/pe_sxtag.py (97%) create mode 100644 app/pe/moys/pe_tabletags.py delete mode 100644 app/pe/pe_interclasstag.py delete mode 100644 app/pe/pe_tabletags.py diff --git a/app/pe/moys/__init__.py b/app/pe/moys/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/pe/moys/pe_interclasstag.py b/app/pe/moys/pe_interclasstag.py new file mode 100644 index 00000000..92114f41 --- /dev/null +++ b/app/pe/moys/pe_interclasstag.py @@ -0,0 +1,325 @@ +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +############################################################################## +# Module "Avis de poursuite d'étude" +# conçu et développé par Cléo Baras (IUT de Grenoble) +############################################################################## + +""" +Created on Thu Sep 8 09:36:33 2016 + +@author: barasc +""" + +import pandas as pd +import numpy as np + +from app.models import Identite +from app.pe.moys import pe_tabletags, pe_moy, pe_moytag, pe_sxtag +from app.pe.rcss import pe_rcs +import app.pe.pe_comp as pe_comp + + +class InterClassTag(pe_tabletags.TableTag): + """ + Interclasse l'ensemble des étudiants diplômés à une année + donnée (celle du jury), pour un RCS donné (par ex: 'S2', '3S'), qu'il soit + de type SemX ou RCSemX, + en reportant les moyennes obtenues sur à la version tagguée + du RCS (de type SxTag ou RCSTag). + Sont ensuite calculés les classements (uniquement) + sur les étudiants diplômes. + + Args: + nom_rcs: Le nom de l'aggrégat + type_interclassement: Le type d'interclassement (par UE ou par compétences) + etudiants_diplomes: L'identité des étudiants diplômés + rcss: Un dictionnaire {(nom_rcs, fid_final): RCS} donnant soit + les SemX soit les RCSemX recencés par le jury PE + rcstag: Un dictionnaire {(nom_rcs, fid_final): RCSTag} donnant + soit les SxTag (associés aux SemX) + soit les RCSTags (associés au RCSemX) calculés par le jury PE + suivis: Un dictionnaire associé à chaque étudiant son rcss + (de la forme ``{etudid: {nom_rcs: RCS_suivi}}``) + """ + + def __init__( + self, + nom_rcs: str, + type_interclassement: str, + etudiants_diplomes: dict[int, Identite], + rcss: dict[(str, int) : pe_rcs.RCS], + rcstags: dict[(str, int) : pe_tabletags.TableTag], + suivis: dict[int:dict], + ): + pe_tabletags.TableTag.__init__(self) + + self.nom_rcs: str = nom_rcs + """Le nom du RCS interclassé""" + + # Le type d'interclassement + self.type = type_interclassement + + # Les informations sur les étudiants diplômés + self.etuds: list[Identite] = list(etudiants_diplomes.values()) + """Identités des étudiants diplômés""" + self.add_etuds(self.etuds) + + self.diplomes_ids = set(etudiants_diplomes.keys()) + """Etudids des étudiants diplômés""" + + # Les RCS de l'aggrégat (SemX ou RCSemX) + self.rcss: dict[(str, int), pe_rcs.RCS] = {} + """Ensemble des SemX ou des RCSemX associés à l'aggrégat""" + for (nom, fid), rcs in rcss.items(): + if nom == nom_rcs: + self.rcss[(nom, fid)] = rcss + + # Les données tagguées + self.rcstags: dict[(str, int), pe_tabletags.TableTag] = {} + """Ensemble des SxTag ou des RCSTags associés à l'aggrégat""" + for rcs_id in self.rcss: + self.rcstags[rcs_id] = rcstags[rcs_id] + + # Les RCS (SemX ou RCSemX) suivis par les étudiants du jury, + # en ne gardant que ceux associés aux diplomés + self.suivis: dict[int, pe_rcs.RCS] = {} + """Association entre chaque étudiant et le SxTag ou RCSTag à prendre + pour l'aggrégat""" + for etudid in self.diplomes_ids: + self.suivis[etudid] = suivis[etudid][nom_rcs] + + # Les données sur les tags + self.tags_sorted = self._do_taglist() + """Liste des tags (triés par ordre alphabétique)""" + + # Les données sur les UEs (si SxTag) ou compétences (si RCSTag) + self.champs_sorted = self._do_ues_ou_competences_list() + + # Construit la matrice de notes + etudids_sorted = sorted(list(self.diplomes_ids)) + + self.nom = self.get_repr() + """Représentation textuelle de l'interclassement""" + + # Synthétise les moyennes/classements par tag + self.moyennes_tags: dict[str, pe_moytag.MoyennesTag] = {} + for tag in self.tags_sorted: + notes = self.compute_notes_matrice(tag, etudids_sorted, self.champs_sorted) + coeffs = self.compute_coeffs_matrice( + tag, etudids_sorted, self.champs_sorted + ) + self.moyennes_tags[tag] = pe_moytag.MoyennesTag( + tag, + self.type, + notes, + coeffs, # limite les moyennes aux étudiants de la promo + ) + + def get_repr(self) -> str: + """Une représentation textuelle""" + return f"{self.nom_rcs} par {self.type}" + + def _do_taglist(self): + """Synthétise les tags à partir des TableTags (SXTag ou RCSTag) + + Returns: + Une liste de tags triés par ordre alphabétique + """ + tags = [] + for rcstag in self.rcstags.values(): + tags.extend(rcstag.tags_sorted) + return sorted(set(tags)) + + def compute_notes_matrice( + self, tag, etudids_sorted: list[int], champs_sorted: list[str] + ) -> pd.DataFrame: + """Construit la matrice de notes (etudids x champs) en + reportant les moyennes obtenues par les étudiants + aux semestres de l'aggrégat pour le tag visé. + + Les champs peuvent être des acronymes d'UEs ou des compétences. + + Args: + etudids_sorted: Les etudids des étudiants (diplômés) triés + champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice + Return: + Le dataFrame (etudids x champs) + reportant les moyennes des étudiants aux champs + """ + + # Partant d'un dataframe vierge + df = pd.DataFrame(np.nan, index=etudids_sorted, columns=champs_sorted) + + for rcstag in self.rcstags.values(): + # Charge les moyennes au tag d'un RCStag + if tag in rcstag.moyennes_tags: + moytag: pd.DataFrame = rcstag.moyennes_tags[tag].matrice_notes + + # Etudiants/Champs communs entre le RCSTag et les données interclassées + ( + etudids_communs, + champs_communs, + ) = pe_comp.find_index_and_columns_communs(df, moytag) + + # Injecte les notes par tag + df.loc[etudids_communs, champs_communs] = moytag.loc[ + etudids_communs, champs_communs + ] + + return df + + def compute_coeffs_matrice( + self, tag, etudids_sorted: list[int], champs_sorted: list[str] + ) -> pd.DataFrame: + """Idem que compute_notes_matrices mais pour les coeffs + + Args: + etudids_sorted: Les etudids des étudiants (diplômés) triés + champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice + Return: + Le dataFrame (etudids x champs) + reportant les moyennes des étudiants aux champs + """ + + # Partant d'un dataframe vierge + df = pd.DataFrame(np.nan, index=etudids_sorted, columns=champs_sorted) + + for rcstag in self.rcstags.values(): + if tag in rcstag.moyennes_tags: + # Charge les coeffs au tag d'un RCStag + coeffs: pd.DataFrame = rcstag.moyennes_tags[tag].matrice_notes + + # Etudiants/Champs communs entre le RCSTag et les données interclassées + ( + etudids_communs, + champs_communs, + ) = pe_comp.find_index_and_columns_communs(df, coeffs) + + # Injecte les coeffs par tag + df.loc[etudids_communs, champs_communs] = coeffs.loc[ + etudids_communs, champs_communs + ] + + return df + + def _do_ues_ou_competences_list(self) -> list[str]: + """Synthétise les champs (UEs ou compétences) sur lesquels + sont calculés les moyennes. + + Returns: + Un dictionnaire {'acronyme_ue' : 'compétences'} + """ + dict_champs = [] + for rcstag in self.rcstags.values(): + if isinstance(rcstag, pe_sxtag.SxTag): + champs = rcstag.acronymes_sorted + else: # pe_rcstag.RCSTag + champs = rcstag.competences_sorted + dict_champs.extend(champs) + return sorted(set(dict_champs)) + + def has_tags(self): + """Indique si l'interclassement a des tags (cas d'un + interclassement sur un S5 qui n'a pas eu lieu) + """ + return len(self.tags_sorted) > 0 + + def _un_rcstag_significatif(self, rcsstags: dict[(str, int):pe_tabletags]): + """Renvoie un rcstag significatif (ayant des tags et des notes aux tags) + parmi le dictionnaire de rcsstags""" + for rcstag_id, rcstag in rcsstags.items(): + moystags: pe_moytag.MoyennesTag = rcstag.moyennes_tags + for tag, moystag in moystags.items(): + tags_tries = moystag.get_all_significant_tags() + if tags_tries: + return moystag + return None + + def compute_df_synthese_moyennes_tag( + self, tag, aggregat=None, type_colonnes=False + ) -> pd.DataFrame: + """Construit le dataframe retraçant pour les données des moyennes + pour affichage dans la synthèse du jury PE. (cf. to_df()) + + Args: + etudids_sorted: Les etudids des étudiants (diplômés) triés + champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice + Return: + Le dataFrame (etudids x champs) + reportant les moyennes des étudiants aux champs + """ + if aggregat: + assert ( + aggregat == self.nom_rcs + ), "L'interclassement ciblé ne correspond pas à l'aggrégat visé" + + etudids_sorted = sorted(list(self.diplomes_ids)) + + if self.rcstags: + return None + + # Un rcstag significatif pour initier les colonnes + moytag = self._un_rcstag_significatif(self.rcstags) + if not moytag: + return None + df_moytag = moytag.to_df( + aggregat=aggregat, + cohorte="Groupe", + ) + colonnes = list(df_moytag.columns) + + # Partant d'un dataframe vierge + df = pd.DataFrame(index=etudids_sorted, columns=colonnes) # colonnes) + for col in colonnes: + if "rang" in col: + df[col] = df[col].astype(str) + df.columns = list(range(len(colonnes))) + + for rcstag in self.rcstags.values(): + # Charge les moyennes au tag d'un RCStag (SemX ou RCSXTag) + if tag in rcstag.moyennes_tags: + # Les infos sur les moyennes du tag + moytag: pe_moytag.MoyennesTag = rcstag.moyennes_tags[tag] + df_moytag = moytag.to_df( + aggregat=aggregat, + cohorte="Groupe", + ) + df_moytag.columns = list(range(len(colonnes))) + + # Etudiants/Champs communs entre le df et les données interclassées + ( + etudids_communs, + champs_communs, # les colonnes de synthèse + ) = pe_comp.find_index_and_columns_communs(df, df_moytag) + + # Injecte les données par tag + df.loc[etudids_communs, champs_communs] = df_moytag.loc[ + etudids_communs, champs_communs + ] + + # Refixe les colonnes + df.columns = colonnes + return df diff --git a/app/pe/pe_moytag.py b/app/pe/moys/pe_moy.py similarity index 58% rename from app/pe/pe_moytag.py rename to app/pe/moys/pe_moy.py index 1029c66c..2342a7a1 100644 --- a/app/pe/pe_moytag.py +++ b/app/pe/moys/pe_moy.py @@ -1,13 +1,12 @@ import numpy as np import pandas as pd -from app import comp from app.comp.moy_sem import comp_ranks_series from app.pe import pe_affichage class Moyenne: - CRITERES = [ + COLONNES = [ "note", "classement", "rang", @@ -17,6 +16,9 @@ class Moyenne: "nb_etuds", "nb_inscrits", ] + """Colonnes du df""" + + COLONNES_SYNTHESE = ["note", "rang", "min", "max", "moy"] def __init__(self, notes: pd.Series): """Classe centralisant la synthèse des moyennes/classements d'une série @@ -58,7 +60,7 @@ class Moyenne: df = pd.DataFrame( np.nan, index=self.etudids, - columns=Moyenne.CRITERES, + columns=Moyenne.COLONNES, ) # Supprime d'éventuelles chaines de caractères dans les notes @@ -67,14 +69,18 @@ class Moyenne: # Les nb d'étudiants & nb d'inscrits df["nb_etuds"] = len(self.etudids) + df["nb_etuds"] = df["nb_etuds"].astype(int) + # Les étudiants dont la note n'est pas nulle inscrits_ids = notes[notes.notnull()].index.to_list() df.loc[inscrits_ids, "nb_inscrits"] = len(inscrits_ids) + # df["nb_inscrits"] = df["nb_inscrits"].astype(int) # Le classement des inscrits notes_non_nulles = notes[inscrits_ids] (class_str, class_int) = comp_ranks_series(notes_non_nulles) df.loc[inscrits_ids, "classement"] = class_int + # df["classement"] = df["classement"].astype(int) # Le rang (classement/nb_inscrit) df["rang"] = df["rang"].astype(str) @@ -91,6 +97,10 @@ class Moyenne: return df + def get_df_synthese(self): + """Renvoie le df de synthese limité aux colonnes de synthese""" + return self.df[self.COLONNES_SYNTHESE] + def to_dict(self) -> dict: """Renvoie un dictionnaire de synthèse des moyennes/classements/statistiques générale (but)""" synthese = { @@ -103,10 +113,6 @@ class Moyenne: } return synthese - def get_notes(self): - """Série des notes, arrondies à 2 chiffres après la virgule""" - return self.df_gen["note"].round(2) - def get_rangs_inscrits(self) -> pd.Series: """Série des rangs classement/nbre_inscrit""" return self.df_gen["rang"] @@ -151,91 +157,3 @@ class Moyenne: def is_significatif(self) -> bool: """Indique si la moyenne est significative (c'est-à-dire à des notes)""" return self.synthese["nb_inscrits"] > 0 - - -class MoyennesTag: - def __init__( - self, - tag: str, - matrice_notes: pd.DataFrame, # etudids x colonnes - matrice_coeffs: pd.DataFrame, # etudids x colonnes - ): - """Classe centralisant la synthèse des moyennes/classements d'une série - d'étudiants à un tag donné, en différenciant les notes - obtenues aux UE et au général (toutes UEs confondues) - - - Args: - tag: Un tag - matrice_notes: Les moyennes (etudid x acronymes_ues ou etudid x compétences) aux différentes UEs ou compétences - matrice_coeffs: Les coeff à appliquer pour le calcul de la moyenne générale - # notes_gen: Une série de notes (moyenne) sous forme d'un ``pd.Series`` (toutes UEs confondues) - """ - self.tag = tag - """Le tag associé aux moyennes""" - - # Les moyennes par UE - self.matrice_notes: pd.DataFrame = matrice_notes - """Les notes aux UEs ou aux compétences (DataFrame)""" - - self.matrice_coeffs_moy_gen: pd.DataFrame = matrice_coeffs - """Les coeffs à appliquer pour le calcul des moyennes générales - (toutes UE ou compétences confondues). NaN si étudiant non inscrit""" - - self.moyennes: dict[int, pd.DataFrame] = {} - """Les dataframes retraçant les moyennes/classements/statistiques des étudiants aux UEs""" - colonnes = self.matrice_notes.columns - for col in colonnes: # if ue.type != UE_SPORT: - notes = matrice_notes[col] - self.moyennes[col] = Moyenne(notes) - - # Les moyennes générales - notes_gen = pd.Series(np.nan, index=self.matrice_notes.index) - """Les notes générales (moyenne toutes UEs confonudes)""" - if self.has_notes(): - notes_gen = self.compute_moy_gen( - self.matrice_notes, self.matrice_coeffs_moy_gen - ) - self.notes_gen = notes_gen - self.moyenne_gen = Moyenne(notes_gen) - """Le dataframe retraçant les moyennes/classements/statistiques général""" - - def has_notes(self): - """Détermine si les moyennes (aux UEs ou aux compétences) - ont des notes - - Returns: - True si la moytag a des notes, False sinon - """ - notes = self.matrice_notes - nbre_nan = notes.isna().sum().sum() - nbre_notes_potentielles = len(notes.index) * len(notes.columns) - if nbre_nan == nbre_notes_potentielles: - return False - else: - return True - - def compute_moy_gen(self, moys: pd.DataFrame, coeffs: pd.DataFrame) -> pd.Series: - """Calcule la moyenne générale (toutes UE/compétences confondus) - pour le tag considéré, en pondérant les notes obtenues au UE - par les coeff (généralement les crédits ECTS). - - Args: - moys: Les moyennes etudids x acronymes_ues/compétences - coeff: Les coeff etudids x ueids/compétences - """ - - # Calcule la moyenne générale dans le semestre (pondérée par le ECTS) - try: - moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects( - moys, - coeffs.fillna(0.0), - # formation_id=self.formsemestre.formation_id, - skip_empty_ues=True, - ) - except TypeError as e: - raise TypeError( - "Pb dans le calcul de la moyenne toutes UEs/compétences confondues" - ) - - return moy_gen_tag diff --git a/app/pe/moys/pe_moytag.py b/app/pe/moys/pe_moytag.py new file mode 100644 index 00000000..2870be55 --- /dev/null +++ b/app/pe/moys/pe_moytag.py @@ -0,0 +1,155 @@ +import numpy as np +import pandas as pd + +from app import comp +from app.comp.moy_sem import comp_ranks_series +from app.pe.moys import pe_moy + + +CODE_MOY_UE = "UEs" +CODE_MOY_COMPETENCES = "Compétences" +CHAMP_GENERAL = "Général" # Nom du champ de la moyenne générale + + +class MoyennesTag: + def __init__( + self, + tag: str, + type_moyenne: str, + matrice_notes: pd.DataFrame, # etudids x colonnes + matrice_coeffs: pd.DataFrame, # etudids x colonnes + ): + """Classe centralisant la synthèse des moyennes/classements d'une série + d'étudiants à un tag donné, en différenciant les notes + obtenues aux UE et au général (toutes UEs confondues) + + + Args: + tag: Un tag + matrice_notes: Les moyennes (etudid x acronymes_ues ou etudid x compétences) aux différentes UEs ou compétences + matrice_coeffs: Les coeff à appliquer pour le calcul de la moyenne générale + # notes_gen: Une série de notes (moyenne) sous forme d'un ``pd.Series`` (toutes UEs confondues) + """ + self.tag = tag + """Le tag associé aux moyennes""" + + self.type = type_moyenne + """Le type de moyennes (par UEs ou par compétences)""" + + # Les moyennes par UE + self.matrice_notes: pd.DataFrame = matrice_notes + """Les notes aux UEs ou aux compétences (DataFrame)""" + + self.matrice_coeffs_moy_gen: pd.DataFrame = matrice_coeffs + """Les coeffs à appliquer pour le calcul des moyennes générales + (toutes UE ou compétences confondues). NaN si étudiant non inscrit""" + + self.moyennes: dict[int, pd.DataFrame] = {} + """Les dataframes retraçant les moyennes/classements/statistiques des étudiants aux UEs""" + + self.etudids = self.matrice_notes.index + """Les étudids renseignés dans les moyennes""" + + self.champs = self.matrice_notes.columns + """Les champs (acronymes d'UE ou compétences) renseignés dans les moyennes""" + for col in self.champs: # if ue.type != UE_SPORT: + notes = matrice_notes[col] + self.moyennes[col] = pe_moy.Moyenne(notes) + + # Les moyennes générales + notes_gen = pd.Series(np.nan, index=self.matrice_notes.index) + """Les notes générales (moyenne toutes UEs confonudes)""" + if self.has_notes(): + notes_gen = self.compute_moy_gen( + self.matrice_notes, self.matrice_coeffs_moy_gen + ) + self.notes_gen = notes_gen + self.moyenne_gen = pe_moy.Moyenne(notes_gen) + """Le dataframe retraçant les moyennes/classements/statistiques général""" + + def has_notes(self): + """Détermine si les moyennes (aux UEs ou aux compétences) + ont des notes + + Returns: + True si la moytag a des notes, False sinon + """ + notes = self.matrice_notes + nbre_nan = notes.isna().sum().sum() + nbre_notes_potentielles = len(notes.index) * len(notes.columns) + if nbre_nan == nbre_notes_potentielles: + return False + else: + return True + + def compute_moy_gen(self, moys: pd.DataFrame, coeffs: pd.DataFrame) -> pd.Series: + """Calcule la moyenne générale (toutes UE/compétences confondus) + pour le tag considéré, en pondérant les notes obtenues au UE + par les coeff (généralement les crédits ECTS). + + Args: + moys: Les moyennes etudids x acronymes_ues/compétences + coeff: Les coeff etudids x ueids/compétences + """ + + # Calcule la moyenne générale dans le semestre (pondérée par le ECTS) + try: + moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects( + moys, + coeffs.fillna(0.0), + # formation_id=self.formsemestre.formation_id, + skip_empty_ues=True, + ) + except TypeError as e: + raise TypeError( + "Pb dans le calcul de la moyenne toutes UEs/compétences confondues" + ) + + return moy_gen_tag + + def to_df(self, aggregat=None, cohorte=None) -> pd.DataFrame: + """Renvoie le df synthétisant l'ensemble des données + connues + Adapte les intitulés des colonnes aux données fournies + (nom d'aggrégat, type de cohorte). + """ + + etudids_sorted = sorted(self.etudids) + + df = pd.DataFrame(index=etudids_sorted) + + # Ajout des notes pour tous les champs + champs = list(self.champs) + for champ in champs: + df_champ = self.moyennes[champ].get_df_synthese() # le dataframe + # Renomme les colonnes + cols = [ + get_colonne_df(aggregat, self.tag, champ, cohorte, critere) + for critere in pe_moy.Moyenne.COLONNES_SYNTHESE + ] + df_champ.columns = cols + df = df.join(df_champ) + + # Ajoute la moy générale + df_moy_gen = self.moyenne_gen.get_df_synthese() + cols = [ + get_colonne_df(aggregat, self.tag, CHAMP_GENERAL, cohorte, critere) + for critere in pe_moy.Moyenne.COLONNES_SYNTHESE + ] + df_moy_gen.columns = cols + df = df.join(df_moy_gen) + + return df + + +def get_colonne_df(aggregat, tag, champ, cohorte, critere): + """Renvoie le tuple (aggregat, tag, champ, cohorte, critere) + utilisé pour désigner les colonnes du df""" + liste_champs = [] + if aggregat: + liste_champs += [aggregat] + liste_champs += [tag, champ] + if cohorte: + liste_champs += [cohorte] + liste_champs += [critere] + return tuple(liste_champs) diff --git a/app/pe/pe_rcstag.py b/app/pe/moys/pe_rcstag.py similarity index 97% rename from app/pe/pe_rcstag.py rename to app/pe/moys/pe_rcstag.py index f8f6b9dc..664ff988 100644 --- a/app/pe/pe_rcstag.py +++ b/app/pe/moys/pe_rcstag.py @@ -36,19 +36,17 @@ Created on Fri Sep 9 09:15:05 2016 @author: barasc """ -from app.comp.res_sem import load_formsemestre_results from app.models import FormSemestre from app.pe import pe_affichage import pandas as pd import numpy as np from app.pe.rcss import pe_rcs, pe_rcsemx -import app.pe.pe_sxtag as pe_sxtag +import app.pe.moys.pe_sxtag as pe_sxtag import app.pe.pe_comp as pe_comp -from app.pe.pe_tabletags import TableTag -from app.pe.pe_moytag import MoyennesTag +from app.pe.moys import pe_tabletags, pe_moytag -class RCSTag(TableTag): +class RCSTag(pe_tabletags.TableTag): def __init__( self, rcsemx: pe_rcsemx.RCSemX, sxstags: dict[(str, int) : pe_sxtag.SxTag] ): @@ -62,7 +60,7 @@ class RCSTag(TableTag): rcsemx: Le RCSemX (identifié par un nom et l'id de son semestre terminal) sxstags: Les données sur les SemX taggués """ - TableTag.__init__(self) + pe_tabletags.TableTag.__init__(self) self.rcs_id: tuple(str, int) = rcsemx.rcs_id """Identifiant du RCSTag (identique au RCSemX sur lequel il s'appuie)""" @@ -121,10 +119,9 @@ class RCSTag(TableTag): pe_affichage.pe_print(f"* Tags : {', '.join(self.tags_sorted)}") # Les moyennes - self.moyennes_tags: dict[str, MoyennesTag] = {} + self.moyennes_tags: dict[str, pe_moytag.MoyennesTag] = {} """Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)""" for tag in self.tags_sorted: - print(tag) # Cube de notes (etudids_sorted x compétences_sorted x sxstags) notes_df, notes_cube = self.compute_notes_comps_cube( tag, self.etudids_sorted, self.competences_sorted, self.sxstags @@ -147,8 +144,11 @@ class RCSTag(TableTag): coeffs_cube, notes_cube, self.etudids_sorted, self.competences_sorted ) # Mémorise les moyennes et les coeff associés - self.moyennes_tags[tag] = MoyennesTag( - tag, moys_competences, matrice_coeffs_moy_gen + self.moyennes_tags[tag] = pe_moytag.MoyennesTag( + tag, + pe_moytag.CODE_MOY_COMPETENCES, + moys_competences, + matrice_coeffs_moy_gen, ) def __eq__(self, other): diff --git a/app/pe/pe_ressemtag.py b/app/pe/moys/pe_ressemtag.py similarity index 95% rename from app/pe/pe_ressemtag.py rename to app/pe/moys/pe_ressemtag.py index 09b88ab7..0732cb8e 100644 --- a/app/pe/pe_ressemtag.py +++ b/app/pe/moys/pe_ressemtag.py @@ -37,16 +37,13 @@ Created on Fri Sep 9 09:15:05 2016 """ import pandas as pd -from app import db, ScoValueError +from app import ScoValueError from app import comp from app.comp.res_but import ResultatsSemestreBUT -from app.comp.res_sem import load_formsemestre_results from app.models import FormSemestre, UniteEns -from app.models.moduleimpls import ModuleImpl import app.pe.pe_affichage as pe_affichage import app.pe.pe_etudiant as pe_etudiant -import app.pe.pe_tabletags as pe_tabletags -from app.pe.pe_moytag import MoyennesTag +from app.pe.moys import pe_tabletags, pe_moytag from app.scodoc import sco_tag_module from app.scodoc import codes_cursus as sco_codes @@ -126,17 +123,20 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): # pe_affichage.pe_print(f" -> Traitement du tag {tag}") infos_tag = tags_dict["personnalises"][tag] moy_ues_tag = self.compute_moy_ues_tag(infos_tag) - self.moyennes_tags[tag] = MoyennesTag( - tag, moy_ues_tag, self.matrice_coeffs_moy_gen + self.moyennes_tags[tag] = pe_moytag.MoyennesTag( + tag, pe_moytag.CODE_MOY_UE, moy_ues_tag, self.matrice_coeffs_moy_gen ) # Ajoute les moyennes par UEs + la moyenne générale (but) moy_gen = self.compute_moy_gen() - self.moyennes_tags["but"] = MoyennesTag( - "but", moy_gen, self.matrice_coeffs_moy_gen # , moy_gen_but + self.moyennes_tags["but"] = pe_moytag.MoyennesTag( + "but", + pe_moytag.CODE_MOY_UE, + moy_gen, + self.matrice_coeffs_moy_gen, # , moy_gen_but ) - self.tags_sorted = self.get_all_tags() + self.tags_sorted = self.get_all_significant_tags() """Tags (personnalisés+compétences) par ordre alphabétique""" def get_repr(self, verbose=False) -> str: @@ -302,11 +302,12 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): dict_tags["auto"] = {"but": {}} noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp - + aff_tags_auto = ", ".join([f"👜{nom}" for nom in noms_tags_auto]) + aff_tags_perso = ", ".join([f"👜{nom}" for nom in noms_tags_perso]) # Affichage pe_affichage.pe_print( - f"* Tags du programme de formation : {', '.join(noms_tags_perso)} " - + f"Tags automatiques : {', '.join(noms_tags_auto)}" + f"* Tags du programme de formation : {aff_tags_perso} + " + + f"Tags automatiques : {aff_tags_auto}" ) return dict_tags diff --git a/app/pe/pe_sxtag.py b/app/pe/moys/pe_sxtag.py similarity index 97% rename from app/pe/pe_sxtag.py rename to app/pe/moys/pe_sxtag.py index b7600bca..abdd0bde 100644 --- a/app/pe/pe_sxtag.py +++ b/app/pe/moys/pe_sxtag.py @@ -37,16 +37,15 @@ Created on Fri Sep 9 09:15:05 2016 """ from app.pe import pe_affichage, pe_comp -import app.pe.pe_ressemtag as pe_ressemtag +import app.pe.moys.pe_ressemtag as pe_ressemtag import pandas as pd import numpy as np -from app.pe.pe_tabletags import TableTag -from app.pe.pe_moytag import MoyennesTag +from app.pe.moys import pe_moytag, pe_tabletags import app.pe.rcss.pe_trajectoires as pe_trajectoires -class SxTag(TableTag): +class SxTag(pe_tabletags.TableTag): def __init__( self, sxtag_id: (str, int), @@ -80,7 +79,7 @@ class SxTag(TableTag): les semestres à regrouper et les résultats/moyennes par tag des semestres """ - TableTag.__init__(self) + pe_tabletags.TableTag.__init__(self) assert sxtag_id and len(sxtag_id) == 2 and sxtag_id[1] in ressembuttags @@ -186,8 +185,11 @@ class SxTag(TableTag): self.__aff_profil_coeff_ects(tag) # Mémorise les infos pour la moyennes au tag - self.moyennes_tags[tag] = MoyennesTag( - tag, matrice_moys_ues, self.matrice_coeffs_moy_gen + self.moyennes_tags[tag] = pe_moytag.MoyennesTag( + tag, + pe_moytag.CODE_MOY_UE, + matrice_moys_ues, + self.matrice_coeffs_moy_gen, ) def __aff_profil_coeff_ects(self, tag): diff --git a/app/pe/moys/pe_tabletags.py b/app/pe/moys/pe_tabletags.py new file mode 100644 index 00000000..67564736 --- /dev/null +++ b/app/pe/moys/pe_tabletags.py @@ -0,0 +1,204 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +############################################################################## +# Module "Avis de poursuite d'étude" +# conçu et développé par Cléo Baras (IUT de Grenoble) +############################################################################## + + +""" +Created on Thu Sep 8 09:36:33 2016 + +@author: barasc +""" + +import pandas as pd + +from app.models import Identite +from app.pe.moys import pe_moytag + +TAGS_RESERVES = ["but"] + +CHAMPS_ADMINISTRATIFS = ["Civilité", "Nom", "Prénom"] + + +class TableTag(object): + def __init__(self): + """Classe centralisant différentes méthodes communes aux + SemestreTag, TrajectoireTag, AggregatInterclassTag + """ + # Les étudiants + # self.etuds: list[Identite] = None # A venir + """Les étudiants""" + # self.etudids: list[int] = {} + """Les etudids""" + + def add_etuds(self, etuds: list[Identite]): + """Mémorise les informations sur les étudiants + + Args: + etuds: la liste des identités de l'étudiant + """ + # self.etuds = etuds + self.etudids = list({etud.etudid for etud in etuds}) + + def get_all_significant_tags(self): + """Liste des tags de la table, triée par ordre alphabétique, + extraite des clés du dictionnaire ``moyennes_tags``, en ne + considérant que les moyennes ayant des notes. + + Returns: + Liste de tags triés par ordre alphabétique + """ + tags = [] + tag: str = "" + moytag: pe_moytag.MoyennesTag = None + for tag, moytag in self.moyennes_tags.items(): + if moytag.has_notes(): + tags.append(tag) + return sorted(tags) + + def to_df( + self, + administratif=True, + aggregat=None, + tags_cibles=None, + cohorte=None, + type_colonnes=True, + ) -> pd.DataFrame: + """Renvoie un dataframe listant toutes les données + des moyennes/classements/nb_inscrits/min/max/moy + des étudiants aux différents tags. + + tags_cibles limitent le dataframe aux tags indiqués + type_colonnes indiquent si les colonnes doivent être passées en multiindex + + Args: + administratif: Indique si les données administratives sont incluses + aggregat: l'aggrégat représenté + tags_cibles: la liste des tags ciblés + cohorte: la cohorte représentée + Returns: + Le dataframe complet de synthèse + """ + if not self.is_significatif(): + return None + + # Les tags visés + tags_tries = self.get_all_significant_tags() + if not tags_cibles: + tags_cibles = tags_tries + tags_cibles = sorted(tags_cibles) + + # Les tags visés avec des notes + + # Les étudiants visés + if administratif: + df = df_administratif(self.etuds, aggregat, cohorte) + else: + df = pd.DataFrame(index=self.etudids) + + # Ajout des données par tags + for tag in tags_cibles: + moy_tag_df = self.moyennes_tags[tag].to_df(aggregat, cohorte) + df = df.join(moy_tag_df) + + # Tri par nom, prénom + if administratif: + colonnes_tries = [ + _get_champ_administratif(champ, aggregat, cohorte) + for champ in CHAMPS_ADMINISTRATIFS[1:] + ] # Nom + Prénom + df = df.sort_values(by=colonnes_tries) + + # Conversion des colonnes en multiindex + if type_colonnes: + df.columns = pd.MultiIndex.from_tuples(df.columns) + + return df + + def has_etuds(self): + """Indique si un tabletag contient des étudiants""" + return len(self.etuds) > 0 + + def is_significatif(self): + """Indique si une tabletag a des données""" + # A des étudiants + if not self.etuds: + return False + # A des tags avec des notes + tags_tries = self.get_all_significant_tags() + if not tags_tries: + return False + return True + + +def _get_champ_administratif(champ, aggregat=None, cohorte=None): + """Pour un champ donné, renvoie l'index (ou le multindex) + à intégrer au dataframe""" + liste = [] + if aggregat: + liste += [aggregat] + liste += ["Administratif", "Identité"] + if cohorte: + liste += [champ] + liste += [champ] + return tuple(liste) + + +def df_administratif( + etuds: list[Identite], aggregat=None, cohorte=None +) -> pd.DataFrame: + """Renvoie un dataframe donnant les données administratives + des étudiants du TableTag + + Args: + etuds: Identité des étudiants générant les données administratives + """ + identites = {etud.etudid: etud for etud in etuds} + + donnees = {} + etud: Identite = None + for etudid, etud in identites.items(): + data = { + CHAMPS_ADMINISTRATIFS[0]: etud.civilite_str, + CHAMPS_ADMINISTRATIFS[1]: etud.nom, + CHAMPS_ADMINISTRATIFS[2]: etud.prenom_str, + } + donnees[etudid] = { + _get_champ_administratif(champ, aggregat, cohorte): data[champ] + for champ in CHAMPS_ADMINISTRATIFS + } + + colonnes = [ + _get_champ_administratif(champ, aggregat, cohorte) + for champ in CHAMPS_ADMINISTRATIFS + ] + df = pd.DataFrame.from_dict(donnees, orient="index", columns=colonnes) + df = df.sort_values(by=colonnes[1:]) + return df diff --git a/app/pe/pe_interclasstag.py b/app/pe/pe_interclasstag.py deleted file mode 100644 index 3de44d23..00000000 --- a/app/pe/pe_interclasstag.py +++ /dev/null @@ -1,160 +0,0 @@ -############################################################################## -# -# Gestion scolarite IUT -# -# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Emmanuel Viennet emmanuel.viennet@viennet.net -# -############################################################################## - -############################################################################## -# Module "Avis de poursuite d'étude" -# conçu et développé par Cléo Baras (IUT de Grenoble) -############################################################################## - -""" -Created on Thu Sep 8 09:36:33 2016 - -@author: barasc -""" - -import pandas as pd -import numpy as np - -from app.pe.pe_tabletags import TableTag -from app.pe.pe_moytag import MoyennesTag -import app.pe.pe_etudiant as pe_etudiant -import app.pe.rcss.pe_rcs as pe_rcs -import app.pe.pe_rcss_jury as pe_rcss_jury -import app.pe.pe_rcstag as pe_rcstag - - -class RCSInterclasseTag(TableTag): - """ - Interclasse l'ensemble des étudiants diplômés à une année - donnée (celle du jury), pour un RCS donné (par ex: 'S2', '3S') - en reportant : - - * les moyennes obtenues sur la trajectoire qu'il ont suivi pour atteindre - le numéro de semestre de fin de l'aggrégat (indépendamment de son - formsemestre) - * calculant le classement sur les étudiants diplômes - """ - - def __init__( - self, - nom_rcs: str, - etudiants: pe_etudiant.EtudiantsJuryPE, - rcss_jury_pe: pe_rcss_jury.RCSsJuryPE, - rcss_tags: dict[tuple, pe_rcstag.RCSTag], - ): - TableTag.__init__(self) - - self.nom_rcs = nom_rcs - """Le nom du RCS interclassé""" - - self.nom = self.get_repr() - """Représentation textuelle de l'interclassement""" - - self.diplomes_ids = etudiants.etudiants_diplomes - """Identité des étudiants diplômés""" - - self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids} - """Etudids des étudiants diplômés""" - - # Les RCSemX et leur versions taggués SxTag, en ne gardant que - # celles associées à l'aggrégat - self.rcss: dict[int, pe_rcs.RCS] = {} - """Ensemble des trajectoires associées à l'aggrégat""" - for trajectoire_id in rcss_jury_pe.trajectoires: - trajectoire = rcss_jury_pe.trajectoires[trajectoire_id] - if trajectoire_id[0] == nom_rcs: - self.rcss[trajectoire_id] = trajectoire - - self.trajectoires_taggues: dict[int, pe_rcs.RCS] = {} - """Ensemble des trajectoires tagguées associées à l'aggrégat""" - for trajectoire_id in self.rcss: - self.trajectoires_taggues[trajectoire_id] = rcss_tags[trajectoire_id] - - # Les trajectoires suivies par les étudiants du jury, en ne gardant que - # celles associées aux diplomés - self.suivi: dict[int, pe_rcs.RCS] = {} - """Association entre chaque étudiant et la trajectoire tagguée à prendre en - compte pour l'aggrégat""" - for etudid in self.diplomes_ids: - self.suivi[etudid] = rcss_jury_pe.trajectoires_suivies[etudid][nom_rcs] - - self.tags_sorted = self.do_taglist() - """Liste des tags (triés par ordre alphabétique)""" - - # Construit la matrice de notes - self.notes = self.compute_notes_matrice() - """Matrice des notes de l'aggrégat""" - - # Synthétise les moyennes/classements par tag - self.moyennes_tags: dict[str, MoyennesTag] = {} - for tag in self.tags_sorted: - moy_gen_tag = self.notes[tag] - self.moyennes_tags[tag] = MoyennesTag(tag, moy_gen_tag) - - # Est significatif ? (aka a-t-il des tags et des notes) - self.significatif = len(self.tags_sorted) > 0 - - def get_repr(self) -> str: - """Une représentation textuelle""" - return f"Aggrégat {self.nom_rcs}" - - def do_taglist(self): - """Synthétise les tags à partir des trajectoires_tagguées - - Returns: - Une liste de tags triés par ordre alphabétique - """ - tags = [] - for trajectoire in self.trajectoires_taggues.values(): - tags.extend(trajectoire.tags_sorted) - return sorted(set(tags)) - - def compute_notes_matrice(self): - """Construit la matrice de notes (etudid x tags) - retraçant les moyennes obtenues par les étudiants dans les semestres associés à - l'aggrégat (une trajectoire ayant pour numéro de semestre final, celui de l'aggrégat). - """ - # nb_tags = len(self.tags_sorted) unused ? - # nb_etudiants = len(self.diplomes_ids) - - # Index de la matrice (etudids -> dim 0, tags -> dim 1) - etudids = list(self.diplomes_ids) - tags = self.tags_sorted - - # Partant d'un dataframe vierge - df = pd.DataFrame(np.nan, index=etudids, columns=tags) - - for trajectoire in self.trajectoires_taggues.values(): - # Charge les moyennes par tag de la trajectoire tagguée - notes = trajectoire.notes - # Etudiants/Tags communs entre la trajectoire_tagguée et les données interclassées - etudids_communs = df.index.intersection(notes.index) - tags_communs = df.columns.intersection(notes.columns) - - # Injecte les notes par tag - df.loc[etudids_communs, tags_communs] = notes.loc[ - etudids_communs, tags_communs - ] - - return df diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 92fb6f95..aa87cc8e 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -51,13 +51,18 @@ import numpy as np import pandas as pd from app.pe.rcss import pe_rcs -from app.pe import pe_sxtag +from app.pe.moys import pe_sxtag from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE import app.pe.pe_affichage as pe_affichage import app.pe.pe_etudiant as pe_etudiant -from app.pe.pe_rcstag import RCSTag -from app.pe.pe_ressemtag import ResSemBUTTag -from app.pe.pe_interclasstag import RCSInterclasseTag +from app.pe.moys import ( + pe_tabletags, + pe_ressemtag, + pe_sxtag, + pe_rcstag, + pe_interclasstag, + pe_moytag, +) import app.pe.pe_rcss_jury as pe_rcss_jury @@ -85,8 +90,7 @@ class JuryPE(object): ) # Chargement des étudiants à prendre en compte dans le jury pe_affichage.pe_print( - f"""*** Recherche et chargement des étudiants diplômés en { - self.diplome}""" + f"""*** Recherche des étudiants diplômés 🎓 en {self.diplome}""" ) self.etudiants = pe_etudiant.EtudiantsJuryPE( self.diplome @@ -110,8 +114,8 @@ class JuryPE(object): self._gen_xls_sxtags(zipfile) self._gen_rcsemxs() self._gen_xls_rcstags(zipfile) - # self._gen_xls_interclassements_rcss(zipfile) - # self._gen_xls_synthese_jury_par_tag(zipfile) + self._gen_xls_interclasstags(zipfile) + self._gen_xls_synthese_jury_par_tag(zipfile) # self._gen_xls_synthese_par_etudiant(zipfile) except Exception as e: raise e @@ -159,7 +163,7 @@ class JuryPE(object): self.ressembuttags = {} for frmsem_id, formsemestre in formsemestres.items(): # Crée le semestre_tag et exécute les calculs de moyennes - self.ressembuttags[frmsem_id] = ResSemBUTTag(formsemestre) + self.ressembuttags[frmsem_id] = pe_ressemtag.ResSemBUTTag(formsemestre) # Intègre le bilan des semestres taggués au zip final output = io.BytesIO() @@ -170,7 +174,7 @@ class JuryPE(object): for res_sem_tag in self.ressembuttags.values(): onglet = res_sem_tag.get_repr(verbose=True) onglets += [] - df = res_sem_tag.df_moyennes_et_classements() + df = res_sem_tag.to_df() # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") @@ -219,7 +223,6 @@ class JuryPE(object): for rcf_id, rcf in self.rcss_jury.semXs.items(): # SxTag traduisant le RCF sxtag_id = rcf_id - self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, rcf, self.ressembuttags) # Intègre le bilan des semestres taggués au zip final @@ -230,20 +233,21 @@ class JuryPE(object): onglets = [] for sxtag in self.sxtags.values(): onglet = sxtag.get_repr(verbose=False) - onglets += [onglet] - df = sxtag.df_moyennes_et_classements() - # écriture dans l'onglet - df.to_excel(writer, onglet, index=True, header=True) + if sxtag.is_significatif(): + df = sxtag.to_df() + onglets += [onglet] + # écriture dans l'onglet + df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) - - self.add_file_to_zip( - zipfile, - f"semestres_taggues_{self.diplome}.xlsx", - output.read(), - path="details", - ) + if onglets: + self.add_file_to_zip( + zipfile, + f"semestres_taggues_{self.diplome}.xlsx", + output.read(), + path="details", + ) def _gen_rcsemxs(self): """Génère les regroupements cohérents de RCFs qu'ont suivi chaque étudiant""" @@ -279,7 +283,7 @@ class JuryPE(object): self.rcss_tags = {} for rcs_id, rcsemx in self.rcss_jury.rcsemxs.items(): - self.rcss_tags[rcs_id] = RCSTag(rcsemx, self.sxtags) + self.rcss_tags[rcs_id] = pe_rcstag.RCSTag(rcsemx, self.sxtags) # Intègre le bilan des trajectoires tagguées au zip final output = io.BytesIO() @@ -289,27 +293,67 @@ class JuryPE(object): onglets = [] for rcs_tag in self.rcss_tags.values(): onglet = rcs_tag.get_repr(verbose=False) - onglets += [onglet] - df = rcs_tag.df_moyennes_et_classements() - # écriture dans l'onglet - df.to_excel(writer, onglet, index=True, header=True) + if rcs_tag.is_significatif(): + df = rcs_tag.to_df() + onglets += [onglet] + # écriture dans l'onglet + df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) - self.add_file_to_zip( - zipfile, - f"RCRCFs_{self.diplome}.xlsx", - output.read(), - path="details", - ) + if onglets: + self.add_file_to_zip( + zipfile, + f"RCRCFs_{self.diplome}.xlsx", + output.read(), + path="details", + ) - def _gen_xls_interclassements_rcss(self, zipfile: ZipFile): - """Intègre le bilan des RCS (interclassé par promo) au zip""" - # Génère les interclassements (par promo et) par (nom d') aggrégat - pe_affichage.pe_print("*** Génère les interclassements") - self.interclassements_taggues = compute_interclassements( - self.etudiants, self.rcss_jury, self.rcss_tags + def _gen_xls_interclasstags(self, zipfile: ZipFile): + """Génère les interclassements sur la promo de diplômés + par (nom d') aggrégat + en distinguant les interclassements par accronymes d'UEs (sur les SxTag) + et ceux par compétences (sur les RCSTag). + """ + pe_affichage.pe_print( + "*** Génère les interclassements sur chaque type de RCS/agrgégat" ) + self.interclasstags = { + pe_moytag.CODE_MOY_UE: {}, + pe_moytag.CODE_MOY_COMPETENCES: {}, + } + + etudiants_diplomes = self.etudiants.etudiants_diplomes + + # Les interclassements par UE + for Sx in pe_rcs.TOUS_LES_SEMESTRES: + pe_affichage.pe_print( + f"--> Interclassement par (acronyme d')UEs pour le RCS {Sx}" + ) + interclass = pe_interclasstag.InterClassTag( + Sx, + pe_moytag.CODE_MOY_UE, + etudiants_diplomes, + self.rcss_jury.semXs, + self.sxtags, + self.rcss_jury.semXs_suivis, + ) + self.interclasstags[pe_moytag.CODE_MOY_UE][Sx] = interclass + + # Les interclassements par compétences + for nom_rcs in pe_rcs.TOUS_LES_RCS: + pe_affichage.pe_print( + f"--> Interclassement par compétences pour le RCS {nom_rcs}" + ) + interclass = pe_interclasstag.InterClassTag( + nom_rcs, + pe_moytag.CODE_MOY_COMPETENCES, + etudiants_diplomes, + self.rcss_jury.rcsemxs, + self.rcss_tags, + self.rcss_jury.rcsemxs_suivis, + ) + self.interclasstags[pe_moytag.CODE_MOY_COMPETENCES][nom_rcs] = interclass # Intègre le bilan des aggrégats (interclassé par promo) au zip final output = io.BytesIO() @@ -317,23 +361,29 @@ class JuryPE(object): output, engine="openpyxl" ) as writer: onglets = [] - for interclass_tag in self.interclassements_taggues.values(): - if interclass_tag.significatif: # Avec des notes - onglet = interclass_tag.get_repr() - onglets += [onglet] - df = interclass_tag.df_moyennes_et_classements() - # écriture dans l'onglet - df.to_excel(writer, onglet, index=True, header=True) + for type_interclass in [ + pe_moytag.CODE_MOY_UE, + pe_moytag.CODE_MOY_COMPETENCES, + ]: + interclasstag = self.interclasstags[type_interclass] + for nom_rcs, interclass in interclasstag.items(): + onglet = interclass.get_repr() + if interclass.is_significatif(): + df = interclass.to_df(cohorte="Promo") + onglets += [onglet] + # écriture dans l'onglet + df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) - self.add_file_to_zip( - zipfile, - f"interclassements_taggues_{self.diplome}.xlsx", - output.read(), - path="details", - ) + if onglets: + self.add_file_to_zip( + zipfile, + f"InterClassTags_{self.diplome}.xlsx", + output.read(), + path="details", + ) def _gen_xls_synthese_jury_par_tag(self, zipfile: ZipFile): """Synthèse des éléments du jury PE tag par tag""" @@ -347,7 +397,8 @@ class JuryPE(object): output, engine="openpyxl" ) as writer: onglets = [] - for onglet, df in self.synthese.items(): + for (tag, type_moy), df in self.synthese.items(): + onglet = f"{tag} {type_moy}" onglets += [onglet] # écriture dans l'onglet: df.to_excel(writer, onglet, index=True, header=True) @@ -407,15 +458,18 @@ class JuryPE(object): self.zipdata.seek(0) return self.zipdata - def do_tags_list(self, interclassements: dict[str, RCSInterclasseTag]): + def _do_tags_list(self, interclassements: dict[str, dict]): """La liste des tags extraites des interclassements""" tags = [] - for aggregat in interclassements: - interclass = interclassements[aggregat] - if interclass.tags_sorted: - tags.extend(interclass.tags_sorted) - tags = sorted(set(tags)) - return tags + # Pour chaque type d'interclassements + for type in interclassements: + interclassement = interclassements[type] + for aggregat in interclassement: + interclass = interclassement[aggregat] + if interclass.tags_sorted: + tags.extend(interclass.tags_sorted) + tags = sorted(set(tags)) + return tags # **************************************************************************************************************** # # Méthodes pour la synthèse du juryPE @@ -423,154 +477,79 @@ class JuryPE(object): def synthetise_jury_par_tags(self) -> dict[pd.DataFrame]: """Synthétise tous les résultats du jury PE dans des dataframes, - dont les onglets sont les tags""" + dont les onglets sont les tags et des types de calcul de moyennes + (par UEs ou par compétences)""" - pe_affichage.pe_print("*** Synthèse finale des moyennes par tag***") + pe_affichage.pe_print( + "*** Synthèse finale des moyennes par tag et par type de moyennes (UEs ou Compétences)***" + ) synthese = {} pe_affichage.pe_print(" -> Synthèse des données administratives") synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids) - tags = self.do_tags_list(self.interclassements_taggues) + tags = self._do_tags_list(self.interclasstags) for tag in tags: - pe_affichage.pe_print(f" -> Synthèse du tag {tag}") - synthese[tag] = self.df_tag(tag) + for type_moy in [pe_moytag.CODE_MOY_UE, pe_moytag.CODE_MOY_COMPETENCES]: + pe_affichage.pe_print(f" -> Synthèse du tag {tag} par {type_moy}") + synthese[(tag, type_moy)] = self.df_tag_type(tag, type_moy) return synthese - def df_tag(self, tag): - """Génère le DataFrame synthétisant les moyennes/classements (groupe, - interclassement promo) pour tous les aggrégats prévus, + def df_tag_type(self, tag, type_moy, type_colonnes=True): + """Génère le DataFrame synthétisant les moyennes/classements (groupe + + interclassement promo) pour tous les aggrégats prévus, en fonction + du type (UEs ou Compétences) de données souhaitées, tels que fourni dans l'excel final. + Si type=UEs => tous les sxtag du tag + Si type=Compétences => tous les rcstag du tag + Args: tag: Un des tags (a minima `but`) + type_moy: Un type de moyenne Returns: """ - etudids = list(self.diplomes_ids) - # Les données des étudiants - donnees_etudiants = {} - for etudid in etudids: - etudiant = self.etudiants.identites[etudid] - donnees_etudiants[etudid] = { - ("Identité", "", "Civilite"): etudiant.civilite_str, - ("Identité", "", "Nom"): etudiant.nom, - ("Identité", "", "Prenom"): etudiant.prenom, - } - df_synthese = pd.DataFrame.from_dict(donnees_etudiants, orient="index") + etuds = [etud for etudid, etud in self.etudiants.etudiants_diplomes.items()] + df = pe_tabletags.df_administratif(etuds, aggregat="", cohorte="") - # Ajout des aggrégats - for aggregat in pe_rcs.TOUS_LES_RCS: - descr = app.pe.rcss.pe_rcs.TYPES_RCS[aggregat]["descr"] + if type_moy == pe_moytag.CODE_MOY_UE: + aggregats = pe_rcs.TOUS_LES_SEMESTRES + else: + aggregats = pe_rcs.TOUS_LES_RCS - # Les trajectoires (tagguées) suivies par les étudiants pour l'aggrégat et le tag - # considéré - trajectoires_tagguees = [] - for etudid in etudids: - trajectoire = self.rcss_jury.trajectoires_suivies[etudid][aggregat] - if trajectoire: - tid = trajectoire.sxtag_id - trajectoire_tagguee = self.rcss_tags[tid] - if ( - tag in trajectoire_tagguee.moyennes_tags - and trajectoire_tagguee not in trajectoires_tagguees - ): - trajectoires_tagguees.append(trajectoire_tagguee) + for aggregat in aggregats: + print(aggregat) + # Descr de l'aggrégat + descr = pe_rcs.TYPES_RCS[aggregat]["descr"] - # Combien de notes vont être injectées ? - nbre_notes_injectees = 0 - for traj in trajectoires_tagguees: - moy_traj = traj.moyennes_tags[tag] - inscrits_traj = moy_traj.inscrits_ids - etudids_communs = set(etudids) & set(inscrits_traj) - nbre_notes_injectees += len(etudids_communs) + # L'interclassement associé + interclass = self.interclasstags[type_moy][aggregat] - # Si l'aggrégat est significatif (aka il y a des notes) - if nbre_notes_injectees > 0: - # Ajout des données classements & statistiques - nom_stat_promo = f"{NOM_STAT_PROMO} {self.diplome}" - donnees = pd.DataFrame( - index=etudids, - columns=[ - [descr] * (1 + 4 * 2), - [""] + [NOM_STAT_GROUPE] * 4 + [nom_stat_promo] * 4, - ["note"] + ["class.", "min", "moy", "max"] * 2, - ], + if interclass.is_significatif(): + # Le dataframe du classement sur le groupe + df_groupe = interclass.compute_df_synthese_moyennes_tag( + tag, aggregat=aggregat, type_colonnes=False ) + df = df.join(df_groupe) - for traj in trajectoires_tagguees: - # Les données des trajectoires_tagguees - moy_traj = traj.moyennes_tags[tag] + # Le dataframe du classement sur la promo + df_promo = interclass.to_df( + administratif=False, + aggregat=aggregat, + tags_cibles=[tag], + cohorte="Promo", + type_colonnes=False, + ) + df = df.join(df_promo) - # Les étudiants communs entre tableur de synthèse et trajectoires - inscrits_traj = moy_traj.inscrits_ids - etudids_communs = list(set(etudids) & set(inscrits_traj)) - - # Les notes - champ = (descr, "", "note") - notes_traj = moy_traj.get_notes() - donnees.loc[etudids_communs, champ] = notes_traj.loc[ - etudids_communs - ] - - # Les rangs - champ = (descr, NOM_STAT_GROUPE, "class.") - rangs = moy_traj.get_rangs_inscrits() - donnees.loc[etudids_communs, champ] = rangs.loc[etudids_communs] - - # Les mins - champ = (descr, NOM_STAT_GROUPE, "min") - mins = moy_traj.get_min() - donnees.loc[etudids_communs, champ] = mins.loc[etudids_communs] - - # Les max - champ = (descr, NOM_STAT_GROUPE, "max") - maxs = moy_traj.get_max() - donnees.loc[etudids_communs, champ] = maxs.loc[etudids_communs] - - # Les moys - champ = (descr, NOM_STAT_GROUPE, "moy") - moys = moy_traj.get_moy() - donnees.loc[etudids_communs, champ] = moys.loc[etudids_communs] - - # Ajoute les données d'interclassement - interclass = self.interclassements_taggues[aggregat] - moy_interclass = interclass.moyennes_tags[tag] - - # Les étudiants communs entre tableur de synthèse et l'interclassement - inscrits_interclass = moy_interclass.inscrits_ids - etudids_communs = list(set(etudids) & set(inscrits_interclass)) - - # Les classements d'interclassement - champ = (descr, nom_stat_promo, "class.") - rangs = moy_interclass.get_rangs_inscrits() - donnees.loc[etudids_communs, champ] = rangs.loc[etudids_communs] - - # Les mins - champ = (descr, nom_stat_promo, "min") - mins = moy_interclass.get_min() - donnees.loc[etudids_communs, champ] = mins.loc[etudids_communs] - - # Les max - champ = (descr, nom_stat_promo, "max") - maxs = moy_interclass.get_max() - donnees.loc[etudids_communs, champ] = maxs.loc[etudids_communs] - - # Les moys - champ = (descr, nom_stat_promo, "moy") - moys = moy_interclass.get_moy() - donnees.loc[etudids_communs, champ] = moys.loc[etudids_communs] - - df_synthese = df_synthese.join(donnees) - # Fin de l'aggrégat - - # Tri par nom/prénom - df_synthese.sort_values( - by=[("Identité", "", "Nom"), ("Identité", "", "Prenom")], inplace=True - ) - return df_synthese + # Conversion des colonnes en multiindex + if type_colonnes: + df.columns = pd.MultiIndex.from_tuples(df.columns) + return df + # Fin de l'aggrégat def synthetise_jury_par_etudiants(self) -> dict[pd.DataFrame]: """Synthétise tous les résultats du jury PE dans des dataframes, @@ -600,7 +579,7 @@ class JuryPE(object): def df_synthese_etudiant(self, etudid: int) -> pd.DataFrame: """Créé un DataFrame pour un étudiant donné par son etudid, retraçant toutes ses moyennes aux différents tag et aggrégats""" - tags = self.do_tags_list(self.interclassements_taggues) + tags = self._do_tags_list(self.interclasstags) donnees = {} @@ -620,7 +599,7 @@ class JuryPE(object): trajectoire_tagguee = self.rcss_tags[trajectoire.sxtag_id] if tag in trajectoire_tagguee.moyennes_tags: # L'interclassement - interclass = self.interclassements_taggues[aggregat] + interclass = self.interclasstags[aggregat] # Injection des données dans un dictionnaire donnees[tag] |= get_dict_synthese_aggregat( @@ -662,25 +641,6 @@ def get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict: return semestres -def compute_interclassements( - etudiants: pe_etudiant.EtudiantsJuryPE, - trajectoires_jury_pe: pe_rcss_jury.RCSsJuryPE, - trajectoires_tagguees: dict[tuple, pe_rcs.RCS], -): - """Interclasse les étudiants, (nom d') aggrégat par aggrégat, - pour fournir un classement sur la promo. Le classement est établi au regard du nombre - d'étudiants ayant participé au même aggrégat. - """ - aggregats_interclasses_taggues = {} - for nom_aggregat in pe_rcs.TOUS_LES_RCS: - pe_affichage.pe_print(f" --> Interclassement {nom_aggregat}") - interclass = RCSInterclasseTag( - nom_aggregat, etudiants, trajectoires_jury_pe, trajectoires_tagguees - ) - aggregats_interclasses_taggues[nom_aggregat] = interclass - return aggregats_interclasses_taggues - - def get_defaut_dict_synthese_aggregat(nom_rcs: str, diplome: int) -> dict: """Renvoie le dictionnaire de synthèse (à intégrer dans un tableur excel) pour décrire les résultats d'un aggrégat @@ -723,8 +683,8 @@ def get_defaut_dict_synthese_aggregat(nom_rcs: str, diplome: int) -> dict: def get_dict_synthese_aggregat( aggregat: str, - trajectoire_tagguee: RCSTag, - interclassement_taggue: RCSInterclasseTag, + trajectoire_tagguee: pe_rcstag.RCSTag, + interclassement_taggue: pe_interclasstag.InterClassTag, etudid: int, tag: str, diplome: int, diff --git a/app/pe/pe_tabletags.py b/app/pe/pe_tabletags.py deleted file mode 100644 index a14f593b..00000000 --- a/app/pe/pe_tabletags.py +++ /dev/null @@ -1,122 +0,0 @@ -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -############################################################################## -# -# Gestion scolarite IUT -# -# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Emmanuel Viennet emmanuel.viennet@viennet.net -# -############################################################################## - -############################################################################## -# Module "Avis de poursuite d'étude" -# conçu et développé par Cléo Baras (IUT de Grenoble) -############################################################################## - - -""" -Created on Thu Sep 8 09:36:33 2016 - -@author: barasc -""" - -import pandas as pd - -from app.models import Identite - -TAGS_RESERVES = ["but"] - - -class TableTag(object): - def __init__(self): - """Classe centralisant différentes méthodes communes aux - SemestreTag, TrajectoireTag, AggregatInterclassTag - """ - # Les étudiants - # self.etuds: list[Identite] = None # A venir - """Les étudiants""" - # self.etudids: list[int] = {} - """Les etudids""" - - def add_etuds(self, etuds: list[Identite]): - """Mémorise les informations sur les étudiants - - Args: - etuds: la liste des identités de l'étudiant - """ - # self.etuds = etuds - self.etudids = list({etud.etudid for etud in etuds}) - - def get_all_tags(self): - """Liste des tags de la table, triée par ordre alphabétique, - extraite des clés du dictionnaire ``moyennes_tags`` connues (tags en doublon - possible). - - Returns: - Liste de tags triés par ordre alphabétique - """ - return sorted(list(self.moyennes_tags.keys())) - - def df_moyennes_et_classements(self) -> pd.DataFrame: - """Renvoie un dataframe listant toutes les moyennes, - et les classements des étudiants pour tous les tags. - - Est utilisé pour afficher le détail d'un tableau taggué - (semestres, trajectoires ou aggrégat) - - Returns: - Le dataframe des notes et des classements - """ - - etudiants = {etud.etudid: [etud.nom, etud.prenom] for etud in self.etuds} - df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom", "prenom"]) - - tags_tries = self.get_all_tags() - for tag in tags_tries: - moy_tag = self.moyennes_tags[tag] - for acronyme in moy_tag.moyennes: - moy = moy_tag.moyennes[acronyme] # une moyenne - df = df.join(moy.synthese["notes"].rename(f"Moy {tag}-{acronyme}")) - df = df.join( - moy.synthese["classements"].rename(f"Class {tag}-{acronyme}") - ) - moy_gen = moy_tag.moyenne_gen - df = df.join(moy_gen.synthese["notes"].rename(f"Moy {tag} (gen)")) - df = df.join(moy_gen.synthese["classements"].rename(f"Class {tag} (gen)")) - - df.sort_values(by=["nom", "prenom"]) - - return df - - def df_notes(self) -> pd.DataFrame | None: - """Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags - - Returns: - Un dataframe etudids x tag (avec tag par ordre alphabétique) - """ - tags_tries = self.get_all_tags() - if tags_tries: - dict_series = {} - for tag in tags_tries: - # Les moyennes associés au tag - moy_tag = self.moyennes_tags[tag] - dict_series[tag] = moy_tag.synthese["notes"] - df = pd.DataFrame(dict_series) - return df diff --git a/app/pe/rcss/pe_rcsemx.py b/app/pe/rcss/pe_rcsemx.py index 8c6bb0b0..94d3db5d 100644 --- a/app/pe/rcss/pe_rcsemx.py +++ b/app/pe/rcss/pe_rcsemx.py @@ -10,7 +10,7 @@ Created on 01-2024 """ from app.models import FormSemestre -from app.pe import pe_sxtag, pe_affichage +from app.pe.moys import pe_sxtag from app.pe.rcss import pe_rcs, pe_trajectoires From eff28d64f90509f1b30c1e8b56fea3b3cab56f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Sat, 24 Feb 2024 09:31:47 +0100 Subject: [PATCH 21/41] Divers --- app/pe/moys/pe_interclasstag.py | 64 ++++++++++++++------------------- app/pe/moys/pe_moytag.py | 6 ++-- app/pe/moys/pe_tabletags.py | 16 +++++---- app/pe/pe_jury.py | 33 +++++++++++++---- 4 files changed, 66 insertions(+), 53 deletions(-) diff --git a/app/pe/moys/pe_interclasstag.py b/app/pe/moys/pe_interclasstag.py index 92114f41..c7125607 100644 --- a/app/pe/moys/pe_interclasstag.py +++ b/app/pe/moys/pe_interclasstag.py @@ -278,48 +278,38 @@ class InterClassTag(pe_tabletags.TableTag): etudids_sorted = sorted(list(self.diplomes_ids)) - if self.rcstags: + if not self.rcstags: return None - # Un rcstag significatif pour initier les colonnes - moytag = self._un_rcstag_significatif(self.rcstags) - if not moytag: - return None - df_moytag = moytag.to_df( - aggregat=aggregat, - cohorte="Groupe", - ) - colonnes = list(df_moytag.columns) - # Partant d'un dataframe vierge - df = pd.DataFrame(index=etudids_sorted, columns=colonnes) # colonnes) - for col in colonnes: - if "rang" in col: - df[col] = df[col].astype(str) - df.columns = list(range(len(colonnes))) + initialisation = False + df = pd.DataFrame() - for rcstag in self.rcstags.values(): - # Charge les moyennes au tag d'un RCStag (SemX ou RCSXTag) - if tag in rcstag.moyennes_tags: - # Les infos sur les moyennes du tag - moytag: pe_moytag.MoyennesTag = rcstag.moyennes_tags[tag] - df_moytag = moytag.to_df( - aggregat=aggregat, - cohorte="Groupe", - ) - df_moytag.columns = list(range(len(colonnes))) + for etudid in etudids_sorted: + # Charge ses moyennes au RCSTag suivi + rcs = self.suivis[etudid] # Son Sx ou son RCSemX suivi + if rcs: + rcstag = self.rcstags[rcs.rcs_id] # Son SxTag ou RCSTag + # Charge la moyenne + if tag in rcstag.moyennes_tags: + moytag: pd.DataFrame = rcstag.moyennes_tags[tag] + df_moytag = moytag.to_df( + aggregat=aggregat, + cohorte="Groupe", + ) - # Etudiants/Champs communs entre le df et les données interclassées - ( - etudids_communs, - champs_communs, # les colonnes de synthèse - ) = pe_comp.find_index_and_columns_communs(df, df_moytag) + # Modif les colonnes au regard du 1er df_moytag significatif lu + if not initialisation: + df = pd.DataFrame( + np.nan, index=etudids_sorted, columns=df_moytag.columns + ) + colonnes = list(df_moytag.columns) + for col in colonnes: + if col.endswith("rang"): + df[col] = df[col].astype(str) + initialisation = True - # Injecte les données par tag - df.loc[etudids_communs, champs_communs] = df_moytag.loc[ - etudids_communs, champs_communs - ] + # Injecte les notes de l'étudiant + df.loc[etudid, :] = df_moytag.loc[etudid, :] - # Refixe les colonnes - df.columns = colonnes return df diff --git a/app/pe/moys/pe_moytag.py b/app/pe/moys/pe_moytag.py index 2870be55..3b7374f2 100644 --- a/app/pe/moys/pe_moytag.py +++ b/app/pe/moys/pe_moytag.py @@ -146,10 +146,10 @@ def get_colonne_df(aggregat, tag, champ, cohorte, critere): """Renvoie le tuple (aggregat, tag, champ, cohorte, critere) utilisé pour désigner les colonnes du df""" liste_champs = [] - if aggregat: + if aggregat != None: liste_champs += [aggregat] liste_champs += [tag, champ] - if cohorte: + if cohorte != None: liste_champs += [cohorte] liste_champs += [critere] - return tuple(liste_champs) + return "|".join(liste_champs) diff --git a/app/pe/moys/pe_tabletags.py b/app/pe/moys/pe_tabletags.py index 67564736..8a093a6b 100644 --- a/app/pe/moys/pe_tabletags.py +++ b/app/pe/moys/pe_tabletags.py @@ -125,8 +125,9 @@ class TableTag(object): # Ajout des données par tags for tag in tags_cibles: - moy_tag_df = self.moyennes_tags[tag].to_df(aggregat, cohorte) - df = df.join(moy_tag_df) + if tag in self.moyennes_tags: + moy_tag_df = self.moyennes_tags[tag].to_df(aggregat, cohorte) + df = df.join(moy_tag_df) # Tri par nom, prénom if administratif: @@ -138,7 +139,9 @@ class TableTag(object): # Conversion des colonnes en multiindex if type_colonnes: - df.columns = pd.MultiIndex.from_tuples(df.columns) + colonnes = list(df.columns) + colonnes = [tuple(col.split("|")) for col in colonnes] + df.columns = pd.MultiIndex.from_tuples(colonnes) return df @@ -162,13 +165,13 @@ def _get_champ_administratif(champ, aggregat=None, cohorte=None): """Pour un champ donné, renvoie l'index (ou le multindex) à intégrer au dataframe""" liste = [] - if aggregat: + if aggregat != None: liste += [aggregat] liste += ["Administratif", "Identité"] - if cohorte: + if cohorte != None: liste += [champ] liste += [champ] - return tuple(liste) + return "|".join(liste) def df_administratif( @@ -199,6 +202,7 @@ def df_administratif( _get_champ_administratif(champ, aggregat, cohorte) for champ in CHAMPS_ADMINISTRATIFS ] + df = pd.DataFrame.from_dict(donnees, orient="index", columns=colonnes) df = df.sort_values(by=colonnes[1:]) return df diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index aa87cc8e..81382ffa 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -422,9 +422,16 @@ class JuryPE(object): ) as writer: onglets = [] for onglet, df in synthese.items(): - onglets += [onglet] + if isinstance(onglet, tuple): + if onglet[1] == pe_moytag.CODE_MOY_COMPETENCES: + nom_onglet = onglet[0][: 31 - 5] + "/Comp." + else: + nom_onglet = onglet[0][: 31 - 3] + "/UE" + else: + nom_onglet = onglet + onglets += [nom_onglet] # écriture dans l'onglet: - df.to_excel(writer, onglet, index=True, header=True) + df.to_excel(writer, nom_onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") output.seek(0) @@ -491,7 +498,6 @@ class JuryPE(object): tags = self._do_tags_list(self.interclasstags) for tag in tags: for type_moy in [pe_moytag.CODE_MOY_UE, pe_moytag.CODE_MOY_COMPETENCES]: - pe_affichage.pe_print(f" -> Synthèse du tag {tag} par {type_moy}") synthese[(tag, type_moy)] = self.df_tag_type(tag, type_moy) return synthese @@ -520,8 +526,8 @@ class JuryPE(object): else: aggregats = pe_rcs.TOUS_LES_RCS + aff_aggregat = [] for aggregat in aggregats: - print(aggregat) # Descr de l'aggrégat descr = pe_rcs.TYPES_RCS[aggregat]["descr"] @@ -533,7 +539,9 @@ class JuryPE(object): df_groupe = interclass.compute_df_synthese_moyennes_tag( tag, aggregat=aggregat, type_colonnes=False ) - df = df.join(df_groupe) + if not df_groupe.empty: + aff_aggregat += [aggregat] + df = df.join(df_groupe) # Le dataframe du classement sur la promo df_promo = interclass.to_df( @@ -543,11 +551,22 @@ class JuryPE(object): cohorte="Promo", type_colonnes=False, ) - df = df.join(df_promo) + if not df_promo.empty: + aff_aggregat += [aggregat] + df = df.join(df_promo) + if aff_aggregat: + aff_aggregat = sorted(set(aff_aggregat)) + pe_affichage.pe_print( + f" -> Synthèse de 👜{tag} par {type_moy} avec {', '.join(aff_aggregat)}" + ) + else: + pe_affichage.pe_print(f" -> Synthèse du tag {tag} par {type_moy} : ") # Conversion des colonnes en multiindex if type_colonnes: - df.columns = pd.MultiIndex.from_tuples(df.columns) + colonnes = list(df.columns) + colonnes = [tuple(col.split("|")) for col in colonnes] + df.columns = pd.MultiIndex.from_tuples(colonnes) return df # Fin de l'aggrégat From 02b057ca5a57e4c9faa5c0d71b4458f481aa1a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9o=20BARAS=20=28IUT1=20Grenoble=29?= Date: Sat, 24 Feb 2024 10:48:38 +0100 Subject: [PATCH 22/41] Finalisation des interclassements --- app/pe/moys/pe_tabletags.py | 14 +- app/pe/pe_affichage.py | 2 - app/pe/pe_jury.py | 287 +++++------------- .../pe/pe_view_resultats_etudiant.j2 | 38 +++ app/templates/pe/pe_view_sem_recap.j2 | 4 +- 5 files changed, 109 insertions(+), 236 deletions(-) create mode 100644 app/templates/pe/pe_view_resultats_etudiant.j2 diff --git a/app/pe/moys/pe_tabletags.py b/app/pe/moys/pe_tabletags.py index 8a093a6b..4f5c1c31 100644 --- a/app/pe/moys/pe_tabletags.py +++ b/app/pe/moys/pe_tabletags.py @@ -84,12 +84,7 @@ class TableTag(object): return sorted(tags) def to_df( - self, - administratif=True, - aggregat=None, - tags_cibles=None, - cohorte=None, - type_colonnes=True, + self, administratif=True, aggregat=None, tags_cibles=None, cohorte=None ) -> pd.DataFrame: """Renvoie un dataframe listant toutes les données des moyennes/classements/nb_inscrits/min/max/moy @@ -136,13 +131,6 @@ class TableTag(object): for champ in CHAMPS_ADMINISTRATIFS[1:] ] # Nom + Prénom df = df.sort_values(by=colonnes_tries) - - # Conversion des colonnes en multiindex - if type_colonnes: - colonnes = list(df.columns) - colonnes = [tuple(col.split("|")) for col in colonnes] - df.columns = pd.MultiIndex.from_tuples(colonnes) - return df def has_etuds(self): diff --git a/app/pe/pe_affichage.py b/app/pe/pe_affichage.py index f1ee72a7..55d4f07e 100644 --- a/app/pe/pe_affichage.py +++ b/app/pe/pe_affichage.py @@ -41,5 +41,3 @@ def pe_get_log() -> str: # Affichage dans le tableur pe en cas d'absence de notes SANS_NOTE = "-" -NOM_STAT_GROUPE = "statistiques du groupe" -NOM_STAT_PROMO = "statistiques de la promo" diff --git a/app/pe/pe_jury.py b/app/pe/pe_jury.py index 81382ffa..b4802a24 100644 --- a/app/pe/pe_jury.py +++ b/app/pe/pe_jury.py @@ -52,7 +52,7 @@ import pandas as pd from app.pe.rcss import pe_rcs from app.pe.moys import pe_sxtag -from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE + import app.pe.pe_affichage as pe_affichage import app.pe.pe_etudiant as pe_etudiant from app.pe.moys import ( @@ -116,7 +116,7 @@ class JuryPE(object): self._gen_xls_rcstags(zipfile) self._gen_xls_interclasstags(zipfile) self._gen_xls_synthese_jury_par_tag(zipfile) - # self._gen_xls_synthese_par_etudiant(zipfile) + self._gen_html_synthese_par_etudiant(zipfile) except Exception as e: raise e # et le log @@ -175,6 +175,8 @@ class JuryPE(object): onglet = res_sem_tag.get_repr(verbose=True) onglets += [] df = res_sem_tag.to_df() + # Conversion colonnes en multiindex + df = convert_colonnes_to_multiindex(df) # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") @@ -235,6 +237,8 @@ class JuryPE(object): onglet = sxtag.get_repr(verbose=False) if sxtag.is_significatif(): df = sxtag.to_df() + # Conversion colonnes en multiindex + df = convert_colonnes_to_multiindex(df) onglets += [onglet] # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) @@ -295,6 +299,8 @@ class JuryPE(object): onglet = rcs_tag.get_repr(verbose=False) if rcs_tag.is_significatif(): df = rcs_tag.to_df() + # Conversion colonnes en multiindex + df = convert_colonnes_to_multiindex(df) onglets += [onglet] # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) @@ -370,6 +376,8 @@ class JuryPE(object): onglet = interclass.get_repr() if interclass.is_significatif(): df = interclass.to_df(cohorte="Promo") + # Conversion colonnes en multiindex + df = convert_colonnes_to_multiindex(df) onglets += [onglet] # écriture dans l'onglet df.to_excel(writer, onglet, index=True, header=True) @@ -387,8 +395,20 @@ class JuryPE(object): def _gen_xls_synthese_jury_par_tag(self, zipfile: ZipFile): """Synthèse des éléments du jury PE tag par tag""" - # Synthèse des éléments du jury PE - self.synthese = self.synthetise_jury_par_tags() + pe_affichage.pe_print( + "*** Synthèse finale des moyennes par tag et par type de moyennes (UEs ou Compétences)***" + ) + + self.synthese = {} + pe_affichage.pe_print(" -> Synthèse des données administratives") + self.synthese["administratif"] = self.etudiants.df_administratif( + self.diplomes_ids + ) + + tags = self._do_tags_list(self.interclasstags) + for tag in tags: + for type_moy in [pe_moytag.CODE_MOY_UE, pe_moytag.CODE_MOY_COMPETENCES]: + self.synthese[(tag, type_moy)] = self.df_tag_type(tag, type_moy) # Export des données => mode 1 seule feuille -> supprimé pe_affichage.pe_print("*** Export du jury de synthese par tags") @@ -397,36 +417,15 @@ class JuryPE(object): output, engine="openpyxl" ) as writer: onglets = [] - for (tag, type_moy), df in self.synthese.items(): - onglet = f"{tag} {type_moy}" - onglets += [onglet] - # écriture dans l'onglet: - df.to_excel(writer, onglet, index=True, header=True) - pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}") - output.seek(0) - - self.add_file_to_zip( - zipfile, f"synthese_jury_{self.diplome}_par_tag.xlsx", output.read() - ) - - def _gen_xls_synthese_par_etudiant(self, zipfile: ZipFile): - """Synthèse des éléments du jury PE, étudiant par étudiant""" - # Synthèse des éléments du jury PE - synthese = self.synthetise_jury_par_etudiants() - - # Export des données => mode 1 seule feuille -> supprimé - pe_affichage.pe_print("*** Export du jury de synthese par étudiants") - output = io.BytesIO() - with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated - output, engine="openpyxl" - ) as writer: - onglets = [] - for onglet, df in synthese.items(): + for onglet, df in self.synthese.items(): + # Conversion colonnes en multiindex + df = convert_colonnes_to_multiindex(df) + # Nom de l'onglet if isinstance(onglet, tuple): if onglet[1] == pe_moytag.CODE_MOY_COMPETENCES: - nom_onglet = onglet[0][: 31 - 5] + "/Comp." + nom_onglet = onglet[0][: 31 - 7] + " (Comp)" else: - nom_onglet = onglet[0][: 31 - 3] + "/UE" + nom_onglet = onglet[0][: 31 - 5] + " (UE)" else: nom_onglet = onglet onglets += [nom_onglet] @@ -436,9 +435,21 @@ class JuryPE(object): output.seek(0) self.add_file_to_zip( - zipfile, f"synthese_jury_{self.diplome}_par_etudiant.xlsx", output.read() + zipfile, f"synthese_jury_{self.diplome}_par_tag.xlsx", output.read() ) + def _gen_html_synthese_par_etudiant(self, zipfile: ZipFile): + """Synthèse des éléments du jury PE, étudiant par étudiant""" + # Synthèse des éléments du jury PE + pe_affichage.pe_print("*** Synthèse finale étudiant par étudiant***") + + etudids = list(self.diplomes_ids) + for etudid in etudids: + nom, prenom, html = self.synthetise_jury_etudiant(etudid) + self.add_file_to_zip( + zipfile, f"{nom}_{prenom}.html", html, path="etudiants" + ) + def _add_log_to_zip(self, zipfile): """Add a text file with the log messages""" log_data = pe_affichage.pe_get_log() @@ -482,26 +493,7 @@ class JuryPE(object): # Méthodes pour la synthèse du juryPE # ***************************************************************************************************************** - def synthetise_jury_par_tags(self) -> dict[pd.DataFrame]: - """Synthétise tous les résultats du jury PE dans des dataframes, - dont les onglets sont les tags et des types de calcul de moyennes - (par UEs ou par compétences)""" - - pe_affichage.pe_print( - "*** Synthèse finale des moyennes par tag et par type de moyennes (UEs ou Compétences)***" - ) - - synthese = {} - pe_affichage.pe_print(" -> Synthèse des données administratives") - synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids) - - tags = self._do_tags_list(self.interclasstags) - for tag in tags: - for type_moy in [pe_moytag.CODE_MOY_UE, pe_moytag.CODE_MOY_COMPETENCES]: - synthese[(tag, type_moy)] = self.df_tag_type(tag, type_moy) - return synthese - - def df_tag_type(self, tag, type_moy, type_colonnes=True): + def df_tag_type(self, tag, type_moy): """Génère le DataFrame synthétisant les moyennes/classements (groupe + interclassement promo) pour tous les aggrégats prévus, en fonction du type (UEs ou Compétences) de données souhaitées, @@ -549,7 +541,6 @@ class JuryPE(object): aggregat=aggregat, tags_cibles=[tag], cohorte="Promo", - type_colonnes=False, ) if not df_promo.empty: aff_aggregat += [aggregat] @@ -562,81 +553,31 @@ class JuryPE(object): ) else: pe_affichage.pe_print(f" -> Synthèse du tag {tag} par {type_moy} : ") - # Conversion des colonnes en multiindex - if type_colonnes: - colonnes = list(df.columns) - colonnes = [tuple(col.split("|")) for col in colonnes] - df.columns = pd.MultiIndex.from_tuples(colonnes) + return df # Fin de l'aggrégat - def synthetise_jury_par_etudiants(self) -> dict[pd.DataFrame]: - """Synthétise tous les résultats du jury PE dans des dataframes, - dont les onglets sont les étudiants""" - pe_affichage.pe_print("*** Synthèse finale des moyennes par étudiants***") + def synthetise_jury_etudiant(self, etudid) -> (str, str, str): + """Synthétise les résultats d'un étudiant dans un + fichier html à son nom en s'appuyant sur la synthese final - synthese = {} - pe_affichage.pe_print(" -> Synthèse des données administratives") - synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids) + Returns: + Un tuple nom, prenom, html + """ + etudiant = self.etudiants.identites[etudid] + nom = etudiant.nom + prenom = etudiant.prenom # initial du prénom + html = "toto" - etudids = list(self.diplomes_ids) - - for etudid in etudids: - etudiant = self.etudiants.identites[etudid] - nom = etudiant.nom - prenom = etudiant.prenom[0] # initial du prénom - - onglet = f"{nom} {prenom}. ({etudid})" - if len(onglet) > 32: # limite sur la taille des onglets - fin_onglet = f"{prenom}. ({etudid})" - onglet = f"{nom[:32-len(fin_onglet)-2]}." + fin_onglet - - pe_affichage.pe_print(f" -> Synthèse de l'étudiant {etudid}") - synthese[onglet] = self.df_synthese_etudiant(etudid) - return synthese - - def df_synthese_etudiant(self, etudid: int) -> pd.DataFrame: - """Créé un DataFrame pour un étudiant donné par son etudid, retraçant - toutes ses moyennes aux différents tag et aggrégats""" tags = self._do_tags_list(self.interclasstags) + # for onglet, df_synthese in self.synthese.items(): + # if isinstance(onglet, tuple): # Les onglets autres que "administratif" + # tag = onglet[0] + # type_moy = onglet[1] - donnees = {} + # colonnes = list(df_synthese.columns) - for tag in tags: - # Une ligne pour le tag - donnees[tag] = {("", "", "tag"): tag} - - for aggregat in pe_rcs.TOUS_LES_RCS: - # Le dictionnaire par défaut des moyennes - donnees[tag] |= get_defaut_dict_synthese_aggregat( - aggregat, self.diplome - ) - - # La trajectoire de l'étudiant sur l'aggrégat - trajectoire = self.rcss_jury.trajectoires_suivies[etudid][aggregat] - if trajectoire: - trajectoire_tagguee = self.rcss_tags[trajectoire.sxtag_id] - if tag in trajectoire_tagguee.moyennes_tags: - # L'interclassement - interclass = self.interclasstags[aggregat] - - # Injection des données dans un dictionnaire - donnees[tag] |= get_dict_synthese_aggregat( - aggregat, - trajectoire_tagguee, - interclass, - etudid, - tag, - self.diplome, - ) - - # Fin de l'aggrégat - # Construction du dataFrame - df = pd.DataFrame.from_dict(donnees, orient="index") - - # Tri par nom/prénom - df.sort_values(by=[("", "", "tag")], inplace=True) - return df + return (nom, prenom, html) def get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict: @@ -660,102 +601,10 @@ def get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict: return semestres -def get_defaut_dict_synthese_aggregat(nom_rcs: str, diplome: int) -> dict: - """Renvoie le dictionnaire de synthèse (à intégrer dans - un tableur excel) pour décrire les résultats d'un aggrégat - - Args: - nom_rcs : Le nom du RCS visé - diplôme : l'année du diplôme - """ - # L'affichage de l'aggrégat dans le tableur excel - descr = pe_rcs.get_descr_rcs(nom_rcs) - - nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}" - donnees = { - (descr, "", "note"): SANS_NOTE, - # Les stat du groupe - (descr, NOM_STAT_GROUPE, "class."): SANS_NOTE, - (descr, NOM_STAT_GROUPE, "min"): SANS_NOTE, - (descr, NOM_STAT_GROUPE, "moy"): SANS_NOTE, - (descr, NOM_STAT_GROUPE, "max"): SANS_NOTE, - # Les stats de l'interclassement dans la promo - (descr, nom_stat_promo, "class."): SANS_NOTE, - ( - descr, - nom_stat_promo, - "min", - ): SANS_NOTE, - ( - descr, - nom_stat_promo, - "moy", - ): SANS_NOTE, - ( - descr, - nom_stat_promo, - "max", - ): SANS_NOTE, - } - return donnees - - -def get_dict_synthese_aggregat( - aggregat: str, - trajectoire_tagguee: pe_rcstag.RCSTag, - interclassement_taggue: pe_interclasstag.InterClassTag, - etudid: int, - tag: str, - diplome: int, -): - """Renvoie le dictionnaire (à intégrer au tableur excel de synthese) - traduisant les résultats (moy/class) d'un étudiant à une trajectoire tagguée associée - à l'aggrégat donné et pour un tag donné""" - donnees = {} - # L'affichage de l'aggrégat dans le tableur excel - descr = pe_rcs.get_descr_rcs(aggregat) - - # La note de l'étudiant (chargement à venir) - note = np.nan - - # Les données de la trajectoire tagguée pour le tag considéré - moy_tag = trajectoire_tagguee.moyennes_tags[tag] - - # Les données de l'étudiant - note = moy_tag.get_note_for_df(etudid) - - classement = moy_tag.get_class_for_df(etudid) - nmin = moy_tag.get_min_for_df() - nmax = moy_tag.get_max_for_df() - nmoy = moy_tag.get_moy_for_df() - - # Statistiques sur le groupe - if not pd.isna(note) and note != np.nan: - # Les moyennes de cette trajectoire - donnees |= { - (descr, "", "note"): note, - (descr, NOM_STAT_GROUPE, "class."): classement, - (descr, NOM_STAT_GROUPE, "min"): nmin, - (descr, NOM_STAT_GROUPE, "moy"): nmoy, - (descr, NOM_STAT_GROUPE, "max"): nmax, - } - - # L'interclassement - moy_tag = interclassement_taggue.moyennes_tags[tag] - - classement = moy_tag.get_class_for_df(etudid) - nmin = moy_tag.get_min_for_df() - nmax = moy_tag.get_max_for_df() - nmoy = moy_tag.get_moy_for_df() - - if not pd.isna(note) and note != np.nan: - nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}" - - donnees |= { - (descr, nom_stat_promo, "class."): classement, - (descr, nom_stat_promo, "min"): nmin, - (descr, nom_stat_promo, "moy"): nmoy, - (descr, nom_stat_promo, "max"): nmax, - } - - return donnees +def convert_colonnes_to_multiindex(df): + """Convertit les colonnes d'un df pour obtenir des colonnes + multiindex""" + colonnes = list(df.columns) + colonnes = [tuple(col.split("|")) for col in colonnes] + df.columns = pd.MultiIndex.from_tuples(colonnes) + return df diff --git a/app/templates/pe/pe_view_resultats_etudiant.j2 b/app/templates/pe/pe_view_resultats_etudiant.j2 new file mode 100644 index 00000000..4aa297e0 --- /dev/null +++ b/app/templates/pe/pe_view_resultats_etudiant.j2 @@ -0,0 +1,38 @@ + + + + + + {% block title %}Service d'un prof{% endblock %} + + + + + + + +

    Résultats PE de {{prenom}} {{nom}}

    + +

    Résultats calculés par UEs

    + +{% for tag in tags %} + +

    {{ tag }}

    + +{% endfor %} + +

    Résultats calculés par Compétences

    + +{% for tag in tags %} + +

    {{ tag }}

    + +{% endfor %} + + + + \ No newline at end of file diff --git a/app/templates/pe/pe_view_sem_recap.j2 b/app/templates/pe/pe_view_sem_recap.j2 index 250dc078..4766daac 100644 --- a/app/templates/pe/pe_view_sem_recap.j2 +++ b/app/templates/pe/pe_view_sem_recap.j2 @@ -30,7 +30,7 @@

    Cette fonction génère un ensemble de feuilles de calcul (xlsx) permettant d'éditer des avis de poursuites d'études pour les étudiants - de BUT diplômés. + de BUT diplômés. Les calculs sous-jacents peuvent prendre un peu de temps (1 à 2 minutes).
    De nombreux aspects sont paramétrables: Avis de poursuites d'études de la promo {{ annee_diplome }}

    - Seront pris en compte les étudiants ayant été inscrits à l'un des semestres suivants : + Seront pris en compte les étudiants ayant (au moins) été inscrits à l'un des semestres suivants :