Skip to content

Conversation

@fidoriel
Copy link
Collaborator

closes #2001

@fidoriel fidoriel force-pushed the average-grades-in-export branch 5 times, most recently from af9697b to 382680b Compare January 13, 2025 20:13
@fidoriel fidoriel force-pushed the average-grades-in-export branch from dced350 to 6163186 Compare January 27, 2025 18:58
Copy link
Member

@niklasmohrin niklasmohrin left a 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

@richardebeling richardebeling self-requested a review February 17, 2025 19:11

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,
Copy link
Member

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.

Copy link
Member

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)

richardebeling
richardebeling previously approved these changes Apr 14, 2025
@fidoriel fidoriel force-pushed the average-grades-in-export branch from 84a538a to eb96a66 Compare May 19, 2025 16:17
janno42
janno42 previously approved these changes Jul 14, 2025
Copy link
Member

@janno42 janno42 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📊

Copy link
Member

@niklasmohrin niklasmohrin left a 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,
Copy link
Member

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

Comment on lines +118 to +124
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,
Copy link
Member

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 ?)

Copy link
Member

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.

Copy link
Member

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

Comment on lines +415 to 417
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):
Copy link
Member

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

Copy link
Member

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.

Comment on lines +296 to +297
def _get_average_grade_and_approval_ratio(
cls, questionnaire_id: int, question: Question, results: OrderedDict[int, list[QuestionResult]]
Copy link
Member

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
Copy link
Member

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")
Copy link
Member

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(
Copy link
Member

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):
Copy link
Member

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:

  1. evaluations from previous semesters are included
  2. evaluations from other programs and course types are included
  3. only published evaluations are included
    a. even if the unpublished evaluations are part of the export
  4. which contributions are used
  5. we are averaging over the averages of several results
  6. 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

@richardebeling richardebeling removed their request for review August 4, 2025 15:27
@janno42 janno42 self-requested a review September 1, 2025 14:31
@niklasmohrin niklasmohrin marked this pull request as draft September 21, 2025 19:19
Co-authored-by: Niklas Mohrin <dev@niklasmohrin.de>
fidoriel and others added 6 commits October 20, 2025 19:25
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Add averages to results exports

5 participants