# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2021 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 # ############################################################################## """Fonctions de calcul des moyennes de modules (modules, ressources ou SAÉ) Rappel: pour éviter les confusions, on appelera *poids* les coefficients d'une évaluation dans un module, et *coefficients* ceux utilisés pour le calcul de la moyenne générale d'une UE. """ import numpy as np import pandas as pd from pandas.core.frame import DataFrame from app import db from app import models from app.models import ModuleImpl, Evaluation, EvaluationUEPoids from app.scodoc import sco_utils as scu def df_load_evaluations_poids(moduleimpl_id: int, default_poids=0.0) -> pd.DataFrame: """Charge poids des évaluations d'un module et retourne un dataframe rows = evaluations, columns = UE, value = poids (float). Les valeurs manquantes (évaluations sans coef vers des UE) sont remplies par default_poids. """ modimpl = ModuleImpl.query.get(moduleimpl_id) evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all() ues = modimpl.formsemestre.query_ues().all() ue_ids = [ue.id for ue in ues] evaluation_ids = [evaluation.id for evaluation in evaluations] df = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float) for eval_poids in EvaluationUEPoids.query.join( EvaluationUEPoids.evaluation ).filter_by(moduleimpl_id=moduleimpl_id): df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids if default_poids is not None: df.fillna(value=default_poids, inplace=True) return df def check_moduleimpl_conformity( moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame ) -> bool: """Vérifie que les évaluations de ce moduleimpl sont bien conformes au PN. Un module est dit *conforme* si et seulement si la somme des poids de ses évaluations vers une UE de coefficient non nul est non nulle. """ module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0 check = all( (modules_coefficients[moduleimpl.module.id].to_numpy() != 0) == module_evals_poids ) return check def df_load_modimpl_notes(moduleimpl_id: int) -> pd.DataFrame: """Construit un dataframe avec toutes les notes des évaluations du module. colonnes: evaluation_id (le nom de la colonne est l'evaluation_id en str) index (lignes): etudid L'ensemble des étudiants est celui des inscrits au module. Valeurs des notes: note : float (valeur enregistrée brute, pas normalisée sur 20) pas de note: NaN absent: 0. excusé: NOTES_NEUTRALISE (voir sco_utils) attente: NOTES_ATTENTE N'utilise pas de cache ScoDoc. """ etudids = [e.etudid for e in ModuleImpl.query.get(moduleimpl_id).inscriptions] evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id) df = pd.DataFrame(index=etudids, dtype=float) # empty df with all students for evaluation in evaluations: eval_df = pd.read_sql( """SELECT etudid, value AS "%(evaluation_id)s" FROM notes_notes WHERE evaluation_id=%(evaluation_id)s""", db.engine, params={"evaluation_id": evaluation.evaluation_id}, index_col="etudid", ) # Remplace les ABS (NULL en BD, donc NaN ici) par des zéros. eval_df.fillna(value=0.0, inplace=True) df = df.merge(eval_df, how="outer", left_index=True, right_index=True) return df def compute_module_moy(evals_notes: pd.DataFrame, evals_poids: pd.DataFrame): """Calcule les moyennes des étudiants dans ce module - evals_notes : DataFrame, colonnes: EVALS, Lignes: etudid valeur: float, ou NOTES_ATTENTE ou NOTES_NEUTRALISE Les NaN (ABS) doivent avoir déjà été remplacés par des zéros. - evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs Résultat: DataFrame, colonnes UE, lignes etud = la note de l'étudiant dans chaque UE pour ce module. ou NaN si les évaluations (dans lesquelles l'étudiant à des notes) ne donnent pas de coef vers cette UE. """ nb_etuds = len(evals_notes) nb_ues = evals_poids.shape[1] etud_moy_module_arr = np.zeros((nb_etuds, nb_ues)) evals_poids_arr = evals_poids.to_numpy().transpose() evals_notes_arr = evals_notes.values # .to_numpy() val_neutres = np.array((scu.NOTES_NEUTRALISE, scu.NOTES_ATTENTE)) for i in range(nb_etuds): note_vect = evals_notes_arr[ i ] # array [note_ue1, note_ue2, ...] de l'étudiant i # Les poids des évals pour cet étudiant: là où il a des notes non neutralisées evals_poids_etud_arr = np.where( np.isin(note_vect, val_neutres, invert=True), evals_poids_arr, 0.0 ) # Calcule la moyenne pondérée sur les notes disponibles with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) etud_moy_module_arr[i] = (note_vect * evals_poids_etud_arr).sum( axis=1 ) / evals_poids_etud_arr.sum(axis=1) etud_moy_module_df = pd.DataFrame( etud_moy_module_arr, index=evals_notes.index, columns=evals_poids.columns ) return etud_moy_module_df