ScoDoc/app/pe/pe_tabletags.py

264 lines
9.0 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.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 moyenne est non nulle"""
self.df: pd.DataFrame = self.comp_moy_et_stat(notes)
"""Le dataframe retraçant les moyennes/classements/statistiques"""
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 (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)
"""
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)
df.loc[self.inscrits_ids, "nb_inscrits"] = len(self.inscrits_ids)
# Le classement des inscrits
notes_non_nulles = notes[self.inscrits_ids]
(class_str, class_int) = comp_ranks_series(notes_non_nulles)
df.loc[self.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[self.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()
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(),
}
return synthese
def get_notes(self):
"""Série des notes, arrondies à 2 chiffres après la virgule"""
return self.df["note"].round(2)
def get_rangs_inscrits(self) -> pd.Series:
"""Série des rangs classement/nbre_inscrit"""
return self.df["rang"]
def get_min(self) -> pd.Series:
"""Série des min"""
return self.df["min"].round(2)
def get_max(self) -> pd.Series:
"""Série des max"""
return self.df["max"].round(2)
def get_moy(self) -> pd.Series:
"""Série des moy"""
return self.df["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)
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["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
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