Compare commits
24 Commits
9e0ea14d05
...
refact
Author | SHA1 | Date | |
---|---|---|---|
83eb9c327b | |||
ff1ecfef25 | |||
921292a0d2 | |||
2d08671247 | |||
a16211cbd4 | |||
876f583d51 | |||
97b97af2de | |||
d8d84690c6 | |||
18f855ab83 | |||
36425e587e | |||
8cdeecfc53 | |||
1a7c97d869 | |||
ab5de2711e | |||
235019102b | |||
8ec24a24b3 | |||
2e86b3a0a2 | |||
7e6b24aaea | |||
6889ddd97c | |||
10b9954c05 | |||
7553628306 | |||
effc049578 | |||
411f910ce6 | |||
00d81d694a | |||
a8b2ac455d |
@@ -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,.,,,,,,,,,,,,,,,
|
||||
|
|
23
recopytex/dashboard/common/formating.py
Normal file
23
recopytex/dashboard/common/formating.py
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
|
||||
def highlight_scores(highlight_columns, score_color):
|
||||
""" Cells style in a datatable for scores
|
||||
|
||||
:param highlight_columns: columns to highlight
|
||||
:param value_color: dictionnary {"score": "color"}
|
||||
|
||||
"""
|
||||
hight = []
|
||||
for v, color in score_color.items():
|
||||
if v:
|
||||
hight += [
|
||||
{
|
||||
"if": {"filter_query": "{{{}}} = {}".format(col, v), "column_id": col},
|
||||
"backgroundColor": color,
|
||||
"color": "white",
|
||||
}
|
||||
for col in highlight_columns
|
||||
]
|
||||
return hight
|
@@ -7,62 +7,106 @@ 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",
|
||||
),
|
||||
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",
|
||||
),
|
||||
html.Section(
|
||||
children=[
|
||||
dash_table.DataTable(
|
||||
id="scores_table",
|
||||
columns=[],
|
||||
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",
|
||||
),
|
||||
dcc.Store(id="scores"),
|
||||
],
|
||||
)
|
||||
),
|
||||
dcc.Store(id="scores"),
|
||||
],
|
||||
)
|
||||
|
@@ -2,14 +2,27 @@
|
||||
# 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 ...app import app
|
||||
from .models import get_tribes, get_exams, get_unstack_scores, get_students_from_exam
|
||||
from recopytex.dashboard.app import app
|
||||
from recopytex.dashboard.common.formating import highlight_scores
|
||||
|
||||
from .models import (
|
||||
get_tribes,
|
||||
get_exams,
|
||||
get_unstack_scores,
|
||||
get_students_from_exam,
|
||||
get_score_colors,
|
||||
get_level_color_bar,
|
||||
score_to_final_mark,
|
||||
stack_scores,
|
||||
pivot_score_on,
|
||||
)
|
||||
|
||||
|
||||
@app.callback(
|
||||
@@ -43,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)])
|
||||
@@ -57,11 +69,148 @@ def update_scores_store(exam):
|
||||
"score_rate",
|
||||
"is_leveled",
|
||||
]
|
||||
columns = fixed_columns + list(get_students_from_exam(exam))
|
||||
|
||||
students = list(get_students_from_exam(exam))
|
||||
columns = fixed_columns + students
|
||||
|
||||
score_color = get_score_colors()
|
||||
|
||||
return [
|
||||
[{"id": c, "name": c} for c in columns],
|
||||
scores.to_dict("records"),
|
||||
[],
|
||||
highlight_scores(students, score_color),
|
||||
{"headers": True, "data": len(fixed_columns)},
|
||||
]
|
||||
|
||||
|
||||
@app.callback(
|
||||
[
|
||||
Output("final_score_table", "data"),
|
||||
],
|
||||
[
|
||||
Input("scores_table", "data"),
|
||||
],
|
||||
)
|
||||
def update_finale_score_table(scores):
|
||||
scores_df = pd.DataFrame.from_records(scores)
|
||||
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]
|
||||
|
@@ -1,10 +1,44 @@
|
||||
#!/usr/bin/env python
|
||||
#!/use/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from recopytex.database.filesystem.loader import CSVLoader
|
||||
from recopytex.lib.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_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,10 +55,74 @@ 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(flat_scores, "student_name", "score", kept_columns)
|
||||
return unstack_scores(flat_scores)
|
||||
|
||||
|
||||
def get_students_from_exam(exam):
|
||||
flat_scores = LOADER.get_exam_scores(exam)
|
||||
return flat_scores["student_name"].unique()
|
||||
|
||||
|
||||
def get_score_colors():
|
||||
score_color = {}
|
||||
for key, score in SCORES_CONFIG.items():
|
||||
score_color[score["value"]] = score["color"]
|
||||
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 """
|
||||
|
||||
filtered_scores = filter_clean_score(scores)
|
||||
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")]
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
# encoding: utf-8
|
||||
|
||||
import dash_html_components as html
|
||||
from ....database.filesystem.loader import CSVLoader
|
||||
from recopytex.database.filesystem.loader import CSVLoader
|
||||
from .models import get_tribes, get_exams, get_students
|
||||
|
||||
loader = CSVLoader("./test_config.yml")
|
||||
|
@@ -2,5 +2,5 @@
|
||||
# encoding: utf-8
|
||||
|
||||
from dash.dependencies import Input, Output
|
||||
from ...app import app
|
||||
from recopytex.dashboard.app import app
|
||||
|
||||
|
@@ -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")])
|
||||
|
@@ -21,14 +21,42 @@ competences: # Competences
|
||||
name: Communiquer
|
||||
abrv: Com
|
||||
|
||||
valid_scores: #
|
||||
BAD: 0 # Everything is bad
|
||||
FEW: 1 # Few good things
|
||||
NEARLY: 2 # Nearly good but things are missing
|
||||
GOOD: 3 # Everything is good
|
||||
NOTFILLED: # The item is not scored yet
|
||||
NOANSWER: . # Student gives no answer (count as 0)
|
||||
ABS: "a" # Student has absent (this score won't be impact the final mark)
|
||||
scores: #
|
||||
BAD: # Everything is bad
|
||||
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
|
||||
|
@@ -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'}}, 'valid_scores': {'BAD': 0, 'FEW': 1, 'NEARLY': 2, 'GOOD': 3, 'NOTFILLED': None, 'NOANSWER': '.', 'ABS': 'a'}, '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'}}, 'valid_scores': {'BAD': 0, 'FEW': 1, 'NEARLY': 2, 'GOOD': 3, 'NOTFILLED': None, 'NOANSWER': '.', 'ABS': 'a'}, '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
|
||||
@@ -161,7 +162,8 @@ class CSVLoader(Loader):
|
||||
:example:
|
||||
>>> loader = CSVLoader("./test_config.yml")
|
||||
>>> exams = loader.get_exams(["Tribe1"])
|
||||
>>> all(loader.get_exam_questions([exams.iloc[0]]).columns == loader.score_columns)
|
||||
>>> all(loader.get_exam_questions([exams.iloc[0]]).columns == loader.question_columns)
|
||||
True
|
||||
>>> questions = loader.get_exam_questions(exams)
|
||||
>>> questions.iloc[0]
|
||||
exercise Exercice 1
|
||||
|
0
recopytex/datalib/__init__.py
Normal file
0
recopytex/datalib/__init__.py
Normal file
21
recopytex/datalib/dataframe.py
Normal file
21
recopytex/datalib/dataframe.py
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
|
||||
def column_values_to_column(pivot_column, value_column, kept_columns, df):
|
||||
"""Pivot_column's values go to column with value_column under it, keeping kept_columns
|
||||
|
||||
:param pivot_column: column name where value will become columns
|
||||
:param value_column: column name where value will be under pivot_column
|
||||
:param kept_columns: unchanged columns
|
||||
:param df: DataFrame to work with
|
||||
|
||||
:return: Stack dataframe
|
||||
|
||||
"""
|
||||
if pivot_column in kept_columns:
|
||||
pivot_columns = kept_columns
|
||||
else:
|
||||
pivot_columns = kept_columns + [pivot_column]
|
||||
|
||||
return df.set_index(pivot_columns).unstack(pivot_column)[value_column].reset_index()
|
257
recopytex/datalib/on_score_column.py
Normal file
257
recopytex/datalib/on_score_column.py
Normal file
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from math import ceil
|
||||
import pandas as pd
|
||||
|
||||
|
||||
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 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):
|
||||
"""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)):
|
||||
"""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
|
||||
|
||||
:param x: dictionnary with "is_leveled", "score" (need to be number) and "score_rate" keys
|
||||
:param score_max:
|
||||
:param rounding: rounding mark function
|
||||
:return: the mark
|
||||
|
||||
>>> 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 = 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
|
||||
>>> 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)):
|
||||
raise ValueError(f"The evaluation is out of range: {x['score']} at {x}")
|
||||
return rounding(x["score"] * x["score_rate"] / score_max)
|
||||
|
||||
return rounding(x["score"])
|
||||
|
||||
|
||||
def score_to_level(x, level_max=3):
|
||||
"""Compute the level (".",0,1,2,3).
|
||||
|
||||
:param x: dictionnary with "is_leveled", "score" and "score_rate" keys
|
||||
:return: the level
|
||||
|
||||
>>> 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, 0, 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 0.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
|
||||
>>> df.apply(score_to_level, axis=1)
|
||||
0 3
|
||||
1 1
|
||||
2 0
|
||||
3 3
|
||||
4 1
|
||||
5 3
|
||||
6 2
|
||||
7 3
|
||||
8 3
|
||||
9 2
|
||||
10 2
|
||||
11 3
|
||||
dtype: int64
|
||||
>>> df.apply(lambda x: score_to_level(x, 5), axis=1)
|
||||
0 5
|
||||
1 2
|
||||
2 0
|
||||
3 4
|
||||
4 1
|
||||
5 3
|
||||
6 4
|
||||
7 5
|
||||
8 4
|
||||
9 3
|
||||
10 2
|
||||
11 3
|
||||
dtype: int64
|
||||
"""
|
||||
if x["is_leveled"]:
|
||||
return int(x["score"])
|
||||
|
||||
if x["score"] > x["score_rate"]:
|
||||
raise ValueError(
|
||||
f"score is higher than score_rate ({x['score']} > {x['score_rate']}) for {x}"
|
||||
)
|
||||
|
||||
return int(ceil(x["score"] / x["score_rate"] * level_max))
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Reglages pour 'vim'
|
||||
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:
|
||||
# cursor: 16 del
|
40
recopytex/datalib/on_value.py
Normal file
40
recopytex/datalib/on_value.py
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from math import ceil, floor
|
||||
|
||||
|
||||
def round_with_base(x, base=0.5):
|
||||
"""Round to a multiple of base
|
||||
|
||||
:example:
|
||||
>>> round_with_base(1.33, 0.1)
|
||||
1.3
|
||||
>>> round_with_base(1.33, 0.2)
|
||||
1.4
|
||||
>>> round_with_base(1.33, 1)
|
||||
1
|
||||
>>> round_with_base(1.33, 2)
|
||||
2
|
||||
"""
|
||||
try:
|
||||
prec = len(str(base).split(".")[1])
|
||||
except IndexError:
|
||||
prec = 0
|
||||
return round(base * round(float(x) / base), prec)
|
||||
|
||||
|
||||
def round_half_point(x):
|
||||
"""Round to nearest half point
|
||||
|
||||
:example:
|
||||
>>> round_half_point(1.33)
|
||||
1.5
|
||||
>>> round_half_point(1.1)
|
||||
1.0
|
||||
>>> round_half_point(1.66)
|
||||
1.5
|
||||
>>> round_half_point(1.76)
|
||||
2.0
|
||||
"""
|
||||
return round_with_base(x, base=0.5)
|
@@ -2,8 +2,7 @@
|
||||
# encoding: utf-8
|
||||
|
||||
import click
|
||||
from ..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()
|
||||
|
Reference in New Issue
Block a user