This commit is contained in:
Emmanuel Viennet 2024-02-02 18:24:00 +01:00
commit 027f11e494
10 changed files with 146 additions and 190 deletions

View File

@ -100,7 +100,7 @@ def compute_sem_moys_apc_using_ects(
def comp_ranks_series(notes: pd.Series) -> tuple[pd.Series, pd.Series]:
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
"""Calcul rangs à partir d'une série ("vecteur") de notes (index etudid, valeur
numérique) en tenant compte des ex-aequos.
Result: couple (tuple)

View File

@ -273,7 +273,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
return s.index[s.notna()]
def etud_parcours_ues_ids(self, etudid: int) -> set[int]:
"""Ensemble les id des UEs que l'étudiant doit valider dans ce semestre compte tenu
"""Ensemble des id des UEs que l'étudiant doit valider dans ce semestre compte tenu
du parcours dans lequel il est inscrit.
Se base sur le parcours dans ce semestre, et le référentiel de compétences.
Note: il n'est pas nécessairement inscrit à toutes ces UEs.

View File

@ -85,16 +85,12 @@ class EtudiantsJuryPE:
self.abandons_ids = {}
"""Les etudids des étudiants redoublants/réorientés"""
def find_etudiants(self, formation_id: int):
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``
dans la formation ``formation_id``. XXX TODO voir si on garde formation_id qui n'est pas utilisé ici
de manière automatique par rapport à leur année de diplomation ``annee_diplome``.
Les données obtenues sont stockées dans les attributs de EtudiantsJuryPE.
formation_id: L'identifiant de la formation (inutilisé)
*Remarque* : ex: JuryPE.get_etudiants_in_jury()
"""
cosemestres = pe_comp.get_cosemestres_diplomants(self.annee_diplome, None)

View File

@ -59,8 +59,9 @@ class AggregatInterclasseTag(TableTag):
for etudid in self.diplomes_ids:
self.suivi[etudid] = trajectoires_jury_pe.suivi[etudid][nom_aggregat]
"""Les tags"""
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()
@ -69,15 +70,7 @@ class AggregatInterclasseTag(TableTag):
self.moyennes_tags = {}
for tag in self.tags_sorted:
moy_gen_tag = self.notes[tag]
class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int
self.moyennes_tags[tag] = {
"notes": moy_gen_tag,
"classements": class_gen_tag,
"min": moy_gen_tag.min(),
"max": moy_gen_tag.max(),
"moy": moy_gen_tag.mean(),
"nb_inscrits": len(moy_gen_tag),
}
self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag)
# Est significatif ? (aka a-t-il des tags et des notes)
self.significatif = len(self.tags_sorted) > 0
@ -125,4 +118,7 @@ class AggregatInterclasseTag(TableTag):
etudids_communs, tags_communs
]
# Force les nan
df.fillna(np.nan)
return df

View File

@ -83,7 +83,7 @@ class JuryPE(object):
# leur affichage dans les avis latex
# ------------------------------------------------------------------------------------------------------------------
def __init__(self, diplome, formation_id):
def __init__(self, diplome):
"""
Création d'une table PE sur la base d'un semestre selectionné. De ce semestre est déduit :
1. l'année d'obtention du DUT,
@ -98,19 +98,16 @@ class JuryPE(object):
self.diplome = diplome
"L'année du diplome"
self.formation_id = formation_id
"La formation associée au diplome"
self.nom_export_zip = f"Jury_PE_{self.diplome}"
"Nom du zip où ranger les fichiers générés"
# 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} pour la formation {self.formation_id}"""
self.diplome}"""
)
self.etudiants = EtudiantsJuryPE(self.diplome) # Les infos sur les étudiants
self.etudiants.find_etudiants(self.formation_id)
self.etudiants.find_etudiants()
self.diplomes_ids = self.etudiants.diplomes_ids
self.zipdata = io.BytesIO()
@ -610,7 +607,7 @@ def get_dict_synthese_aggregat(
note = TableTag.get_note_for_df(bilan, etudid)
# Statistiques sur le groupe
if note != np.nan:
if not pd.isna(note) and note != np.nan:
# Les moyennes de cette trajectoire
donnees |= {
(descr, "", "note"): note,
@ -640,7 +637,7 @@ def get_dict_synthese_aggregat(
if tag in interclassement_taggue.moyennes_tags:
bilan = interclassement_taggue.moyennes_tags[tag]
if note != np.nan:
if not pd.isna(note) and note != np.nan:
nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}"
donnees |= {

View File

@ -35,9 +35,12 @@ Created on Fri Sep 9 09:15:05 2016
@author: barasc
"""
import numpy as np
import app.pe.pe_etudiant
from app import db, log
from app.comp import res_sem, moy_ue, moy_sem
from app.comp.moy_sem import comp_ranks_series
from app.comp.res_compat import NotesTableCompat
from app.comp.res_sem import load_formsemestre_results
from app.models import FormSemestre
@ -49,8 +52,8 @@ import app.pe.pe_affichage as pe_affichage
from app.pe.pe_tabletags import TableTag, TAGS_RESERVES
import pandas as pd
class SemestreTag(TableTag):
class SemestreTag(TableTag):
def __init__(self, formsemestre_id: int):
"""
Un SemestreTag représente les résultats des étudiants à un semestre, en donnant
@ -89,47 +92,50 @@ class SemestreTag(TableTag):
self.ues_inscr_parcours_df = self.nt.load_ues_inscr_parcours()
self.dispense_ues = self.nt.dispense_ues
# Les tags (en supprimant les tags réservés)
self.tags = get_synthese_tags_semestre(self.nt.formsemestre)
for tag in TAGS_RESERVES:
if tag in self.tags:
del self.tags[tag]
# Les tags :
## Saisis par l'utilisateur
self.tags_personnalises = get_synthese_tags_personnalises_semestre(
self.nt.formsemestre
)
## Déduit des compétences
self.tags_competences = get_noms_competences_from_ues(self.nt.formsemestre)
# Supprime les doublons dans les tags
tags_reserves = TAGS_RESERVES + list(self.tags_competences.values())
for tag in self.tags_personnalises:
if tag in tags_reserves:
del self.tags_personnalises[tag]
pe_affichage.pe_print(f"Supprime le tag {tag}")
# Calcul des moyennes & les classements de chaque étudiant à chaque tag
self.moyennes_tags = {}
for tag in self.tags:
for tag in self.tags_personnalises:
# pe_affichage.pe_print(f" -> Traitement du tag {tag}")
moy_gen_tag = self.compute_moyenne_tag(tag)
class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int
self.moyennes_tags[tag] = {
"notes": moy_gen_tag,
"classements": class_gen_tag,
"min": moy_gen_tag.min(),
"max": moy_gen_tag.max(),
"moy": moy_gen_tag.mean(),
"nb_inscrits": len(moy_gen_tag),
}
self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag)
# Ajoute les moyennes générales de BUT pour le semestre considéré
moy_gen_but = self.nt.etud_moy_gen
class_gen_but = self.nt.etud_moy_gen_ranks_int
self.moyennes_tags["but"] = {
"notes": moy_gen_but,
"classements": class_gen_but,
"min": moy_gen_but.min(),
"max": moy_gen_but.max(),
"moy": moy_gen_but.mean(),
"nb_inscrits": len(moy_gen_but),
}
moy_gen_but = pd.to_numeric(moy_gen_but, errors="coerce")
self.moyennes_tags["but"] = self.comp_moy_et_stat(moy_gen_but)
# Ajoute les moyennes par compétence
for ue_id, competence in self.tags_competences.items():
moy_ue = self.nt.etud_moy_ue[ue_id]
self.moyennes_tags[competence] = self.comp_moy_et_stat(moy_ue)
# Synthétise l'ensemble des moyennes dans un dataframe
self.tags_sorted = sorted(self.moyennes_tags) # les tags par ordre alphabétique
self.tags_sorted = sorted(
self.moyennes_tags
) # les tags (personnalisés+compétences) par ordre alphabétique
self.notes = (
self.df_notes()
) # Le dataframe synthétique des notes (=moyennes par tag)
pe_affichage.pe_print(f" => Traitement des tags {', '.join(self.tags_sorted)}")
pe_affichage.pe_print(
f" => Traitement des tags {', '.join(self.tags_sorted)}"
)
def get_repr(self):
"""Nom affiché pour le semestre taggué"""
@ -157,13 +163,13 @@ class SemestreTag(TableTag):
"""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 self.tags[tag]:
if modimpl.moduleimpl_id not in self.tags_personnalises[tag]:
modimpls_mask[i] = False
"""Applique la pondération des coefficients"""
modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy()
for modimpl_id in self.tags[tag]:
ponderation = self.tags[tag][modimpl_id]["ponderation"]
for modimpl_id in self.tags_personnalises[tag]:
ponderation = self.tags_personnalises[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)"""
@ -193,95 +199,6 @@ class SemestreTag(TableTag):
return moy_gen_tag
def get_noteEtCoeff_modimpl(self, modimpl_id, etudid, profondeur=2):
"""Renvoie un couple donnant la note et le coeff normalisé d'un étudiant à un module d'id modimpl_id.
La note et le coeff sont extraits :
1) soit des données du semestre en normalisant le coefficient par rapport à la somme des coefficients des modules du semestre,
2) soit des données des UE précédemment capitalisées, en recherchant un module de même CODE que le modimpl_id proposé,
le coefficient normalisé l'étant alors par rapport au total des coefficients du semestre auquel appartient l'ue capitalisée
TODO:: A rependre si nécessaire
"""
def get_ue_capitalisees(etudid) -> list[dict]:
"""Renvoie la liste des capitalisation effectivement capitalisées par un étudiant"""
if etudid in self.nt.validations.ue_capitalisees.index:
return self.nt.validations.ue_capitalisees.loc[[etudid]].to_dict("records")
return []
(note, coeff_norm) = (None, None)
modimpl = get_moduleimpl(modimpl_id) # Le module considéré
if modimpl == None or profondeur < 0:
return (None, None)
# Y-a-t-il eu capitalisation d'UE ?
ue_capitalisees = get_ue_capitalisees(
etudid
) # les ue capitalisées des étudiants
ue_capitalisees_id = {
ue_cap["ue_id"] for ue_cap in ue_capitalisees
} # les id des ue capitalisées
# Si le module ne fait pas partie des UE capitalisées
if modimpl.module.ue.id not in ue_capitalisees_id:
note = self.nt.get_etud_mod_moy(modimpl_id, etudid) # lecture de la note
coeff = modimpl.module.coefficient or 0.0 # le coeff (! non compatible BUT)
coeff_norm = (
coeff / self.somme_coeffs if self.somme_coeffs != 0 else 0
) # le coeff normalisé
# Si le module fait partie d'une UE capitalisée
elif len(ue_capitalisees) > 0:
moy_ue_actuelle = get_moy_ue_from_nt(
self.nt, etudid, modimpl_id
) # la moyenne actuelle
# A quel semestre correspond l'ue capitalisée et quelles sont ses notes ?
fids_prec = [
ue_cap["formsemestre_id"]
for ue_cap in ue_capitalisees
if ue_cap["ue_code"] == modimpl.module.ue.ue_code
] # and ue['semestre_id'] == semestre_id]
if len(fids_prec) > 0:
# => le formsemestre_id du semestre dont vient la capitalisation
fid_prec = fids_prec[0]
# Lecture des notes de ce semestre
# le tableau de note du semestre considéré:
formsemestre_prec = FormSemestre.get_formsemestre(fid_prec)
nt_prec: NotesTableCompat = res_sem.load_formsemestre_results(
formsemestre_prec
)
# Y-a-t-il un module équivalent c'est à dire correspondant au même code (le module_id n'étant pas significatif en cas de changement de PPN)
modimpl_prec = [
modi
for modi in nt_prec.formsemestre.modimpls_sorted
if modi.module.code == modimpl.module.code
]
if len(modimpl_prec) > 0: # si une correspondance est trouvée
modprec_id = modimpl_prec[0].id
moy_ue_capitalisee = get_moy_ue_from_nt(nt_prec, etudid, modprec_id)
if (
moy_ue_capitalisee is None
) or moy_ue_actuelle >= moy_ue_capitalisee: # on prend la meilleure ue
note = self.nt.get_etud_mod_moy(
modimpl_id, etudid
) # lecture de la note
coeff = modimpl.module.coefficient # le coeff
# nota: self.somme_coeffs peut être None
coeff_norm = (
coeff / self.somme_coeffs if self.somme_coeffs else 0
) # le coeff normalisé
else:
semtag_prec = SemestreTag(nt_prec, nt_prec.sem)
(note, coeff_norm) = semtag_prec.get_noteEtCoeff_modimpl(
modprec_id, etudid, profondeur=profondeur - 1
) # lecture de la note via le semtag associé au modimpl capitalisé
# Sinon - pas de notes à prendre en compte
return (note, coeff_norm)
def get_moduleimpl(modimpl_id) -> dict:
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
@ -308,21 +225,13 @@ def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float:
return ue_status["moy"]
# -----------------------------------------------------------------------------
def get_synthese_tags_semestre(formsemestre: FormSemestre):
def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre):
"""Etant données les implémentations des modules du semestre (modimpls),
synthétise les tags les concernant (tags saisis dans le programme pédagogique)
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)).
{ tagname1: { modimpl_id1: { 'module_id': ...,
'coeff': ...,
'coeff_norm': ...,
'ponderation': ...,
'module_code': ...,
'ue_xxx': ...},
}
}
Args:
formsemestre: Le formsemestre à la base de la recherche des tags
@ -364,3 +273,26 @@ def get_synthese_tags_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.
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.type != UE_SPORT:
ordre = ue.niveau_competence.ordre
nom = ue.niveau_competence.competence.titre
noms_competences[ue.ue_id] = f"comp. {nom}"
return noms_competences

View File

@ -40,6 +40,8 @@ Created on Thu Sep 8 09:36:33 2016
import datetime
import numpy as np
from app.comp.moy_sem import comp_ranks_series
from app.pe import pe_affichage
from app.scodoc import sco_utils as scu
import pandas as pd
@ -104,6 +106,44 @@ class TableTag(object):
return df.to_csv(sep=";")
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": len(indices),
}
return synthese
@classmethod
def get_min_for_df(cls, bilan: dict) -> float:
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
@ -127,12 +167,15 @@ class TableTag(object):
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
renvoie le classement ramené au nombre d'inscrits,
pour un étudiant donné par son etudid"""
return f"{bilan['classements'].loc[etudid]}/{bilan['nb_inscrits']}"
classement = bilan['classements'].loc[etudid]
if not pd.isna(classement):
return f"{classement}/{bilan['nb_inscrits']}"
else:
return pe_affichage.SANS_NOTE
@classmethod
def get_note_for_df(cls, bilan: dict, etudid: int):
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
renvoie la note (moyenne)
pour un étudiant donné par son etudid"""
return round(bilan["notes"].loc[etudid], 2)
return round(bilan["notes"].loc[etudid], 2)

View File

@ -47,11 +47,8 @@ from app.pe.pe_tabletags import TableTag
class TrajectoireTag(TableTag):
def __init__(
self,
trajectoire: Trajectoire,
semestres_taggues: dict[int, SemestreTag]
self, trajectoire: Trajectoire, semestres_taggues: dict[int, SemestreTag]
):
"""Calcule les moyennes par tag d'une combinaison de semestres
(trajectoires), identifiée par un nom d'aggrégat (par ex: '3S') et
@ -71,11 +68,13 @@ class TrajectoireTag(TableTag):
# Le nom de la trajectoire tagguée (identique à la trajectoire)
self.nom = self.get_repr()
"""Le formsemestre terminal et les semestres aggrégés"""
self.formsemestre_terminal = trajectoire.semestre_final
"""Le formsemestre terminal"""
# Les résultats du formsemestre terminal
nt = load_formsemestre_results(self.formsemestre_terminal)
self.semestres_aggreges = trajectoire.semestres_aggreges
"""Les semestres aggrégés"""
self.semestres_tags_aggreges = {}
"""Les semestres tags associés aux semestres aggrégés"""
@ -87,32 +86,25 @@ class TrajectoireTag(TableTag):
"""Les étudiants (état civil + cursus connu)"""
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}
"""Les tags extraits de tous les semestres"""
self.tags_sorted = self.do_taglist()
"""Tags extraits de tous les semestres"""
"""Construit le cube de notes"""
self.notes_cube = self.compute_notes_cube()
"""Cube de notes"""
"""Calcul les moyennes par tag sous forme d'un dataframe"""
etudids = list(self.etudiants.keys())
self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted)
"""Calcul les moyennes par tag sous forme d'un dataframe"""
"""Synthétise les moyennes/classements par tag"""
self.moyennes_tags = {}
"""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]
class_gen_tag = moy_sem.comp_ranks_series(moy_gen_tag)[1] # en int
self.moyennes_tags[tag] = {
"notes": moy_gen_tag,
"classements": class_gen_tag,
"min": moy_gen_tag.min(),
"max": moy_gen_tag.max(),
"moy": moy_gen_tag.mean(),
"nb_inscrits": len(moy_gen_tag),
}
self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag)
def get_repr(self, verbose=False) -> str:
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
@ -123,11 +115,11 @@ class TrajectoireTag(TableTag):
"""Construit le cube de notes (etudid x tags x semestre_aggregé)
nécessaire au calcul des moyennes de l'aggrégat
"""
nb_tags = len(self.tags_sorted)
nb_etudiants = len(self.etuds)
nb_semestres = len(self.semestres_tags_aggreges)
# 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)"""
# 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())
@ -135,17 +127,17 @@ class TrajectoireTag(TableTag):
dfs = {}
for frmsem_id in semestres_id:
"""Partant d'un dataframe vierge"""
# Partant d'un dataframe vierge
df = pd.DataFrame(np.nan, index=etudids, columns=tags)
"""Charge les notes du semestre tag"""
# Charge les notes du semestre tag
notes = self.semestres_tags_aggreges[frmsem_id].notes
"""Les étudiants & les tags commun au dataframe final et aux notes du semestre)"""
# 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)
"""Injecte les notes par tag"""
# Injecte les notes par tag
df.loc[etudids_communs, tags_communs] = notes.loc[
etudids_communs, tags_communs
]
@ -154,7 +146,7 @@ class TrajectoireTag(TableTag):
for col in df.columns:
df[col] = pd.to_numeric(df[col], errors="coerce")
"""Stocke le df"""
# Stocke le df
dfs[frmsem_id] = df
"""Réunit les notes sous forme d'un cube etdids x tags x semestres"""
@ -175,8 +167,6 @@ class TrajectoireTag(TableTag):
return sorted(set(tags))
def compute_tag_moy(set_cube: np.array, etudids: list, tags: list):
"""Calcul de la moyenne par tag sur plusieurs semestres.
La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles
@ -214,4 +204,6 @@ def compute_tag_moy(set_cube: np.array, etudids: list, tags: list):
columns=tags, # les tags
)
etud_moy_tag_df.fillna(np.nan)
return etud_moy_tag_df

View File

@ -78,7 +78,7 @@ def pe_view_sem_recap(formsemestre_id: int):
sco=ScoData(formsemestre=formsemestre),
)
jury = pe_jury.JuryPE(annee_diplome, formsemestre.formation.formation_id)
jury = pe_jury.JuryPE(annee_diplome)
if not jury.diplomes_ids:
flash("aucun étudiant à considérer !")
return redirect(

View File

@ -882,7 +882,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<form>
<input type="checkbox" class="sco_tag_checkbox"
{'checked' if show_tags else ''}
>montrer les tags des modules</input>
> Montrer les tags des modules voire en ajouter <i>(ceux correspondant aux titres des compétences étant ajoutés par défaut)</i></input>
</form>
"""
)