Compare commits

13 Commits

9 changed files with 299 additions and 48 deletions

View File

@@ -1,5 +1,5 @@
Trimestre,Nom,Date,Exercice,Question,Competence,Domaine,Commentaire,Bareme,Est_nivele,Star Tice,Umberto Dingate,Starlin Crangle,Humbert Bourcq,Gabriella Handyside,Stewart Eaves,Erick Going,Ase Praton,Rollins Planks,Dunstan Sarjant,Stacy Guiton,Ange Stanes,Amabelle Elleton,Darn Broomhall,Dyan Chatto,Keane Rennebach,Nari Paulton,Brandy Wase,Jaclyn Firidolfi,Violette Lockney
1,DS,12/01/2021,Exercice 1,1,Calculer,Plop,Coucou,1,1,,,1.0,0,1.0,2.0,3.0,0.0,3.0,3.0,2.0,,1.0,,,,,,,
1,DS,12/01/2021,Exercice 1,2,Calculer,C'est trop chouette!,Coucou,1,1,,,1.0,2,,,3.0,3.0,,,,,2.0,,,,,,,
1,DS,12/01/2021,Exercice 1,3,Calculer,Null,Coucou,1,1,,,,3,2.0,,,,,,,,3.0,,,,,,,
1,DS,12/01/2021,Exercice 1,3,Calculer,Nié,DChic,1,1,,,,2,,,,,,,,,,,,,,,,
1,DS,12/01/2021,Exercice 1,1,Calculer,Plop,Coucou,1,1,,,1,0,1,2,3,0,3,3,2,,1,,,,,,,
1,DS,12/01/2021,Exercice 1,2,Calculer,C'est trop chouette!,Coucou,1,1,,,1,2,,,3,3,,,,,2,,,,,,,
1,DS,12/01/2021,Exercice 1,3,Calculer,Null,Coucou,1,1,,,,3,2,,,,,,,,3,,,,,,,
1,DS,12/01/2021,Exercice 1,3,Calculer,Nié,DChic,1,1,,,,2,.,,,,,,,,,,,,,,,
1 Trimestre Nom Date Exercice Question Competence Domaine Commentaire Bareme Est_nivele Star Tice Umberto Dingate Starlin Crangle Humbert Bourcq Gabriella Handyside Stewart Eaves Erick Going Ase Praton Rollins Planks Dunstan Sarjant Stacy Guiton Ange Stanes Amabelle Elleton Darn Broomhall Dyan Chatto Keane Rennebach Nari Paulton Brandy Wase Jaclyn Firidolfi Violette Lockney
2 1 DS 12/01/2021 Exercice 1 1 Calculer Plop Coucou 1 1 1.0 1 0 1.0 1 2.0 2 3.0 3 0.0 0 3.0 3 3.0 3 2.0 2 1.0 1
3 1 DS 12/01/2021 Exercice 1 2 Calculer C'est trop chouette! Coucou 1 1 1.0 1 2 3.0 3 3.0 3 2.0 2
4 1 DS 12/01/2021 Exercice 1 3 Calculer Null Coucou 1 1 3 2.0 2 3.0 3
5 1 DS 12/01/2021 Exercice 1 3 Calculer Nié DChic 1 1 2 .

View File

@@ -55,6 +55,33 @@ layout = html.Div(
],
id="final_score_table_container",
),
html.Div(
children=[
dash_table.DataTable(
id="score_statistics_table",
columns=[],
)
],
id="score_statistics_table_container",
),
html.Div(
children=[
dcc.Graph(
id="fig_exam_histo",
config={"displayModeBar": False},
)
],
id="fig_exam_histo_container",
),
html.Div(
children=[
dcc.Graph(
id="fig_questions_bar",
config={"displayModeBar": False},
)
],
id="fig_questions_bar_container",
),
],
id="analysis",
),
@@ -66,6 +93,14 @@ layout = html.Div(
style_data_conditional=[],
fixed_columns={},
editable=True,
style_table={"minWidth": "100%"},
style_cell={
"minWidth": "100px",
"width": "100px",
"maxWidth": "100px",
"overflow": "hidden",
"textOverflow": "ellipsis",
},
)
],
id="edit",

View File

@@ -2,11 +2,12 @@
# encoding: utf-8
from dash.dependencies import Input, Output, State
import dash
from dash.exceptions import PreventUpdate
import plotly.graph_objects as go
import dash_table
import json
import pandas as pd
import numpy as np
from recopytex.dashboard.app import app
from recopytex.dashboard.common.formating import highlight_scores
@@ -17,7 +18,10 @@ from .models import (
get_unstack_scores,
get_students_from_exam,
get_score_colors,
get_level_color_bar,
score_to_final_mark,
stack_scores,
pivot_score_on,
)
@@ -52,7 +56,6 @@ def update_exams_choices(tribe):
],
)
def update_scores_store(exam):
ctx = dash.callback_context
if not exam:
return [[], [], [], {}]
exam = pd.DataFrame.from_dict([json.loads(exam)])
@@ -88,7 +91,126 @@ def update_scores_store(exam):
Input("scores_table", "data"),
],
)
def update_scores_store(scores):
def update_finale_score_table(scores):
scores_df = pd.DataFrame.from_records(scores)
# print(scores_df)
return score_to_final_mark(scores_df)
stacked_scores = stack_scores(scores_df)
return score_to_final_mark(stacked_scores)
@app.callback(
[
Output("score_statistics_table", "columns"),
Output("score_statistics_table", "data"),
],
[
Input("final_score_table", "data"),
],
)
def update_statictics_table(finale_score):
df = pd.DataFrame.from_records(finale_score)
statistics = df["mark"].describe().to_frame().T
return [
[{"id": c, "name": c} for c in statistics.columns],
statistics.to_dict("records"),
]
@app.callback(
[
Output("fig_exam_histo", "figure"),
],
[
Input("final_score_table", "data"),
],
)
def update_exam_histo(finale_scores):
scores = pd.DataFrame.from_records(finale_scores)
if scores.empty:
return [go.Figure(data=[go.Scatter(x=[], y=[])])]
ranges = np.linspace(
-0.5,
scores["score_rate"].max(),
int(scores["score_rate"].max() * 2 + 2),
)
bins = pd.cut(scores["mark"], ranges)
scores["Bin"] = bins
grouped = (
scores.reset_index()
.groupby("Bin")
.agg({"score_rate": "count", "student_name": lambda x: "\n".join(x)})
)
grouped.index = grouped.index.map(lambda i: i.right)
fig = go.Figure()
fig.add_bar(
x=grouped.index,
y=grouped["score_rate"],
text=grouped["student_name"],
textposition="auto",
hovertemplate="",
marker_color="#4E89DE",
)
fig.update_layout(
height=300,
margin=dict(l=5, r=5, b=5, t=5),
)
return [fig]
@app.callback(
[
Output("fig_questions_bar", "figure"),
],
[
Input("scores_table", "data"),
],
)
def update_questions_bar(finale_scores):
scores = pd.DataFrame.from_records(finale_scores)
scores = stack_scores(scores)
if scores.empty:
return [go.Figure(data=[go.Scatter(x=[], y=[])])]
pt = pivot_score_on(scores, ["exercise", "question", "comment"], "score")
# separation between exercises
for i in {i for i in pt.index.get_level_values(0)}:
pt.loc[(str(i), "", ""), :] = ""
pt.sort_index(inplace=True)
# Bar label
index = (
pt.index.get_level_values(0).map(str)
+ ":"
+ pt.index.get_level_values(1).map(str)
+ " "
+ pt.index.get_level_values(2).map(str)
)
fig = go.Figure()
bars = get_level_color_bar()
for b in bars:
try:
fig.add_bar(
x=index, y=pt[b["score"]], name=b["name"], marker_color=b["color"]
)
except KeyError:
pass
fig.update_layout(barmode="relative")
fig.update_layout(
height=500,
margin=dict(l=5, r=5, b=5, t=5),
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
)
)
return [fig]

View File

@@ -70,18 +70,33 @@ def get_score_colors():
return score_color
def get_level_color_bar():
return [
{"score": str(s["value"]), "name": s["comment"], "color": s["color"]}
for s in SCORES_CONFIG.values()
]
is_none_score = lambda x: on_column.is_none_score(x, SCORES_CONFIG)
format_score = lambda x: on_column.format_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 filter_clean_score(scores):
filtered_scores = scores[~scores.apply(is_none_score, axis=1)]
filtered_scores = filtered_scores.assign(
score=filtered_scores.apply(format_score, axis=1)
)
return filtered_scores
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 = filter_clean_score(scores)
filtered_scores = filtered_scores.assign(
score=filtered_scores.apply(score_to_numeric_score, axis=1)
)
@@ -93,3 +108,21 @@ def score_to_final_mark(scores):
].sum()
return [final_score.reset_index().to_dict("records")]
def pivot_score_on(scores, index, columns, aggfunc="size"):
"""Pivot scores on index, columns with aggfunc
It assumes thant scores are levels
"""
filtered_scores = filter_clean_score(scores)
filtered_scores["score"] = filtered_scores["score"].astype(str)
pt = pd.pivot_table(
filtered_scores,
index=index,
columns=columns,
aggfunc=aggfunc,
fill_value=0,
)
return pt

View File

@@ -6,6 +6,7 @@ from dash.dependencies import Input, Output
from .app import app
from .pages.home import app as home
from .pages.exams_scores import app as exams_scores
import dash_html_components as html
@app.callback(Output("page-content", "children"), [Input("url", "pathname")])

View File

@@ -26,30 +26,37 @@ scores: #
value: 0
numeric_value: 0
color: "#E7472B"
comment: Faux
FEW: # Few good things
value: 1
numeric_value: 1
color: "#FF712B"
comment: Peu juste
NEARLY: # Nearly good but things are missing
value: 2
numeric_value: 2
color: "#F2EC4C"
comment: Presque juste
GOOD: # Everything is good
value: 3
numeric_value: 3
color: "#68D42F"
comment: Juste
NOTFILLED: # The item is not scored yet
value: ""
numeric_value: None
color: white
comment: En attente
NOANSWER: # Student gives no answer (count as 0)
value: "."
numeric_value: 0
color: black
comment: Pas de réponse
ABS: # Student has absent (this score won't be impact the final mark)
value: a
numeric_value: None
color: lightgray
comment: Non noté
csv_fields: # dataframe_field: csv_field
term: Trimestre

View File

@@ -31,10 +31,11 @@ class CSVLoader(Loader):
:example:
>>> loader = CSVLoader()
>>> loader.get_config()
{'source': './', 'competences': {'Chercher': {'name': 'Chercher', 'abrv': 'Cher'}, 'Représenter': {'name': 'Représenter', 'abrv': 'Rep'}, 'Modéliser': {'name': 'Modéliser', 'abrv': 'Mod'}, 'Raisonner': {'name': 'Raisonner', 'abrv': 'Rai'}, 'Calculer': {'name': 'Calculer', 'abrv': 'Cal'}, 'Communiquer': {'name': 'Communiquer', 'abrv': 'Com'}}, 'scores': {'BAD': {'value': 0, 'numeric_value': 0, 'color': '#E7472B'}, 'FEW': {'value': 1, 'numeric_value': 1, 'color': '#FF712B'}, 'NEARLY': {'value': 2, 'numeric_value': 2, 'color': '#F2EC4C'}, 'GOOD': {'value': 3, 'numeric_value': 3, 'color': '#68D42F'}, 'NOTFILLED': {'value': '', 'numeric_value': 'None', 'color': 'white'}, 'NOANSWER': {'value': '.', 'numeric_value': 0, 'color': 'black'}, 'ABS': {'value': 'a', 'numeric_value': 'None', 'color': 'lightgray'}}, 'csv_fields': {'term': 'Trimestre', 'exam': 'Nom', 'date': 'Date', 'exercise': 'Exercice', 'question': 'Question', 'competence': 'Competence', 'theme': 'Domaine', 'comment': 'Commentaire', 'score_rate': 'Bareme', 'is_leveled': 'Est_nivele'}, 'id_templates': {'exam': '{name}_{tribe}', 'question': '{exam_id}_{exercise}_{question}_{comment}'}}
{'source': './', 'competences': {'Chercher': {'name': 'Chercher', 'abrv': 'Cher'}, 'Représenter': {'name': 'Représenter', 'abrv': 'Rep'}, 'Modéliser': {'name': 'Modéliser', 'abrv': 'Mod'}, 'Raisonner': {'name': 'Raisonner', 'abrv': 'Rai'}, 'Calculer': {'name': 'Calculer', 'abrv': 'Cal'}, 'Communiquer': {'name': 'Communiquer', 'abrv': 'Com'}}, 'scores': {'BAD': {'value': 0, 'numeric_value': 0, 'color': '#E7472B', 'comment': 'Faux'}, 'FEW': {'value': 1, 'numeric_value': 1, 'color': '#FF712B', 'comment': 'Peu juste'}, 'NEARLY': {'value': 2, 'numeric_value': 2, 'color': '#F2EC4C', 'comment': 'Presque juste'}, 'GOOD': {'value': 3, 'numeric_value': 3, 'color': '#68D42F', 'comment': 'Juste'}, 'NOTFILLED': {'value': '', 'numeric_value': 'None', 'color': 'white', 'comment': 'En attente'}, 'NOANSWER': {'value': '.', 'numeric_value': 0, 'color': 'black', 'comment': 'Pas de réponse'}, 'ABS': {'value': 'a', 'numeric_value': 'None', 'color': 'lightgray', 'comment': 'Non noté'}}, 'csv_fields': {'term': 'Trimestre', 'exam': 'Nom', 'date': 'Date', 'exercise': 'Exercice', 'question': 'Question', 'competence': 'Competence', 'theme': 'Domaine', 'comment': 'Commentaire', 'score_rate': 'Bareme', 'is_leveled': 'Est_nivele'}, 'id_templates': {'exam': '{name}_{tribe}', 'question': '{exam_id}_{exercise}_{question}_{comment}'}}
>>> loader = CSVLoader("./test_config.yml")
>>> loader.get_config()
{'source': './example', 'competences': {'Chercher': {'name': 'Chercher', 'abrv': 'Cher'}, 'Représenter': {'name': 'Représenter', 'abrv': 'Rep'}, 'Modéliser': {'name': 'Modéliser', 'abrv': 'Mod'}, 'Raisonner': {'name': 'Raisonner', 'abrv': 'Rai'}, 'Calculer': {'name': 'Calculer', 'abrv': 'Cal'}, 'Communiquer': {'name': 'Communiquer', 'abrv': 'Com'}}, 'scores': {'BAD': {'value': 0, 'numeric_value': 0, 'color': '#E7472B'}, 'FEW': {'value': 1, 'numeric_value': 1, 'color': '#FF712B'}, 'NEARLY': {'value': 2, 'numeric_value': 2, 'color': '#F2EC4C'}, 'GOOD': {'value': 3, 'numeric_value': 3, 'color': '#68D42F'}, 'NOTFILLED': {'value': '', 'numeric_value': 'None', 'color': 'white'}, 'NOANSWER': {'value': '.', 'numeric_value': 0, 'color': 'black'}, 'ABS': {'value': 'a', 'numeric_value': 'None', 'color': 'lightgray'}}, 'csv_fields': {'term': 'Trimestre', 'exam': 'Nom', 'date': 'Date', 'exercise': 'Exercice', 'question': 'Question', 'competence': 'Competence', 'theme': 'Domaine', 'comment': 'Commentaire', 'score_rate': 'Bareme', 'is_leveled': 'Est_nivele'}, 'id_templates': {'exam': '{name}_{tribe}', 'question': '{exam_id}_{exercise}_{question}_{comment}'}, 'output': './output', 'templates': 'templates/', 'tribes': {'Tribe1': {'name': 'Tribe1', 'type': 'Type1', 'students': 'tribe1.csv'}, 'Tribe2': {'name': 'Tribe2', 'students': 'tribe2.csv'}}}
{'source': './example', 'competences': {'Chercher': {'name': 'Chercher', 'abrv': 'Cher'}, 'Représenter': {'name': 'Représenter', 'abrv': 'Rep'}, 'Modéliser': {'name': 'Modéliser', 'abrv': 'Mod'}, 'Raisonner': {'name': 'Raisonner', 'abrv': 'Rai'}, 'Calculer': {'name': 'Calculer', 'abrv': 'Cal'}, 'Communiquer': {'name': 'Communiquer', 'abrv': 'Com'}}, 'scores': {'BAD': {'value': 0, 'numeric_value': 0, 'color': '#E7472B', 'comment': 'Faux'}, 'FEW': {'value': 1, 'numeric_value': 1, 'color': '#FF712B', 'comment': 'Peu juste'}, 'NEARLY': {'value': 2, 'numeric_value': 2, 'color': '#F2EC4C', 'comment': 'Presque juste'}, 'GOOD': {'value': 3, 'numeric_value': 3, 'color': '#68D42F', 'comment': 'Juste'}, 'NOTFILLED': {'value': '', 'numeric_value': 'None', 'color': 'white', 'comment': 'En attente'}, 'NOANSWER': {'value': '.', 'numeric_value': 0, 'color': 'black', 'comment': 'Pas de réponse'}, 'ABS': {'value': 'a', 'numeric_value': 'None', 'color': 'lightgray', 'comment': 'Non noté'}}, 'csv_fields': {'term': 'Trimestre', 'exam': 'Nom', 'date': 'Date', 'exercise': 'Exercice', 'question': 'Question', 'competence': 'Competence', 'theme': 'Domaine', 'comment': 'Commentaire', 'score_rate': 'Bareme', 'is_leveled': 'Est_nivele'}, 'id_templates': {'exam': '{name}_{tribe}', 'question': '{exam_id}_{exercise}_{question}_{comment}'}, 'output': './output', 'templates': 'templates/', 'tribes': {'Tribe1': {'name': 'Tribe1', 'type': 'Type1', 'students': 'tribe1.csv'}, 'Tribe2': {'name': 'Tribe2', 'students': 'tribe2.csv'}}}
"""
CONFIG = DEFAULT_CONFIG

View File

@@ -2,7 +2,7 @@
# encoding: utf-8
from math import ceil
import numpy as np
import pandas as pd
def is_none_score(x, score_config):
@@ -40,7 +40,58 @@ 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 or x["score"] is None or np.isnan(x["score"])
return x["score"] in none_values or pd.isnull(x["score"])
def format_score(x, score_config):
"""Make sure that score have the appropriate format
>>> import pandas as pd
>>> d = {"Eleve":["E1"]*6,
... "score_rate": [1]*6,
... "is_leveled":[0]+[1]*5,
... "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:format_score(x, score_config), axis=1)
0 0.33
1 .
2 a
3 1
4 2
5 3
dtype: object
>>> format_score({"score": "1.0", "is_leveled": 1}, score_config)
1
>>> format_score({"score": "3.0", "is_leveled": 1}, score_config)
3
>>> format_score({"score": 4, "is_leveled": 1}, score_config)
Traceback (most recent call last):
...
ValueError: 4 (<class 'int'>) can't be a score
"""
if not x["is_leveled"]:
return float(x["score"])
try:
score = int(float(x["score"]))
except ValueError:
score = str(x["score"])
if score in [v["value"] for v in score_config.values()]:
return score
raise ValueError(f"{x['score']} ({type(x['score'])}) can't be a score")
def score_to_numeric_score(x, score_config):
@@ -81,7 +132,7 @@ def score_to_numeric_score(x, score_config):
def score_to_mark(x, score_max, rounding=lambda x: round(x, 2)):
"""Compute the mark from the score
"""Compute the mark from "score" which have to be filtered and in numeric form
if the item is leveled then the score is multiply by the score_rate
otherwise it copies the score
@@ -92,39 +143,38 @@ def score_to_mark(x, score_max, rounding=lambda x: round(x, 2)):
:return: the 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":[1, 0.33, 2, 1.5, 1, 3, 0.666, 1, 1.5, 1.2, 2, 3],
>>> 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.loc[0]
Eleve E1
score_rate 1
is_leveled 0
score 1.0
Name: 0, dtype: object
>>> score_to_mark(df.loc[0], 3)
1.0
>>> df.loc[10]
Eleve E2
score_rate 2
is_leveled 1
score 2.0
Name: 10, dtype: object
>>> score_to_mark(df.loc[10], 3)
1.33
>>> df = df[~df.apply(lambda x:is_none_score(x, score_config), axis=1)]
>>> df["score"] = df.apply(lambda x:score_to_numeric_score(x, score_config), axis=1)
>>> df.apply(lambda x:score_to_mark(x, 3), axis=1)
0 0.33
2 0.00
4 0.33
5 0.67
6 1.00
dtype: float64
>>> from .on_value import round_half_point
>>> score_to_mark(df.loc[10], 3, round_half_point)
1.5
>>> df.loc[1]
Eleve E1
score_rate 1
is_leveled 0
score 0.33
Name: 1, dtype: object
>>> score_to_mark(df.loc[1], 3)
0.33
>>> df.apply(lambda x:score_to_mark(x, 3, round_half_point), axis=1)
0 0.5
2 0.0
4 0.5
5 0.5
6 1.0
dtype: float64
"""
if x["is_leveled"]:
if x["score"] not in list(range(score_max + 1)):

View File

@@ -2,8 +2,7 @@
# encoding: utf-8
import click
from recopytex.dashboard.app import app as dash
from recopytex.dashboard.index import app as dash
@click.group()
def cli():
@@ -14,3 +13,6 @@ def cli():
@click.option("--debug", default=0, help="Debug mode for dash")
def dashboard(debug):
dash.run_server(debug=bool(debug))
if __name__ == "__main__":
cli()