From 235019102b48f509c26141ad21d17c0726db2333 Mon Sep 17 00:00:00 2001 From: Bertrand Benjamin Date: Tue, 20 Apr 2021 18:30:12 +0200 Subject: [PATCH] Feat: Final mark for students --- recopytex/dashboard/pages/exams_scores/app.py | 109 ++++++++++-------- .../dashboard/pages/exams_scores/callbacks.py | 15 +++ .../dashboard/pages/exams_scores/models.py | 69 ++++++++++- recopytex/datalib/on_score_column.py | 3 +- 4 files changed, 139 insertions(+), 57 deletions(-) diff --git a/recopytex/dashboard/pages/exams_scores/app.py b/recopytex/dashboard/pages/exams_scores/app.py index 4e6fe02..fa567ff 100644 --- a/recopytex/dashboard/pages/exams_scores/app.py +++ b/recopytex/dashboard/pages/exams_scores/app.py @@ -7,62 +7,71 @@ from .models import get_tribes, get_exams from .callbacks import * layout = html.Div( - children=[ - html.Header( - children=[ - html.H1("Analyse des notes"), - html.P("Dernière sauvegarde", id="lastsave"), + children=[ + html.Header( + children=[ + html.H1("Analyse des notes"), + html.P("Dernière sauvegarde", id="lastsave"), + ], + ), + html.Main( + children=[ + html.Section( + children=[ + html.Div( + children=[ + "Classe: ", + dcc.Dropdown( + id="tribe", + options=[ + {"label": t["name"], "value": t["name"]} + for t in get_tribes().values() + ], + value=next(iter(get_tribes().values()))["name"], + ), + ], + ), + html.Div( + children=[ + "Evaluation: ", + dcc.Dropdown(id="exam_select"), + ], + ), ], + id="selects", ), - html.Main( - children=[ - html.Section( - children=[ - html.Div( - children=[ - "Classe: ", - dcc.Dropdown( - id="tribe", - options=[ - {"label": t["name"], "value": t["name"]} - for t in get_tribes().values() - ], - value=next(iter(get_tribes().values()))["name"], - ), + html.Section( + children=[ + html.Div( + children=[ + dash_table.DataTable( + id="final_score_table", + columns=[ + {"name": "Étudiant", "id": "student_name"}, + {"name": "Note", "id": "mark"}, + {"name": "Barème", "id": "score_rate"}, ], - ), - html.Div( - children=[ - "Evaluation: ", - dcc.Dropdown(id="exam_select"), - ], - ), - ], - id="selects", - ), - html.Section( - children=[ - html.Div( - children=[], - id="final_score_table_container", - ), - ], - id="analysis", - ), - html.Section( - children=[ - dash_table.DataTable( - id="scores_table", - columns=[], - style_data_conditional=[], - fixed_columns={}, - editable=True, ) ], - id="edit", + id="final_score_table_container", ), ], + id="analysis", + ), + html.Section( + children=[ + dash_table.DataTable( + id="scores_table", + columns=[], + style_data_conditional=[], + fixed_columns={}, + editable=True, + ) + ], + id="edit", ), - dcc.Store(id="scores"), ], - ) + ), + dcc.Store(id="scores"), + ], +) diff --git a/recopytex/dashboard/pages/exams_scores/callbacks.py b/recopytex/dashboard/pages/exams_scores/callbacks.py index 5cc8b30..ea0da49 100644 --- a/recopytex/dashboard/pages/exams_scores/callbacks.py +++ b/recopytex/dashboard/pages/exams_scores/callbacks.py @@ -17,6 +17,7 @@ from .models import ( get_unstack_scores, get_students_from_exam, get_score_colors, + score_to_final_mark, ) @@ -77,3 +78,17 @@ def update_scores_store(exam): highlight_scores(students, score_color), {"headers": True, "data": len(fixed_columns)}, ] + + +@app.callback( + [ + Output("final_score_table", "data"), + ], + [ + Input("scores_table", "data"), + ], +) +def update_scores_store(scores): + scores_df = pd.DataFrame.from_records(scores) + # print(scores_df) + return score_to_final_mark(scores_df) diff --git a/recopytex/dashboard/pages/exams_scores/models.py b/recopytex/dashboard/pages/exams_scores/models.py index da3d446..ddc051a 100644 --- a/recopytex/dashboard/pages/exams_scores/models.py +++ b/recopytex/dashboard/pages/exams_scores/models.py @@ -1,10 +1,44 @@ -#!/usr/bin/env python +#!/use/bin/env python # encoding: utf-8 from recopytex.database.filesystem.loader import CSVLoader from recopytex.datalib.dataframe import column_values_to_column +import recopytex.datalib.on_score_column as on_column +import pandas as pd -LOADER = CSVLoader("./test_config.yml") +LOADER = CSVLoader("./test_confia.ml") +SCORES_CONFIG = LOADER.get_config()["scores"] + + +def unstack_scores(scores): + """Put student_name values to columns + + :param scores: Score dataframe with one line per score + :returns: Scrore dataframe with student_name in columns + + """ + kept_columns = [col for col in LOADER.score_columns if col != "score"] + return column_values_to_column("student_name", "score", kept_columns, scores) + + +def stack_scores(scores): + """Student columns are melt to rows with student_name column + + :param scores: Score dataframe with student_name in columns + :returns: Scrore dataframe with one line per score + + """ + kept_columns = [ + c for c in LOADER.score_columns if c not in ["score", "student_name"] + ] + student_names = [c for c in scores.columns if c not in kept_columns] + return pd.melt( + scores, + id_vars=kept_columns, + value_vars=student_names, + var_name="student_name", + value_name="score", + ) def get_tribes(): @@ -21,8 +55,7 @@ def get_record_scores(exam): def get_unstack_scores(exam): flat_scores = LOADER.get_exam_scores(exam) - kept_columns = [col for col in LOADER.score_columns if col != "score"] - return column_values_to_column("student_name", "score", kept_columns, flat_scores) + return unstack_scores(flat_scores) def get_students_from_exam(exam): @@ -31,8 +64,32 @@ def get_students_from_exam(exam): def get_score_colors(): - scores_config = LOADER.get_config()["valid_scores"] score_color = {} - for key, score in scores_config.items(): + for key, score in SCORES_CONFIG.items(): score_color[score["value"]] = score["color"] return score_color + + +is_none_score = lambda x: on_column.is_none_score(x, SCORES_CONFIG) +score_to_numeric_score = lambda x: on_column.score_to_numeric_score(x, SCORES_CONFIG) +score_to_mark = lambda x: on_column.score_to_mark( + x, max([v["value"] for v in SCORES_CONFIG.values() if isinstance(v["value"], int)]) +) + + +def score_to_final_mark(scores): + """ Compute marks then reduce to final mark per student """ + + melted_scores = stack_scores(scores) + filtered_scores = melted_scores[~melted_scores.apply(is_none_score, axis=1)] + filtered_scores = filtered_scores.assign( + score=filtered_scores.apply(score_to_numeric_score, axis=1) + ) + filtered_scores = filtered_scores.assign( + mark=filtered_scores.apply(score_to_mark, axis=1) + ) + final_score = filtered_scores.groupby(["student_name"])[ + ["mark", "score_rate"] + ].sum() + return [final_score.reset_index().to_dict("records")] + diff --git a/recopytex/datalib/on_score_column.py b/recopytex/datalib/on_score_column.py index 4592405..6cedb84 100644 --- a/recopytex/datalib/on_score_column.py +++ b/recopytex/datalib/on_score_column.py @@ -2,6 +2,7 @@ # encoding: utf-8 from math import ceil +import numpy as np def is_none_score(x, score_config): @@ -39,7 +40,7 @@ def is_none_score(x, score_config): for v in score_config.values() if str(v["numeric_value"]).lower() == "none" ] - return x["score"] in none_values + return x["score"] in none_values or x["score"] is None or np.isnan(x["score"]) def score_to_numeric_score(x, score_config):