-
Notifications
You must be signed in to change notification settings - Fork 165
Average grades in export #2366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Average grades in export #2366
Conversation
af9697b to
382680b
Compare
dced350 to
6163186
Compare
niklasmohrin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems mostly correct, but I think we should make an effort to keep the structure of these methods clear
evap/results/tests/test_exporters.py
Outdated
|
|
||
| self.assertEqual( | ||
| float(workbook.sheets()[0].row_values(5)[1]), | ||
| (float(workbook.sheets()[0].row_values(5)[2]) + float(workbook.sheets()[0].row_values(5)[3])) / 2, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From a quick glance, it looks like the setup above creates the same exact average result for both evaluations and same count of answers inside of them. This doesn't test whether we compute the average-of-averages with correct weighting. Can we change them to have different answer counts?
Coincidentally, float-equality here made me wonder about correctness. It should work if the two values here are identical, the sum is then 2 * the original value, which is divisible by 2 to perfectly recreate the original value. However, I'd like the test to test a somewhat more sophisticated case. We'll likely need to use assertAlmostEqual then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fidoriel This currently compares a value the system produces against another value that the system produces, which leaves the chance that both might be wrong, but the test still passes.
For a bit more assertiveness, I would appreciate if we could change the assertion here to the exact string that should be shown in the output, optimally in the string-domain (without to-number conversion)
84a538a to
eb96a66
Compare
janno42
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📊
niklasmohrin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The output looks correct, but I think we need to spend some more time on the code and testing, otherwise it will become hard to maintain the exporter
| class ExcelExporter(ABC): | ||
| styles = { | ||
| "default": xlwt.Style.default_style, | ||
| "missing_average": xlwt.Style.default_style, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
both styles should be defined in the same place, so this should be moved to the other class
| def filter_evaluations( # noqa: PLR0912 | ||
| semesters: Iterable[Semester] | None = None, | ||
| evaluation_states: Iterable[Evaluation.State] | None = None, | ||
| program_ids: Iterable[int] | None = None, | ||
| course_type_ids: Iterable[int] | None = None, | ||
| contributor: UserProfile | None = None, | ||
| include_not_enough_voters: bool = False, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that any of these should have defaults, there are two callers and both of them know exactly what they want to filter for.
I was wondering if we could get away without None by passing in Semester.objects.all() which results in something like WHERE "evaluation_course"."semester_id" IN (SELECT "evaluation_semester"."id" FROM "evaluation_semester") - I guess the database should be able to see that this check can be avoided @richardebeling ? But having None is also fine for now, I just think that the caller should be explicit about this
For contributor and include_not_enough_voters, I am not even sure if the current version is correct - wouldn't we want to use answers across all contributors for the average? Currently, we only use answers to the general questionnaire. And I guess include_not_enough_voters should be true for the average (@janno42 ?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused. Aren't there average values for contributor questions in the current version of this PR? I thought so.
But yes, include_not_enough_voters should be True for the averages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I take from https://github.com/fidoriel/EvaP/blob/8a736d0713823104b69910edfbf23cfe49ab1ce4/evap/results/exporters.py#L415 is that even when contributor is not None we use the default of None for filter_evaluations
| all_evaluations_with_results, _, _ = self.filter_evaluations(evaluation_states=[Evaluation.State.PUBLISHED]) | ||
|
|
||
| for sheet_counter, (program_ids, course_type_ids) in enumerate(selection_list, 1): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to be sure, we want to have the same average in all sheets and not the average for the programs and course types of that sheet for every sheet? @janno42
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that you mention it... I think I wouldn't add this filter if we'd just have different course types, because their questions often differ. But for programs it might actually be important to have different averages. And then, when we're already at it, we can do the separation for both.
So yes, please calculate the averages for evaluations within the programs and course types of that sheet only.
| def _get_average_grade_and_approval_ratio( | ||
| cls, questionnaire_id: int, question: Question, results: OrderedDict[int, list[QuestionResult]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This uses questionnaire_id and results only once to do results[questionnaire_id], so we should just do that in the caller
| if grade_result.question.id != question.id or not RatingResult.has_answers(grade_result): | ||
| continue | ||
|
|
||
| value_sum += grade_result.average * grade_result.count_sum |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be fair to just ask the AnsweredRatingResult for the sum of grades instead of having it divided and then multiplied again; we can thus extract the summing out of its average method and make it available
| else: | ||
| self.write_cell(questionnaire.public_name, "bold") | ||
|
|
||
| self.write_cell("", "border_left_right") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can just be merged with the next line (where the comment is also not correct anymore)
| for question in self.filter_text_and_heading_questions(questionnaire.questions.all()): | ||
| self.write_cell(question.text, "italic" if question.is_heading_question else "default") | ||
|
|
||
| average_grade, approval_ratio = self._get_average_of_average_grade_and_approval_ratio( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
together with avg and average_approval_ratio below, it's not really clear what the difference between those four variables is; I think we can should use a clearer name
| self.assertEqual(sheet.row_values(5)[0], questions[1].text) | ||
| self.assertEqual(sheet.row_values(6)[0], questions[2].text) | ||
|
|
||
| def test_total_average(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this does not cover several important details:
- evaluations from previous semesters are included
- evaluations from other programs and course types are included
- only published evaluations are included
a. even if the unpublished evaluations are part of the export - which contributions are used
- we are averaging over the averages of several results
- are results with not enough voters used
So this test should be extended / more tests should be added. I think it is very easy to mess up some of these details in the future and when that happens, these tests should catch it
Co-authored-by: Niklas Mohrin <dev@niklasmohrin.de>
Co-authored-by: Niklas Mohrin <dev@niklasmohrin.de>
Co-authored-by: Niklas Mohrin <dev@niklasmohrin.de>
Co-authored-by: Niklas Mohrin <dev@niklasmohrin.de>
Co-authored-by: Niklas Mohrin <dev@niklasmohrin.de>
closes #2001