Compare commits
No commits in common. "235019102b48f509c26141ad21d17c0726db2333" and "2e86b3a0a20aeaa182ec79930bfd04e4116d9bb9" have entirely different histories.
235019102b
...
2e86b3a0a2
@ -43,16 +43,7 @@ layout = html.Div(
|
|||||||
html.Section(
|
html.Section(
|
||||||
children=[
|
children=[
|
||||||
html.Div(
|
html.Div(
|
||||||
children=[
|
children=[],
|
||||||
dash_table.DataTable(
|
|
||||||
id="final_score_table",
|
|
||||||
columns=[
|
|
||||||
{"name": "Étudiant", "id": "student_name"},
|
|
||||||
{"name": "Note", "id": "mark"},
|
|
||||||
{"name": "Barème", "id": "score_rate"},
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
id="final_score_table_container",
|
id="final_score_table_container",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -74,4 +65,4 @@ layout = html.Div(
|
|||||||
),
|
),
|
||||||
dcc.Store(id="scores"),
|
dcc.Store(id="scores"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -17,7 +17,6 @@ from .models import (
|
|||||||
get_unstack_scores,
|
get_unstack_scores,
|
||||||
get_students_from_exam,
|
get_students_from_exam,
|
||||||
get_score_colors,
|
get_score_colors,
|
||||||
score_to_final_mark,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -78,17 +77,3 @@ def update_scores_store(exam):
|
|||||||
highlight_scores(students, score_color),
|
highlight_scores(students, score_color),
|
||||||
{"headers": True, "data": len(fixed_columns)},
|
{"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)
|
|
||||||
|
@ -1,44 +1,10 @@
|
|||||||
#!/use/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from recopytex.database.filesystem.loader import CSVLoader
|
from recopytex.database.filesystem.loader import CSVLoader
|
||||||
from recopytex.datalib.dataframe import column_values_to_column
|
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_confia.ml")
|
LOADER = CSVLoader("./test_config.yml")
|
||||||
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():
|
def get_tribes():
|
||||||
@ -55,7 +21,8 @@ def get_record_scores(exam):
|
|||||||
|
|
||||||
def get_unstack_scores(exam):
|
def get_unstack_scores(exam):
|
||||||
flat_scores = LOADER.get_exam_scores(exam)
|
flat_scores = LOADER.get_exam_scores(exam)
|
||||||
return unstack_scores(flat_scores)
|
kept_columns = [col for col in LOADER.score_columns if col != "score"]
|
||||||
|
return column_values_to_column("student_name", "score", kept_columns, flat_scores)
|
||||||
|
|
||||||
|
|
||||||
def get_students_from_exam(exam):
|
def get_students_from_exam(exam):
|
||||||
@ -64,32 +31,8 @@ def get_students_from_exam(exam):
|
|||||||
|
|
||||||
|
|
||||||
def get_score_colors():
|
def get_score_colors():
|
||||||
|
scores_config = LOADER.get_config()["valid_scores"]
|
||||||
score_color = {}
|
score_color = {}
|
||||||
for key, score in SCORES_CONFIG.items():
|
for key, score in scores_config.items():
|
||||||
score_color[score["value"]] = score["color"]
|
score_color[score["value"]] = score["color"]
|
||||||
return 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")]
|
|
||||||
|
|
||||||
|
@ -2,82 +2,6 @@
|
|||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from math import ceil
|
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)):
|
def score_to_mark(x, score_max, rounding=lambda x: round(x, 2)):
|
||||||
@ -201,6 +125,10 @@ def score_to_level(x, level_max=3):
|
|||||||
return int(ceil(x["score"] / x["score_rate"] * level_max))
|
return int(ceil(x["score"] / x["score_rate"] * level_max))
|
||||||
|
|
||||||
|
|
||||||
|
def score_to_numeric_score(x, score_config):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Reglages pour 'vim'
|
# Reglages pour 'vim'
|
||||||
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:
|
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:
|
||||||
|
196
recopytex/datalib/on_score_dataframe.py
Normal file
196
recopytex/datalib/on_score_dataframe.py
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
#!/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
|
Loading…
Reference in New Issue
Block a user