Compare commits

...

2 Commits

5 changed files with 213 additions and 256 deletions

View File

@ -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"),
],
)

View File

@ -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)

View File

@ -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")]

View File

@ -2,6 +2,82 @@
# encoding: utf-8
from math import ceil
import numpy as np
def is_none_score(x, score_config):
"""Is a score correspond to a None numeric_value which
>>> import pandas as pd
>>> d = {"Eleve":["E1"]*7,
... "score_rate": [1]*7,
... "is_leveled":[0]+[1]*6,
... "score":[0.33, "", ".", "a", 1, 2, 3],
... }
>>> score_config = {
... 'BAD': {'value': 0, 'numeric_value': 0},
... 'FEW': {'value': 1, 'numeric_value': 1},
... 'NEARLY': {'value': 2, 'numeric_value': 2},
... 'GOOD': {'value': 3, 'numeric_value': 3},
... 'NOTFILLED': {'value': '', 'numeric_value': 'None'},
... 'NOANSWER': {'value': '.', 'numeric_value': 0},
... 'ABS': {'value': 'a', 'numeric_value': 'None'}
... }
>>> df = pd.DataFrame(d)
>>> df.apply(lambda x:is_none_score(x, score_config), axis=1)
0 False
1 True
2 False
3 True
4 False
5 False
6 False
dtype: bool
"""
none_values = [
v["value"]
for v in score_config.values()
if str(v["numeric_value"]).lower() == "none"
]
return x["score"] in none_values or x["score"] is None or np.isnan(x["score"])
def score_to_numeric_score(x, score_config):
"""Convert a score to the corresponding numeric value
>>> import pandas as pd
>>> d = {"Eleve":["E1"]*7,
... "score_rate": [1]*7,
... "is_leveled":[0]+[1]*6,
... "score":[0.33, "", ".", "a", 1, 2, 3],
... }
>>> score_config = {
... 'BAD': {'value': 0, 'numeric_value': 0},
... 'FEW': {'value': 1, 'numeric_value': 1},
... 'NEARLY': {'value': 2, 'numeric_value': 2},
... 'GOOD': {'value': 3, 'numeric_value': 3},
... 'NOTFILLED': {'value': '', 'numeric_value': 'None'},
... 'NOANSWER': {'value': '.', 'numeric_value': 0},
... 'ABS': {'value': 'a', 'numeric_value': 'None'}
... }
>>> df = pd.DataFrame(d)
>>> df.apply(lambda x:score_to_numeric_score(x, score_config), axis=1)
0 0.33
1 None
2 0
3 None
4 1
5 2
6 3
dtype: object
"""
if x["is_leveled"]:
replacements = {v["value"]: v["numeric_value"] for v in score_config.values()}
return replacements[x["score"]]
return x["score"]
def score_to_mark(x, score_max, rounding=lambda x: round(x, 2)):
@ -125,10 +201,6 @@ def score_to_level(x, level_max=3):
return int(ceil(x["score"] / x["score_rate"] * level_max))
def score_to_numeric_score(x, score_config):
pass
# -----------------------------
# Reglages pour 'vim'
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:

View File

@ -1,196 +0,0 @@
#!/usr/bin/env python
# encoding: utf-8
from .on_score_column import score_to_mark, score_to_level
import pandas as pd
def compute_marks(df, score_max, rounding=lambda x: round(x, 2)):
"""Compute the mark for the dataframe
apply score_to_mark to each row
:param df: DataFrame with "score" (need to be number), "is_leveled" and "score_rate" columns.
>>> import pandas as pd
>>> d = {"Eleve":["E1"]*6 + ["E2"]*6,
... "score_rate":[1]*2+[2]*2+[2]*2 + [1]*2+[2]*2+[2]*2,
... "is_leveled":[0]*4+[1]*2 + [0]*4+[1]*2,
... "score":[1, 0.33, 2, 1.5, 1, 3, 0.666, 1, 1.5, 1, 2, 3],
... }
>>> df = pd.DataFrame(d)
>>> df
Eleve score_rate is_leveled score
0 E1 1 0 1.000
1 E1 1 0 0.330
2 E1 2 0 2.000
3 E1 2 0 1.500
4 E1 2 1 1.000
5 E1 2 1 3.000
6 E2 1 0 0.666
7 E2 1 0 1.000
8 E2 2 0 1.500
9 E2 2 0 1.000
10 E2 2 1 2.000
11 E2 2 1 3.000
>>> compute_marks(df, 3)
0 1.00
1 0.33
2 2.00
3 1.50
4 0.67
5 2.00
6 0.67
7 1.00
8 1.50
9 1.00
10 1.33
11 2.00
dtype: float64
>>> from .on_value import round_half_point
>>> compute_marks(df, 3, round_half_point)
0 1.0
1 0.5
2 2.0
3 1.5
4 0.5
5 2.0
6 0.5
7 1.0
8 1.5
9 1.0
10 1.5
11 2.0
dtype: float64
"""
return df[["score", "is_leveled", "score_rate"]].apply(
lambda x: score_to_mark(x, score_max, rounding), axis=1
)
def compute_level(df, level_max=3):
"""Compute level for the dataframe
Applies score_to_level to each row
:param df: DataFrame with "score", "is_leveled" and "score_rate" columns.
:return: Columns with level
>>> import pandas as pd
>>> import numpy as np
>>> d = {"Eleve":["E1"]*6 + ["E2"]*6,
... "score_rate":[1]*2+[2]*2+[2]*2 + [1]*2+[2]*2+[2]*2,
... "is_leveled":[0]*4+[1]*2 + [0]*4+[1]*2,
... "score":[0, 0.33, 2, 1.5, 1, 3, 0.666, 1, 1.5, 1, 2, 3],
... }
>>> df = pd.DataFrame(d)
>>> compute_level(df)
0 0
1 1
2 3
3 3
4 1
5 3
6 2
7 3
8 3
9 2
10 2
11 3
dtype: int64
"""
return df[["score", "is_leveled", "score_rate"]].apply(
lambda x: score_to_level(x, level_max), axis=1
)
def compute_normalized(df, rounding=lambda x: round(x, 2)):
"""Compute the normalized mark (Mark / score_rate)
:param df: DataFrame with "Mark" and "score_rate" columns
:return: column with normalized mark
>>> import pandas as pd
>>> d = {"Eleve":["E1"]*6 + ["E2"]*6,
... "score_rate":[1]*2+[2]*2+[2]*2 + [1]*2+[2]*2+[2]*2,
... "is_leveled":[0]*4+[1]*2 + [0]*4+[1]*2,
... "score":[0, 0.33, 2, 1.5, 1, 3, 0.666, 1, 1.5, 1, 2, 3],
... }
>>> df = pd.DataFrame(d)
>>> df["mark"] = compute_marks(df, 3)
>>> compute_normalized(df)
0 0.00
1 0.33
2 1.00
3 0.75
4 0.34
5 1.00
6 0.67
7 1.00
8 0.75
9 0.50
10 0.66
11 1.00
dtype: float64
"""
return rounding(df["mark"] / df["score_rate"])
def filter_none_score(df, score_config):
"""Filter rows where scores have None numeric values
:example:
>>> import pandas as pd
>>> d = {"Eleve":["E1"]*7,
... "score_rate": [1]*7,
... "is_leveled":[0]+[1]*6,
... "score":[0.33, "", ".", "a", 1, 2, 3],
... }
>>> score_config = {
... 'BAD': {'value': 0, 'numeric_value': 0},
... 'FEW': {'value': 1, 'numeric_value': 1},
... 'NEARLY': {'value': 2, 'numeric_value': 2},
... 'GOOD': {'value': 3, 'numeric_value': 3},
... 'NOTFILLED': {'value': '', 'numeric_value': 'None'},
... 'NOANSWER': {'value': '.', 'numeric_value': 0},
... 'ABS': {'value': 'a', 'numeric_value': 'None'}
... }
>>> df = pd.DataFrame(d)
>>> filter_none_score(df, score_config)
Eleve score_rate is_leveled score
0 E1 1 0 0.33
2 E1 1 1 .
4 E1 1 1 1
5 E1 1 1 2
6 E1 1 1 3
"""
not_leveled_df = df[df["is_leveled"] != 1]
leveled_df = df[df["is_leveled"] == 1]
not_none_values = [
v["value"]
for v in score_config.values()
if str(v["numeric_value"]).lower() != "none"
]
filtered_leveled_df = leveled_df[leveled_df["score"].isin(not_none_values)]
return pd.concat([not_leveled_df, filtered_leveled_df])
def score_to_numeric_score(df, score_config):
"""Transform a score to the corresponding numeric value
>>> d = {"Eleve":["E1"]*6 + ["E2"]*6,
... "score_rate":[1]*2+[2]*2+[2]*2 + [1]*2+[2]*2+[2]*2,
... "is_leveled":[0]*4+[1]*2 + [0]*4+[1]*2,
... "score":[0, 0.33, 2, 1.5, 1, 3, 0.666, 1, 1.5, 1, 2, 3],
... }
"""
pass
# -----------------------------
# Reglages pour 'vim'
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:
# cursor: 16 del