Compare commits

17 Commits

Author SHA1 Message Date
83eb9c327b Feat: import html for 404 layout 2021-11-24 06:04:02 +01:00
ff1ecfef25 Feat: import app from good place (index instead of app)
see https://dash.plotly.com/urls
2021-11-24 06:03:16 +01:00
921292a0d2 Feat: activate cli 2021-11-24 05:48:22 +01:00
2d08671247 Feat: add "." in csv example and fix issues 2021-04-22 08:00:25 +02:00
a16211cbd4 Feat: legend formating and remove toolbox 2021-04-22 08:00:02 +02:00
876f583d51 Feat: format_score 2021-04-22 07:49:51 +02:00
97b97af2de Fix: replace np.isnan with pd.isnull 2021-04-21 07:09:05 +02:00
d8d84690c6 Feat: remove a print 2021-04-21 07:08:58 +02:00
18f855ab83 Feat: question levels bar figure 2021-04-21 07:03:45 +02:00
36425e587e Feat: add comment to score_config 2021-04-21 07:03:31 +02:00
8cdeecfc53 Feat: Score histogram 2021-04-20 19:13:14 +02:00
1a7c97d869 Feat: statistics table 2021-04-20 19:04:06 +02:00
ab5de2711e Feat: score_table style 2021-04-20 18:48:52 +02:00
235019102b Feat: Final mark for students 2021-04-20 18:30:12 +02:00
8ec24a24b3 Feat: delete functions on dataframe and move it to functions on rows 2021-04-19 21:54:44 +02:00
2e86b3a0a2 Feat: filter none numeric scores 2021-04-19 14:30:37 +02:00
7e6b24aaea Feat: rename valid_scores to scores in config 2021-04-19 14:29:57 +02:00
10 changed files with 514 additions and 240 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 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,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.0,2,,,3.0,3.0,,,,,2.0,,,,,,, 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.0,,,,,,,,3.0,,,,,,, 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,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

@@ -7,62 +7,106 @@ from .models import get_tribes, get_exams
from .callbacks import * from .callbacks import *
layout = html.Div( layout = html.Div(
children=[ children=[
html.Header( html.Header(
children=[ children=[
html.H1("Analyse des notes"), html.H1("Analyse des notes"),
html.P("Dernière sauvegarde", id="lastsave"), 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( html.Section(
children=[ children=[
html.Section( html.Div(
children=[ children=[
html.Div( dash_table.DataTable(
children=[ id="final_score_table",
"Classe: ", columns=[
dcc.Dropdown( {"name": "Étudiant", "id": "student_name"},
id="tribe", {"name": "Note", "id": "mark"},
options=[ {"name": "Barème", "id": "score_rate"},
{"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.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"),
],
)

View File

@@ -2,11 +2,12 @@
# encoding: utf-8 # encoding: utf-8
from dash.dependencies import Input, Output, State from dash.dependencies import Input, Output, State
import dash
from dash.exceptions import PreventUpdate from dash.exceptions import PreventUpdate
import plotly.graph_objects as go
import dash_table import dash_table
import json import json
import pandas as pd import pandas as pd
import numpy as np
from recopytex.dashboard.app import app from recopytex.dashboard.app import app
from recopytex.dashboard.common.formating import highlight_scores from recopytex.dashboard.common.formating import highlight_scores
@@ -17,6 +18,10 @@ from .models import (
get_unstack_scores, get_unstack_scores,
get_students_from_exam, get_students_from_exam,
get_score_colors, get_score_colors,
get_level_color_bar,
score_to_final_mark,
stack_scores,
pivot_score_on,
) )
@@ -51,7 +56,6 @@ def update_exams_choices(tribe):
], ],
) )
def update_scores_store(exam): def update_scores_store(exam):
ctx = dash.callback_context
if not exam: if not exam:
return [[], [], [], {}] return [[], [], [], {}]
exam = pd.DataFrame.from_dict([json.loads(exam)]) exam = pd.DataFrame.from_dict([json.loads(exam)])
@@ -77,3 +81,136 @@ 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_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]

View File

@@ -1,10 +1,44 @@
#!/usr/bin/env python #!/use/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_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(): def get_tribes():
@@ -21,8 +55,7 @@ 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)
kept_columns = [col for col in LOADER.score_columns if col != "score"] return unstack_scores(flat_scores)
return column_values_to_column("student_name", "score", kept_columns, flat_scores)
def get_students_from_exam(exam): def get_students_from_exam(exam):
@@ -31,8 +64,65 @@ 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
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

View File

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

View File

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

View File

@@ -31,10 +31,11 @@ class CSVLoader(Loader):
:example: :example:
>>> loader = CSVLoader() >>> loader = CSVLoader()
>>> loader.get_config() >>> 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': {'value': 0, 'color': '#E7472B'}, 'FEW': {'value': 1, 'color': '#FF712B'}, 'NEARLY': {'value': 2, 'color': '#F2EC4C'}, 'GOOD': {'value': 3, 'color': '#68D42F'}, 'NOTFILLED': {'value': '', 'color': 'white'}, 'NOANSWER': {'value': '.', 'color': 'black'}, 'ABS': {'value': 'a', '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 = CSVLoader("./test_config.yml")
>>> loader.get_config() >>> 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': {'value': 0, 'color': '#E7472B'}, 'FEW': {'value': 1, 'color': '#FF712B'}, 'NEARLY': {'value': 2, 'color': '#F2EC4C'}, 'GOOD': {'value': 3, 'color': '#68D42F'}, 'NOTFILLED': {'value': '', 'color': 'white'}, 'NOANSWER': {'value': '.', 'color': 'black'}, 'ABS': {'value': 'a', '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 CONFIG = DEFAULT_CONFIG

View File

@@ -2,53 +2,179 @@
# encoding: utf-8 # encoding: utf-8
from math import ceil 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)): 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 if the item is leveled then the score is multiply by the score_rate
otherwise it copies the score otherwise it copies the score
:param x: dictionnary with "is_leveled", "score" and "score_rate" keys :param x: dictionnary with "is_leveled", "score" (need to be number) and "score_rate" keys
:param score_max: :param score_max:
:param rounding: rounding mark function :param rounding: rounding mark function
:return: the mark :return: the mark
>>> import pandas as pd >>> import pandas as pd
>>> d = {"Eleve":["E1"]*6 + ["E2"]*6, >>> d = {"Eleve":["E1"]*7,
... "score_rate":[1]*2+[2]*2+[2]*2 + [1]*2+[2]*2+[2]*2, ... "score_rate": [1]*7,
... "is_leveled":[0]*4+[1]*2 + [0]*4+[1]*2, ... "is_leveled":[0]+[1]*6,
... "score":[1, 0.33, 2, 1.5, 1, 3, 0.666, 1, 1.5, 1.2, 2, 3], ... "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 = pd.DataFrame(d)
>>> df.loc[0] >>> df = df[~df.apply(lambda x:is_none_score(x, score_config), axis=1)]
Eleve E1 >>> df["score"] = df.apply(lambda x:score_to_numeric_score(x, score_config), axis=1)
score_rate 1 >>> df.apply(lambda x:score_to_mark(x, 3), axis=1)
is_leveled 0 0 0.33
score 1.0 2 0.00
Name: 0, dtype: object 4 0.33
>>> score_to_mark(df.loc[0], 3) 5 0.67
1.0 6 1.00
>>> df.loc[10] dtype: float64
Eleve E2
score_rate 2
is_leveled 1
score 2.0
Name: 10, dtype: object
>>> score_to_mark(df.loc[10], 3)
1.33
>>> from .on_value import round_half_point >>> from .on_value import round_half_point
>>> score_to_mark(df.loc[10], 3, round_half_point) >>> df.apply(lambda x:score_to_mark(x, 3, round_half_point), axis=1)
1.5 0 0.5
>>> df.loc[1] 2 0.0
Eleve E1 4 0.5
score_rate 1 5 0.5
is_leveled 0 6 1.0
score 0.33 dtype: float64
Name: 1, dtype: object
>>> score_to_mark(df.loc[1], 3)
0.33
""" """
if x["is_leveled"]: if x["is_leveled"]:
if x["score"] not in list(range(score_max + 1)): if x["score"] not in list(range(score_max + 1)):

View File

@@ -1,141 +0,0 @@
#!/usr/bin/env python
# encoding: utf-8
from .on_score_column import score_to_mark, score_to_level
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", "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":[1, 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 1.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"])
# -----------------------------
# Reglages pour 'vim'
# vim:set autoindent expandtab tabstop=4 shiftwidth=4:
# cursor: 16 del

View File

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