ScoDoc/app/pe/pe_tabletags.py

229 lines
8.1 KiB
Python

# -*- 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 datetime
import numpy as np
from app import ScoValueError
from app.comp.moy_sem import comp_ranks_series
from app.pe import pe_affichage
from app.pe.pe_affichage import SANS_NOTE
from app.scodoc import sco_utils as scu
import pandas as pd
TAGS_RESERVES = ["but"]
class MoyenneTag:
def __init__(self, tag: str, notes: pd.Series):
"""Classe centralisant la synthèse des moyennes/classements d'une série
d'étudiants à un tag donné, en stockant un dictionnaire :
``
{
"notes": la Serie pandas des notes (float),
"classements": la Serie pandas des classements (float),
"min": la note minimum,
"max": la note maximum,
"moy": la moyenne,
"nb_inscrits": le nombre d'étudiants ayant une note,
}
``
Args:
tag: Un tag
note: Une série de notes (moyenne) sous forme d'un pd.Series()
"""
self.tag = tag
"""Le tag associé à la moyenne"""
self.synthese = self.comp_moy_et_stat(notes)
"""La synthèse 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 (souvent une moyenne par 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)
"""
# Supprime d'éventuels chaines de caractères dans les notes
notes = pd.to_numeric(notes, errors="coerce")
# Les indices des ... et les notes non nulles/pertinentes
indices = notes.notnull()
notes_non_nulles = notes[indices]
# Les classements sur les notes non nulles
(_, class_gen_ue_non_nul) = comp_ranks_series(notes_non_nulles)
# Les classements (toutes notes confondues, avec NaN si pas de notes)
class_gen_ue = pd.Series(np.nan, index=notes.index) # , dtype="Int64")
class_gen_ue[indices] = class_gen_ue_non_nul[indices]
synthese = {
"notes": notes,
"classements": class_gen_ue,
"min": notes.min(),
"max": notes.max(),
"moy": notes.mean(),
"nb_inscrits": sum(indices),
}
return synthese
def get_df_notes(self, arrondi=False):
"""Série des notes, arrondies à 2 chiffres après la virgule"""
if arrondi:
serie = self.synthese["notes"].round(2)
else:
serie = self.synthese["notes"]
df = serie.to_frame("notes")
return df
def get_df_rangs_pertinents(self) -> pd.Series:
"""Série des rangs classement/nbre_inscrit"""
classement = self.synthese["classements"]
indices = classement[classement.notnull()].index.to_list()
classement_non_nul = classement.loc[indices].to_frame("classements")
classement_non_nul.insert(1, "rangs", np.nan)
nb_inscrit = self.synthese["nb_inscrits"]
classement_non_nul["rangs"] = classement_non_nul["classements"].astype(int).astype(str) + "/" + str(nb_inscrit)
return classement_non_nul["rangs"].to_frame("rangs")
def get_note_for_df(self, etudid: int):
"""Note d'un étudiant donné par son etudid"""
return round(self.synthese["notes"].loc[etudid], 2)
def get_min_for_df(self) -> float:
"""Min renseigné pour affichage dans un df"""
return round(self.synthese["min"], 2)
def get_max_for_df(self) -> float:
"""Max renseigné pour affichage dans un df"""
return round(self.synthese["max"], 2)
def get_moy_for_df(self) -> float:
"""Moyenne renseignée pour affichage dans un df"""
return round(self.synthese["moy"], 2)
def get_class_for_df(self, etudid: int) -> str:
"""Classement ramené au nombre d'inscrits,
pour un étudiant donné par son etudid"""
classement = self.synthese["classements"].loc[etudid]
nb_inscrit = self.synthese["nb_inscrits"]
if not pd.isna(classement):
classement = int(classement)
return f"{classement}/{nb_inscrit}"
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
SemestreTag, TrajectoireTag, AggregatInterclassTag
"""
pass
# -----------------------------------------------------------------------------------------------------------
def get_all_tags(self):
"""Liste des tags de la table, triée par ordre alphabétique,
extraite des clés du dictionnaire ``moyennes_tags`` connues (tags en doublon
possible).
Returns:
Liste de tags triés par ordre alphabétique
"""
return sorted(list(self.moyennes_tags.keys()))
def df_moyennes_et_classements(self) -> pd.DataFrame:
"""Renvoie un dataframe listant toutes les moyennes,
et les classements des étudiants pour tous les tags.
Est utilisé pour afficher le détail d'un tableau taggué
(semestres, trajectoires ou aggrégat)
Returns:
Le dataframe des notes et des classements
"""
etudiants = self.etudiants
df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"])
tags_tries = self.get_all_tags()
for tag in tags_tries:
moy_tag = self.moyennes_tags[tag]
df = df.join(moy_tag.synthese["notes"].rename(f"Moy {tag}"))
df = df.join(moy_tag.synthese["classements"].rename(f"Class {tag}"))
return df
def df_notes(self) -> pd.DataFrame | None:
"""Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags
Returns:
Un dataframe etudids x tag (avec tag par ordre alphabétique)
"""
tags_tries = self.get_all_tags()
if tags_tries:
dict_series = {}
for tag in tags_tries:
# Les moyennes associés au tag
moy_tag = self.moyennes_tags[tag]
dict_series[tag] = moy_tag.synthese["notes"]
df = pd.DataFrame(dict_series)
return df