mirror of
https://codeberg.org/Mo8it/AdvLabDB.git
synced 2024-12-04 22:40:30 +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
|
||||
|
||||
import numpy as np
|
||||
from flask import flash, has_request_context, redirect
|
||||
from flask_admin import Admin as FlaskAdmin
|
||||
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_wtf import FlaskForm
|
||||
from flask_wtf.file import FileAllowed, FileField, FileRequired
|
||||
from matplotlib.figure import Figure
|
||||
from sqlalchemy import and_, not_, or_, select
|
||||
from werkzeug.utils import secure_filename
|
||||
from wtforms.fields import (
|
||||
|
@ -60,6 +56,7 @@ from .advlabdb_independent_funs import (
|
|||
flashRandomPassword,
|
||||
str_without_semester_formatter,
|
||||
)
|
||||
from .analysis import assistant_marks_analysis, final_part_marks_analysis
|
||||
from .custom_classes import (
|
||||
SecureAdminBaseView,
|
||||
SecureAdminIndexView,
|
||||
|
@ -1372,154 +1369,22 @@ class ActionsView(SecureAdminBaseView):
|
|||
|
||||
class AnalysisView(SecureAdminBaseView):
|
||||
class AnalysisForm(FlaskForm):
|
||||
assistantMarksSubmit = SubmitField(
|
||||
label="Assistant's marks",
|
||||
assistant_marks_submit = SubmitField(
|
||||
label="Active assistant's marks in all semesters",
|
||||
)
|
||||
finalPartMarksSubmit = SubmitField(
|
||||
label="Final part marks",
|
||||
final_part_marks_submit = SubmitField(
|
||||
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"))
|
||||
def index(self):
|
||||
form = AnalysisView.AnalysisForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.assistantMarksSubmit.data:
|
||||
activeAssistants = assistantQueryFactory()
|
||||
|
||||
oralMarkHists = AnalysisView.markHists("Oral", activeAssistants)
|
||||
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,
|
||||
)
|
||||
if form.assistant_marks_submit.data:
|
||||
return assistant_marks_analysis(self)
|
||||
elif form.final_part_marks_submit.data:
|
||||
return final_part_marks_analysis(self)
|
||||
|
||||
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>
|
||||
|
||||
<form method="POST">
|
||||
{{ form.csrf_token }}
|
||||
{{ form.assistantMarksSubmit }}
|
||||
<hr>
|
||||
{{ form.finalPartMarksSubmit }}
|
||||
{% for field in form %}
|
||||
{% if field.widget.input_type == "submit" %}
|
||||
{{ field(class="btn btn-primary btn-block") }}
|
||||
|
||||
<br>
|
||||
{% else %}
|
||||
{{ field() }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</form>
|
||||
{% endblock body %}
|
||||
|
|
|
@ -6,17 +6,36 @@
|
|||
|
||||
<hr>
|
||||
|
||||
<h2>Assistant's marks analysis</h2>
|
||||
|
||||
<p>
|
||||
This page shows an analysis of all marks of all assistants with an active user.
|
||||
<br>
|
||||
The marks are from all semesters, not only the active semester.
|
||||
The histograms on this page (except the last two histograms) show the oral and protocol marks of each active assistant <em>individually</em>.
|
||||
</p>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
{% for histInd in histIndices %}
|
||||
<img src="data:image/png;base64,{{ oralMarkHists[histInd]}}">
|
||||
<img src="data:image/png;base64,{{ protocolMarkHists[histInd]}}">
|
||||
{% for hist_ind in hist_indices %}
|
||||
<div class="row text-center">
|
||||
<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>
|
||||
{% endfor %}
|
||||
{% endblock body %}
|
||||
|
|
|
@ -6,10 +6,32 @@
|
|||
|
||||
<hr>
|
||||
|
||||
{% for activeSemesterFinalPartMarksHist in activeSemesterFinalPartMarksHists %}
|
||||
<img src="data:image/png;base64,{{ activeSemesterFinalPartMarksHist }}">
|
||||
<hr>
|
||||
{% endfor %}
|
||||
<h2>Final part marks analysis</h2>
|
||||
|
||||
<img src="data:image/png;base64,{{ meanFinalPartMarksPlot }}">
|
||||
<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>
|
||||
{% endfor %}
|
||||
|
||||
<img class="img-fluid" src="data:image/png;base64,{{ mean_final_part_mark_plot }}">
|
||||
</div>
|
||||
|
||||
<br>
|
||||
{% endblock body %}
|
||||
|
|
Loading…
Reference in a new issue