386 lines
12 KiB
Python
386 lines
12 KiB
Python
#!/usr/bin/env python
|
|
# encoding: utf-8
|
|
|
|
import dash
|
|
import dash_html_components as html
|
|
import dash_core_components as dcc
|
|
import dash_table
|
|
from dash.exceptions import PreventUpdate
|
|
import plotly.graph_objects as go
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
import pandas as pd
|
|
import numpy as np
|
|
|
|
|
|
from ... import flat_df_students, pp_q_scores
|
|
from ...config import NO_ST_COLUMNS
|
|
from ...scripts.getconfig import config
|
|
from ..app import app
|
|
|
|
COLORS = {
|
|
".": "black",
|
|
0: "#E7472B",
|
|
1: "#FF712B",
|
|
2: "#F2EC4C",
|
|
3: "#68D42F",
|
|
}
|
|
|
|
layout = html.Div(
|
|
children=[
|
|
html.Header(
|
|
children=[
|
|
html.H1("Analyse des notes"),
|
|
html.P("Dernière sauvegarde", id="lastsave"),
|
|
],
|
|
),
|
|
html.Main(
|
|
[
|
|
html.Section(
|
|
[
|
|
html.Div(
|
|
[
|
|
"Classe: ",
|
|
dcc.Dropdown(
|
|
id="tribe",
|
|
options=[
|
|
{"label": t["name"], "value": t["name"]}
|
|
for t in config["tribes"]
|
|
],
|
|
value=config["tribes"][0]["name"],
|
|
),
|
|
],
|
|
style={
|
|
"display": "flex",
|
|
"flex-flow": "column",
|
|
},
|
|
),
|
|
html.Div(
|
|
[
|
|
"Evaluation: ",
|
|
dcc.Dropdown(id="csv"),
|
|
],
|
|
style={
|
|
"display": "flex",
|
|
"flex-flow": "column",
|
|
},
|
|
),
|
|
],
|
|
id="select",
|
|
style={
|
|
"display": "flex",
|
|
"flex-flow": "row wrap",
|
|
},
|
|
),
|
|
html.Div(
|
|
[
|
|
html.Div(
|
|
dash_table.DataTable(
|
|
id="final_score_table",
|
|
columns=[
|
|
{"id": "Élève", "name": "Élève"},
|
|
{"id": "Note", "name": "Note"},
|
|
{"id": "Barème", "name": "Barème"},
|
|
],
|
|
data=[],
|
|
style_data_conditional=[
|
|
{
|
|
"if": {"row_index": "odd"},
|
|
"backgroundColor": "rgb(248, 248, 248)",
|
|
}
|
|
],
|
|
style_data={
|
|
"width": "100px",
|
|
"maxWidth": "100px",
|
|
"minWidth": "100px",
|
|
},
|
|
),
|
|
id="final_score_table_container",
|
|
),
|
|
html.Div(
|
|
[
|
|
dash_table.DataTable(
|
|
id="final_score_describe",
|
|
columns=[
|
|
{"id": "count", "name": "count"},
|
|
{"id": "mean", "name": "mean"},
|
|
{"id": "std", "name": "std"},
|
|
{"id": "min", "name": "min"},
|
|
{"id": "25%", "name": "25%"},
|
|
{"id": "50%", "name": "50%"},
|
|
{"id": "75%", "name": "75%"},
|
|
{"id": "max", "name": "max"},
|
|
],
|
|
),
|
|
dcc.Graph(
|
|
id="fig_assessment_hist",
|
|
),
|
|
dcc.Graph(id="fig_competences"),
|
|
],
|
|
id="desc_plots",
|
|
),
|
|
],
|
|
id="analysis",
|
|
),
|
|
html.Div(
|
|
[
|
|
dash_table.DataTable(
|
|
id="scores_table",
|
|
columns=[
|
|
{"id": c, "name": c} for c in NO_ST_COLUMNS.values()
|
|
],
|
|
style_cell={
|
|
"whiteSpace": "normal",
|
|
"height": "auto",
|
|
},
|
|
style_data_conditional=[],
|
|
editable=True,
|
|
),
|
|
html.Button("Ajouter un élément", id="btn_add_element"),
|
|
],
|
|
id="big_table",
|
|
),
|
|
dcc.Store(id="final_score"),
|
|
],
|
|
className="content",
|
|
style={
|
|
"width": "95vw",
|
|
"margin": "auto",
|
|
},
|
|
),
|
|
],
|
|
)
|
|
|
|
|
|
@app.callback(
|
|
[
|
|
dash.dependencies.Output("csv", "options"),
|
|
dash.dependencies.Output("csv", "value"),
|
|
],
|
|
[dash.dependencies.Input("tribe", "value")],
|
|
)
|
|
def update_csvs(value):
|
|
if not value:
|
|
raise PreventUpdate
|
|
p = Path(value)
|
|
csvs = list(p.glob("*.csv"))
|
|
try:
|
|
return [{"label": str(c), "value": str(c)} for c in csvs], str(csvs[0])
|
|
except IndexError:
|
|
return []
|
|
|
|
|
|
@app.callback(
|
|
[
|
|
dash.dependencies.Output("final_score", "data"),
|
|
],
|
|
[dash.dependencies.Input("scores_table", "data")],
|
|
)
|
|
def update_final_scores(data):
|
|
if not data:
|
|
raise PreventUpdate
|
|
|
|
scores = pd.DataFrame.from_records(data)
|
|
try:
|
|
if scores.iloc[0]["Commentaire"] == "commentaire":
|
|
scores.drop([0], inplace=True)
|
|
except KeyError:
|
|
pass
|
|
scores = flat_df_students(scores).dropna(subset=["Score"])
|
|
if scores.empty:
|
|
return [{}]
|
|
|
|
scores = pp_q_scores(scores)
|
|
assessment_scores = scores.groupby(["Eleve"]).agg({"Note": "sum", "Bareme": "sum"})
|
|
return [assessment_scores.reset_index().to_dict("records")]
|
|
|
|
|
|
@app.callback(
|
|
[
|
|
dash.dependencies.Output("final_score_table", "data"),
|
|
],
|
|
[dash.dependencies.Input("final_score", "data")],
|
|
)
|
|
def update_final_scores_table(data):
|
|
assessment_scores = pd.DataFrame.from_records(data)
|
|
return [assessment_scores.to_dict("records")]
|
|
|
|
|
|
@app.callback(
|
|
[
|
|
dash.dependencies.Output("final_score_describe", "data"),
|
|
],
|
|
[dash.dependencies.Input("final_score", "data")],
|
|
)
|
|
def update_final_scores_descr(data):
|
|
scores = pd.DataFrame.from_records(data)
|
|
if scores.empty:
|
|
return [[{}]]
|
|
desc = scores["Note"].describe().T.round(2)
|
|
return [[desc.to_dict()]]
|
|
|
|
|
|
@app.callback(
|
|
[
|
|
dash.dependencies.Output("fig_assessment_hist", "figure"),
|
|
],
|
|
[dash.dependencies.Input("final_score", "data")],
|
|
)
|
|
def update_final_scores_hist(data):
|
|
assessment_scores = pd.DataFrame.from_records(data)
|
|
|
|
if assessment_scores.empty:
|
|
return [{"data": [], "layout": []}]
|
|
|
|
ranges = np.linspace(
|
|
-0.5,
|
|
assessment_scores.Bareme.max(),
|
|
int(assessment_scores.Bareme.max() * 2 + 2),
|
|
)
|
|
bins = pd.cut(assessment_scores["Note"], ranges)
|
|
assessment_scores["Bin"] = bins
|
|
assessment_grouped = (
|
|
assessment_scores.reset_index()
|
|
.groupby("Bin")
|
|
.agg({"Bareme": "count", "Eleve": lambda x: "\n".join(x)})
|
|
)
|
|
assessment_grouped.index = assessment_grouped.index.map(lambda i: i.right)
|
|
fig = go.Figure()
|
|
fig.add_bar(
|
|
x=assessment_grouped.index,
|
|
y=assessment_grouped.Bareme,
|
|
text=assessment_grouped.Eleve,
|
|
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(
|
|
[
|
|
dash.dependencies.Output("fig_competences", "figure"),
|
|
],
|
|
[dash.dependencies.Input("scores_table", "data")],
|
|
)
|
|
def update_competence_fig(data):
|
|
scores = pd.DataFrame.from_records(data)
|
|
try:
|
|
if scores.iloc[0]["Commentaire"] == "commentaire":
|
|
scores.drop([0], inplace=True)
|
|
except KeyError:
|
|
pass
|
|
scores = flat_df_students(scores).dropna(subset=["Score"])
|
|
|
|
if scores.empty:
|
|
return [{"data": [], "layout": []}]
|
|
|
|
scores = pp_q_scores(scores)
|
|
pt = pd.pivot_table(
|
|
scores,
|
|
index=["Exercice", "Question", "Commentaire"],
|
|
columns="Score",
|
|
aggfunc="size",
|
|
fill_value=0,
|
|
)
|
|
for i in {i for i in pt.index.get_level_values(0)}:
|
|
pt.loc[(str(i), "", ""), :] = ""
|
|
pt.sort_index(inplace=True)
|
|
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 = [
|
|
{"score": -1, "name": "Pas de réponse", "color": COLORS["."]},
|
|
{"score": 0, "name": "Faux", "color": COLORS[0]},
|
|
{"score": 1, "name": "Peu juste", "color": COLORS[1]},
|
|
{"score": 2, "name": "Presque juste", "color": COLORS[2]},
|
|
{"score": 3, "name": "Juste", "color": COLORS[3]},
|
|
]
|
|
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),
|
|
)
|
|
return [fig]
|
|
|
|
|
|
@app.callback(
|
|
[
|
|
dash.dependencies.Output("lastsave", "children"),
|
|
],
|
|
[
|
|
dash.dependencies.Input("scores_table", "data"),
|
|
dash.dependencies.State("csv", "value"),
|
|
],
|
|
)
|
|
def save_scores(data, csv):
|
|
try:
|
|
scores = pd.DataFrame.from_records(data)
|
|
scores.to_csv(csv, index=False)
|
|
except:
|
|
return [f"Soucis pour sauvegarder à {datetime.today()} dans {csv}"]
|
|
else:
|
|
return [f"Dernière sauvegarde {datetime.today()} dans {csv}"]
|
|
|
|
|
|
def highlight_value(df):
|
|
""" Cells style """
|
|
hight = []
|
|
for v, color in COLORS.items():
|
|
hight += [
|
|
{
|
|
"if": {"filter_query": "{{{}}} = {}".format(col, v), "column_id": col},
|
|
"backgroundColor": color,
|
|
"color": "white",
|
|
}
|
|
for col in df.columns
|
|
if col not in NO_ST_COLUMNS.values()
|
|
]
|
|
return hight
|
|
|
|
|
|
@app.callback(
|
|
[
|
|
dash.dependencies.Output("scores_table", "columns"),
|
|
dash.dependencies.Output("scores_table", "data"),
|
|
dash.dependencies.Output("scores_table", "style_data_conditional"),
|
|
],
|
|
[
|
|
dash.dependencies.Input("csv", "value"),
|
|
dash.dependencies.Input("btn_add_element", "n_clicks"),
|
|
dash.dependencies.State("scores_table", "data"),
|
|
],
|
|
)
|
|
def update_scores_table(csv, add_element, data):
|
|
ctx = dash.callback_context
|
|
if ctx.triggered[0]["prop_id"] == "csv.value":
|
|
stack = pd.read_csv(csv, encoding="UTF8")
|
|
elif ctx.triggered[0]["prop_id"] == "btn_add_element.n_clicks":
|
|
stack = pd.DataFrame.from_records(data)
|
|
infos = pd.DataFrame.from_records(
|
|
[{k: stack.iloc[-1][k] for k in NO_ST_COLUMNS.values()}]
|
|
)
|
|
stack = stack.append(infos)
|
|
return (
|
|
[{"id": c, "name": c} for c in stack.columns],
|
|
stack.to_dict("records"),
|
|
highlight_value(stack),
|
|
)
|