mirror of
https://codeberg.org/Mo8it/AdvLabDB.git
synced 2024-09-19 18:31:16 +00:00
Fix analysis
This commit is contained in:
parent
a70e01114c
commit
0242b4389d
5 changed files with 212 additions and 159 deletions
|
@ -1,8 +1,5 @@
|
||||||
from base64 import b64encode
|
|
||||||
from io import BytesIO
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from flask import flash, has_request_context, redirect
|
from flask import flash, has_request_context, redirect
|
||||||
from flask_admin import Admin as FlaskAdmin
|
from flask_admin import Admin as FlaskAdmin
|
||||||
from flask_admin import expose
|
from flask_admin import expose
|
||||||
|
@ -15,7 +12,6 @@ from flask_security.changeable import admin_change_password
|
||||||
from flask_security.utils import hash_password
|
from flask_security.utils import hash_password
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from flask_wtf.file import FileAllowed, FileField, FileRequired
|
from flask_wtf.file import FileAllowed, FileField, FileRequired
|
||||||
from matplotlib.figure import Figure
|
|
||||||
from sqlalchemy import and_, not_, or_, select
|
from sqlalchemy import and_, not_, or_, select
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from wtforms.fields import (
|
from wtforms.fields import (
|
||||||
|
@ -60,6 +56,7 @@ from .advlabdb_independent_funs import (
|
||||||
flashRandomPassword,
|
flashRandomPassword,
|
||||||
str_without_semester_formatter,
|
str_without_semester_formatter,
|
||||||
)
|
)
|
||||||
|
from .analysis import assistant_marks_analysis, final_part_marks_analysis
|
||||||
from .custom_classes import (
|
from .custom_classes import (
|
||||||
SecureAdminBaseView,
|
SecureAdminBaseView,
|
||||||
SecureAdminIndexView,
|
SecureAdminIndexView,
|
||||||
|
@ -1372,154 +1369,22 @@ class ActionsView(SecureAdminBaseView):
|
||||||
|
|
||||||
class AnalysisView(SecureAdminBaseView):
|
class AnalysisView(SecureAdminBaseView):
|
||||||
class AnalysisForm(FlaskForm):
|
class AnalysisForm(FlaskForm):
|
||||||
assistantMarksSubmit = SubmitField(
|
assistant_marks_submit = SubmitField(
|
||||||
label="Assistant's marks",
|
label="Active assistant's marks in all semesters",
|
||||||
)
|
)
|
||||||
finalPartMarksSubmit = SubmitField(
|
final_part_marks_submit = SubmitField(
|
||||||
label="Final part marks",
|
label="Final part marks in active semester",
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def htmlFig(fig):
|
|
||||||
buf = BytesIO()
|
|
||||||
fig.savefig(buf, format="png")
|
|
||||||
|
|
||||||
return b64encode(buf.getbuffer()).decode("ascii")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def markHist(data, title):
|
|
||||||
fig = Figure()
|
|
||||||
ax = fig.subplots()
|
|
||||||
ax.set_xlim(MIN_MARK - 0.5, MAX_MARK + 0.5)
|
|
||||||
ax.set_xticks(np.arange(MAX_MARK + 1))
|
|
||||||
ax.set_xlabel("Mark")
|
|
||||||
|
|
||||||
N = data.size
|
|
||||||
title += f"\nN = {N}"
|
|
||||||
|
|
||||||
if N > 0:
|
|
||||||
ax.hist(
|
|
||||||
data,
|
|
||||||
bins=np.arange(MAX_MARK) - 0.5,
|
|
||||||
)
|
|
||||||
ax.set_yticks(np.arange(N + 1))
|
|
||||||
title += f" | mean = {round(np.mean(data), 1)}"
|
|
||||||
else:
|
|
||||||
ax.set_yticks(np.arange(2))
|
|
||||||
|
|
||||||
ax.set_title(title)
|
|
||||||
|
|
||||||
return AnalysisView.htmlFig(fig)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_experiment_marks(assistant, attr):
|
|
||||||
data = []
|
|
||||||
|
|
||||||
for experimentMark in assistant.experiment_marks:
|
|
||||||
mark = getattr(experimentMark, attr)
|
|
||||||
if mark is not None:
|
|
||||||
data.append(mark)
|
|
||||||
|
|
||||||
return np.array(data)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def markHists(markType, activeAssistants):
|
|
||||||
attr = markType.lower() + "_mark"
|
|
||||||
markTypeTitleAddition = f" | {markType} marks"
|
|
||||||
marks = [AnalysisView.get_experiment_marks(assistant, attr) for assistant in activeAssistants]
|
|
||||||
|
|
||||||
hists = [
|
|
||||||
AnalysisView.markHist(
|
|
||||||
data=marks[i],
|
|
||||||
title=str(activeAssistants[i]) + markTypeTitleAddition,
|
|
||||||
)
|
|
||||||
for i in range(len(marks))
|
|
||||||
]
|
|
||||||
|
|
||||||
hists.append(
|
|
||||||
AnalysisView.markHist(
|
|
||||||
data=np.hstack(marks),
|
|
||||||
title="All" + markTypeTitleAddition,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return hists
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_final_part_marks(part):
|
|
||||||
data = []
|
|
||||||
for partStudent in part.part_students:
|
|
||||||
mark = partStudent.final_part_mark
|
|
||||||
if mark is not None:
|
|
||||||
data.append(mark)
|
|
||||||
|
|
||||||
return np.array(data)
|
|
||||||
|
|
||||||
@expose("/", methods=("GET", "POST"))
|
@expose("/", methods=("GET", "POST"))
|
||||||
def index(self):
|
def index(self):
|
||||||
form = AnalysisView.AnalysisForm()
|
form = AnalysisView.AnalysisForm()
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
if form.assistantMarksSubmit.data:
|
if form.assistant_marks_submit.data:
|
||||||
activeAssistants = assistantQueryFactory()
|
return assistant_marks_analysis(self)
|
||||||
|
elif form.final_part_marks_submit.data:
|
||||||
oralMarkHists = AnalysisView.markHists("Oral", activeAssistants)
|
return final_part_marks_analysis(self)
|
||||||
protocolMarkHists = AnalysisView.markHists("Protocol", activeAssistants)
|
|
||||||
|
|
||||||
return self.render(
|
|
||||||
"analysis/assistant_marks.jinja.html",
|
|
||||||
histIndices=range(len(oralMarkHists)),
|
|
||||||
oralMarkHists=oralMarkHists,
|
|
||||||
protocolMarkHists=protocolMarkHists,
|
|
||||||
)
|
|
||||||
|
|
||||||
if form.finalPartMarksSubmit.data:
|
|
||||||
parts = current_user.active_semester.parts
|
|
||||||
activeSemesterFinalPartMarksHists = [
|
|
||||||
AnalysisView.markHist(
|
|
||||||
data=AnalysisView.get_final_part_marks(part),
|
|
||||||
title=str(part),
|
|
||||||
)
|
|
||||||
for part in parts
|
|
||||||
]
|
|
||||||
|
|
||||||
semesters = Semester.sortedSemestersStartingWithNewest()
|
|
||||||
meanFinalPartMarks = np.flip(
|
|
||||||
[
|
|
||||||
np.mean(
|
|
||||||
np.hstack(
|
|
||||||
[
|
|
||||||
[partStudent.final_part_mark for partStudent in part.part_students]
|
|
||||||
for part in semester.parts
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for semester in semesters
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
fig = Figure()
|
|
||||||
lenMeanFinalPartMarks = len(meanFinalPartMarks)
|
|
||||||
if lenMeanFinalPartMarks > 0:
|
|
||||||
ax = fig.subplots()
|
|
||||||
x = range(1, lenMeanFinalPartMarks + 1)
|
|
||||||
ax.plot(
|
|
||||||
x,
|
|
||||||
meanFinalPartMarks,
|
|
||||||
marker="d",
|
|
||||||
)
|
|
||||||
# TODO: Change ticks to semester labels
|
|
||||||
# TODO: Check linestyle
|
|
||||||
ax.set_xticks(x)
|
|
||||||
ax.set_xlim(0.5, x[-1] + 0.5)
|
|
||||||
|
|
||||||
meanFinalPartMarksPlot = AnalysisView.htmlFig(fig)
|
|
||||||
|
|
||||||
return self.render(
|
|
||||||
"analysis/final_part_marks.jinja.html",
|
|
||||||
activeSemesterFinalPartMarksHists=activeSemesterFinalPartMarksHists,
|
|
||||||
meanFinalPartMarksPlot=meanFinalPartMarksPlot,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.render("analysis/analysis.jinja.html", form=form)
|
return self.render("analysis/analysis.jinja.html", form=form)
|
||||||
|
|
||||||
|
|
142
advlabdb/analysis.py
Normal file
142
advlabdb/analysis.py
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
from base64 import b64encode
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from flask_login import current_user
|
||||||
|
from matplotlib.figure import Figure
|
||||||
|
from matplotlib.ticker import MaxNLocator
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from .models import MAX_MARK, MIN_MARK, Assistant, Semester, User, db
|
||||||
|
|
||||||
|
|
||||||
|
def html_fig(fig):
|
||||||
|
buf = BytesIO()
|
||||||
|
fig.savefig(buf, format="png")
|
||||||
|
|
||||||
|
return b64encode(buf.getbuffer()).decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def mark_hist(data, title):
|
||||||
|
fig = Figure()
|
||||||
|
ax = fig.subplots()
|
||||||
|
ax.set_xlim(MIN_MARK - 0.5, MAX_MARK + 0.5)
|
||||||
|
ax.set_xticks(np.arange(MAX_MARK + 1))
|
||||||
|
# Only integer ticks
|
||||||
|
ax.yaxis.set_major_locator(MaxNLocator(integer=True))
|
||||||
|
ax.set_xlabel("Mark")
|
||||||
|
|
||||||
|
N = data.size
|
||||||
|
title += f"\nN = {N}"
|
||||||
|
|
||||||
|
if N > 0:
|
||||||
|
ax.hist(
|
||||||
|
data,
|
||||||
|
bins=np.arange(MAX_MARK + 2) - 0.5,
|
||||||
|
)
|
||||||
|
title += f" | mean = {round(np.mean(data), 1)}"
|
||||||
|
|
||||||
|
ax.set_title(title)
|
||||||
|
|
||||||
|
return html_fig(fig)
|
||||||
|
|
||||||
|
|
||||||
|
def get_experiment_marks(assistant, attr):
|
||||||
|
data = []
|
||||||
|
|
||||||
|
for experiment_mark in assistant.experiment_marks:
|
||||||
|
mark = getattr(experiment_mark, attr)
|
||||||
|
if mark is not None:
|
||||||
|
data.append(mark)
|
||||||
|
|
||||||
|
return np.array(data)
|
||||||
|
|
||||||
|
|
||||||
|
def mark_hists(markType, active_assistants):
|
||||||
|
attr = markType.lower() + "_mark"
|
||||||
|
mark_type_title_addition = f" | {markType} marks"
|
||||||
|
marks = [get_experiment_marks(assistant, attr) for assistant in active_assistants]
|
||||||
|
|
||||||
|
hists = [
|
||||||
|
mark_hist(
|
||||||
|
data=marks[i],
|
||||||
|
title=str(active_assistants[i]) + mark_type_title_addition,
|
||||||
|
)
|
||||||
|
for i in range(len(marks))
|
||||||
|
]
|
||||||
|
|
||||||
|
hists.append(
|
||||||
|
mark_hist(
|
||||||
|
data=np.hstack(marks),
|
||||||
|
title="All" + mark_type_title_addition,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return hists
|
||||||
|
|
||||||
|
|
||||||
|
def assistant_marks_analysis(cls):
|
||||||
|
active_assistants = db.session.scalars(select(Assistant).join(User).where(User.active == True)).all()
|
||||||
|
|
||||||
|
oral_mark_hists = mark_hists("Oral", active_assistants)
|
||||||
|
protocol_mark_hists = mark_hists("Protocol", active_assistants)
|
||||||
|
|
||||||
|
return cls.render(
|
||||||
|
"analysis/assistant_marks.jinja.html",
|
||||||
|
hist_indices=range(len(oral_mark_hists)),
|
||||||
|
oral_mark_hists=oral_mark_hists,
|
||||||
|
protocol_mark_hists=protocol_mark_hists,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_final_part_marks(part):
|
||||||
|
data = []
|
||||||
|
|
||||||
|
for part_student in part.part_students:
|
||||||
|
mark = part_student.final_part_mark
|
||||||
|
if mark is not None:
|
||||||
|
data.append(mark)
|
||||||
|
|
||||||
|
return np.array(data)
|
||||||
|
|
||||||
|
|
||||||
|
def final_part_marks_analysis(cls):
|
||||||
|
parts = current_user.active_semester.parts
|
||||||
|
|
||||||
|
active_semester_final_part_marks_hists = [
|
||||||
|
mark_hist(
|
||||||
|
data=get_final_part_marks(part),
|
||||||
|
title=part.str(),
|
||||||
|
)
|
||||||
|
for part in parts
|
||||||
|
]
|
||||||
|
|
||||||
|
semesters = db.session.scalars(select(Semester)).all()
|
||||||
|
mean_final_part_marks = np.array(
|
||||||
|
[np.mean(np.hstack([get_final_part_marks(part) for part in semester.parts])) for semester in semesters]
|
||||||
|
)
|
||||||
|
|
||||||
|
fig = Figure()
|
||||||
|
len_mean_final_part_marks = mean_final_part_marks.size
|
||||||
|
|
||||||
|
ax = fig.subplots()
|
||||||
|
x = range(len_mean_final_part_marks)
|
||||||
|
ax.plot(
|
||||||
|
x,
|
||||||
|
mean_final_part_marks,
|
||||||
|
marker="d",
|
||||||
|
)
|
||||||
|
ax.set_xticks(x, [semester.str() for semester in semesters])
|
||||||
|
|
||||||
|
ax.set_xlabel("Semester")
|
||||||
|
ax.set_ylabel("Mean final experiment mark")
|
||||||
|
|
||||||
|
ax.set_title("Mean final experiment mark over all semesters")
|
||||||
|
|
||||||
|
mean_final_part_mark_plot = html_fig(fig)
|
||||||
|
|
||||||
|
return cls.render(
|
||||||
|
"analysis/final_part_marks.jinja.html",
|
||||||
|
active_semester_final_part_marks_hists=active_semester_final_part_marks_hists,
|
||||||
|
mean_final_part_mark_plot=mean_final_part_mark_plot,
|
||||||
|
)
|
|
@ -7,9 +7,14 @@
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
{{ form.csrf_token }}
|
{% for field in form %}
|
||||||
{{ form.assistantMarksSubmit }}
|
{% if field.widget.input_type == "submit" %}
|
||||||
<hr>
|
{{ field(class="btn btn-primary btn-block") }}
|
||||||
{{ form.finalPartMarksSubmit }}
|
|
||||||
|
<br>
|
||||||
|
{% else %}
|
||||||
|
{{ field() }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
</form>
|
</form>
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
|
@ -6,17 +6,36 @@
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
<h2>Assistant's marks analysis</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
This page shows an analysis of all marks of all assistants with an active user.
|
The histograms on this page (except the last two histograms) show the oral and protocol marks of each active assistant <em>individually</em>.
|
||||||
<br>
|
</p>
|
||||||
The marks are from all semesters, not only the active semester.
|
<p>
|
||||||
|
The last two histograms show the oral and protocol marks of all active assistants <em>together</em>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
An active assistant is an assistant with an active user. The marks are from all semesters, not only from the active semester.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h4>Export</h4>
|
||||||
|
<p>
|
||||||
|
You can export this analysis by printing this page to a PDF file. The shortcut for printing the page is normally <code>Ctrl + p</code>. Select "Save as (PDF) file" afterwards instead of a printer!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
{% for histInd in histIndices %}
|
{% for hist_ind in hist_indices %}
|
||||||
<img src="data:image/png;base64,{{ oralMarkHists[histInd]}}">
|
<div class="row text-center">
|
||||||
<img src="data:image/png;base64,{{ protocolMarkHists[histInd]}}">
|
<div class="col-sm">
|
||||||
|
<img class="img-fluid" src="data:image/png;base64,{{ oral_mark_hists[hist_ind]}}">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<img class="img-fluid" src="data:image/png;base64,{{ protocol_mark_hists[hist_ind]}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
|
@ -6,10 +6,32 @@
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
{% for activeSemesterFinalPartMarksHist in activeSemesterFinalPartMarksHists %}
|
<h2>Final part marks analysis</h2>
|
||||||
<img src="data:image/png;base64,{{ activeSemesterFinalPartMarksHist }}">
|
|
||||||
|
<p>
|
||||||
|
The histograms on this page show the final experiment marks of each part in the active semester.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The plot at the end of the page shows the course of the mean value of the final experiment marks in all parts of each semester over all semesters.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Export</h4>
|
||||||
|
<p>
|
||||||
|
You can export this analysis by printing this page to a PDF file. The shortcut for printing the page is normally <code>Ctrl + p</code>. Select "Save as (PDF) file" afterwards instead of a printer!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
{% for active_semester_final_part_marks_hist in active_semester_final_part_marks_hists %}
|
||||||
|
<img class="img-fluid" src="data:image/png;base64,{{ active_semester_final_part_marks_hist }}">
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<img src="data:image/png;base64,{{ meanFinalPartMarksPlot }}">
|
<img class="img-fluid" src="data:image/png;base64,{{ mean_final_part_mark_plot }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
Loading…
Reference in a new issue