diff --git a/apps/wei/forms/surveys/wei2025.py b/apps/wei/forms/surveys/wei2025.py index ee748c6c..648a88c6 100644 --- a/apps/wei/forms/surveys/wei2025.py +++ b/apps/wei/forms/surveys/wei2025.py @@ -30,117 +30,117 @@ WORDS = { 'Description 1', { 3: 'Réponse 1 Madagas[car]', - 43: 'Réponse 1 Y2[KAR]', + 4: 'Réponse 1 Y2[KAR]', 2: 'Réponse 1 Tcherno[bus]', - 45: 'Réponse 1 [Kar]tier', + 5: 'Réponse 1 [Kar]tier', 1: 'Réponse 1 [Car]cassonne', - 47: 'Réponse 1 O[car]ina', - 48: 'Réponse 1 Show[bus]', - 49: 'Réponse 1 [Car]ioca' + 6: 'Réponse 1 O[car]ina', + 7: 'Réponse 1 Show[bus]', + 8: 'Réponse 1 [Car]ioca' } ], 'Question 2': [ 'Description 2', { 3: 'Réponse 2 Madagas[car]', - 43: 'Réponse 2 Y2[KAR]', + 4: 'Réponse 2 Y2[KAR]', 2: 'Réponse 2 Tcherno[bus]', - 45: 'Réponse 2 [Kar]tier', + 5: 'Réponse 2 [Kar]tier', 1: 'Réponse 2 [Car]cassonne', - 47: 'Réponse 2 O[car]ina', - 48: 'Réponse 2 Show[bus]', - 49: 'Réponse 2 [Car]ioca' + 6: 'Réponse 2 O[car]ina', + 7: 'Réponse 2 Show[bus]', + 8: 'Réponse 2 [Car]ioca' } ], 'Question 3': [ 'Description 3', { 3: 'Réponse 3 Madagas[car]', - 43: 'Réponse 3 Y2[KAR]', + 4: 'Réponse 3 Y2[KAR]', 2: 'Réponse 3 Tcherno[bus]', - 45: 'Réponse 3 [Kar]tier', + 5: 'Réponse 3 [Kar]tier', 1: 'Réponse 3 [Car]cassonne', - 47: 'Réponse 3 O[car]ina', - 48: 'Réponse 3 Show[bus]', - 49: 'Réponse 3 [Car]ioca' + 6: 'Réponse 3 O[car]ina', + 7: 'Réponse 3 Show[bus]', + 8: 'Réponse 3 [Car]ioca' } ], 'Question 4': [ 'Description 4', { 3: 'Réponse 4 Madagas[car]', - 43: 'Réponse 4 Y2[KAR]', + 4: 'Réponse 4 Y2[KAR]', 2: 'Réponse 4 Tcherno[bus]', - 45: 'Réponse 4 [Kar]tier', + 5: 'Réponse 4 [Kar]tier', 1: 'Réponse 4 [Car]cassonne', - 47: 'Réponse 4 O[car]ina', - 48: 'Réponse 4 Show[bus]', - 49: 'Réponse 4 [Car]ioca' + 6: 'Réponse 4 O[car]ina', + 7: 'Réponse 4 Show[bus]', + 8: 'Réponse 4 [Car]ioca' } ], 'Question 5': [ 'Description 5', { 3: 'Réponse 5 Madagas[car]', - 43: 'Réponse 5 Y2[KAR]', + 4: 'Réponse 5 Y2[KAR]', 2: 'Réponse 5 Tcherno[bus]', - 45: 'Réponse 5 [Kar]tier', + 5: 'Réponse 5 [Kar]tier', 1: 'Réponse 5 [Car]cassonne', - 47: 'Réponse 5 O[car]ina', - 48: 'Réponse 5 Show[bus]', - 49: 'Réponse 5 [Car]ioca' + 6: 'Réponse 5 O[car]ina', + 7: 'Réponse 5 Show[bus]', + 8: 'Réponse 5 [Car]ioca' } ], 'Question 6': [ 'Description 6', { 3: 'Réponse 6 Madagas[car]', - 43: 'Réponse 6 Y2[KAR]', + 4: 'Réponse 6 Y2[KAR]', 2: 'Réponse 6 Tcherno[bus]', - 45: 'Réponse 6 [Kar]tier', + 5: 'Réponse 6 [Kar]tier', 1: 'Réponse 6 [Car]cassonne', - 47: 'Réponse 6 O[car]ina', - 48: 'Réponse 6 Show[bus]', - 49: 'Réponse 6 [Car]ioca' + 6: 'Réponse 6 O[car]ina', + 7: 'Réponse 6 Show[bus]', + 8: 'Réponse 6 [Car]ioca' } ], 'Question 7': [ 'Description 7', { 3: 'Réponse 7 Madagas[car]', - 43: 'Réponse 7 Y2[KAR]', + 4: 'Réponse 7 Y2[KAR]', 2: 'Réponse 7 Tcherno[bus]', - 45: 'Réponse 7 [Kar]tier', + 5: 'Réponse 7 [Kar]tier', 1: 'Réponse 7 [Car]cassonne', - 47: 'Réponse 7 O[car]ina', - 48: 'Réponse 7 Show[bus]', - 49: 'Réponse 7 [Car]ioca' + 6: 'Réponse 7 O[car]ina', + 7: 'Réponse 7 Show[bus]', + 8: 'Réponse 7 [Car]ioca' } ], 'Question 8': [ 'Description 8', { 3: 'Réponse 8 Madagas[car]', - 43: 'Réponse 8 Y2[KAR]', + 4: 'Réponse 8 Y2[KAR]', 2: 'Réponse 8 Tcherno[bus]', - 45: 'Réponse 8 [Kar]tier', + 5: 'Réponse 8 [Kar]tier', 1: 'Réponse 8 [Car]cassonne', - 47: 'Réponse 8 O[car]ina', - 48: 'Réponse 8 Show[bus]', - 49: 'Réponse 8 [Car]ioca' + 6: 'Réponse 8 O[car]ina', + 7: 'Réponse 8 Show[bus]', + 8: 'Réponse 8 [Car]ioca' } ], 'Question 9': [ 'Description 9', { 3: 'Réponse 9 Madagas[car]', - 43: 'Réponse 9 Y2[KAR]', + 4: 'Réponse 9 Y2[KAR]', 2: 'Réponse 9 Tcherno[bus]', - 45: 'Réponse 9 [Kar]tier', + 5: 'Réponse 9 [Kar]tier', 1: 'Réponse 9 [Car]cassonne', - 47: 'Réponse 9 O[car]ina', - 48: 'Réponse 9 Show[bus]', - 49: 'Réponse 9 [Car]ioca' + 6: 'Réponse 9 O[car]ina', + 7: 'Réponse 9 Show[bus]', + 8: 'Réponse 9 [Car]ioca' } ] } @@ -366,24 +366,41 @@ class WEISurvey2025(WEISurvey): @lru_cache() def score(self, bus): + """ + The score given by the answers to the questions + """ + if not self.is_complete(): + raise ValueError("Survey is not ended, can't calculate score") + # Score is the given score by the bus subtracted to the mid-score of the buses. + s = sum(1 for q in WORDS['questions'] if getattr(self.information, q) == bus.pk) + return s + + @lru_cache() + def score_words(self, bus): + """ + The score given by the choice of words + """ if not self.is_complete(): raise ValueError("Survey is not ended, can't calculate score") bus_info = self.get_algorithm_class().get_bus_information(bus) # Score is the given score by the bus subtracted to the mid-score of the buses. s = sum(bus_info.scores[getattr(self.information, 'word' + str(i))] - - self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 1 + NB_WORDS)) / NB_WORDS - s += sum(1 for q in WORDS['questions'] if getattr(self.information, q) == str(bus.pk)) + - self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 1 + NB_WORDS)) return s @lru_cache() def scores_per_bus(self): - return {bus: self.score(bus) for bus in self.get_algorithm_class().get_buses()} + return {bus: (self.score(bus), self.score_words(bus)) for bus in self.get_algorithm_class().get_buses()} @lru_cache() def ordered_buses(self): + """ + Force the choice of bus to be in the 3 preferred buses according to the words + """ values = list(self.scores_per_bus().items()) - values.sort(key=lambda item: -item[1]) + values.sort(key=lambda item: -item[1][1]) + values = values[:3] return values @classmethod @@ -421,6 +438,7 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm): """ Gale-Shapley algorithm implementation. We modify it to allow buses to have multiple "weddings". + We use lexigographical order on both scores """ surveys = list(self.get_survey_class()(r) for r in self.get_registrations()) # All surveys surveys = [s for s in surveys if s.is_complete()] # Don't consider invalid surveys @@ -481,7 +499,7 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm): while free_surveys: # Some students are not affected survey = free_surveys[0] buses = survey.ordered_buses() # Preferences of the student - for bus, current_score in buses: + for bus, current_scores in buses: if self.get_bus_information(bus).has_free_seats(surveys, quotas): # Selected bus has free places. Put student in the bus survey.select_bus(bus) @@ -491,17 +509,17 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm): else: # Current bus has not enough places. Remove the least preferred student from the bus if existing least_preferred_survey = None - least_score = -1 + least_scores = (-1, -1) # Find the least student in the bus that has a lower score than the current student for survey2 in surveys: if not survey2.information.valid or survey2.information.get_selected_bus() != bus: continue - score2 = survey2.score(bus) - if current_score <= score2: # Ignore better students + scores2 = survey2.score(bus), survey2.score_words(bus) + if current_scores <= scores2: # Ignore better students continue - if least_preferred_survey is None or score2 < least_score: + if least_preferred_survey is None or scores2 < least_scores: least_preferred_survey = survey2 - least_score = score2 + least_scores = scores2 if least_preferred_survey is not None: # Remove the least student from the bus and put the current student in. diff --git a/apps/wei/tests/test_wei_algorithm_2025.py b/apps/wei/tests/test_wei_algorithm_2025.py index 4b5c91c4..571ba40d 100644 --- a/apps/wei/tests/test_wei_algorithm_2025.py +++ b/apps/wei/tests/test_wei_algorithm_2025.py @@ -30,7 +30,7 @@ class TestWEIAlgorithm(TestCase): ) self.buses = [] - for i in range(10): + for i in range(8): bus = Bus.objects.create(wei=self.wei, name=f"Bus {i}", size=10) self.buses.append(bus) information = WEIBusInformation2025(bus) @@ -74,7 +74,7 @@ class TestWEIAlgorithm(TestCase): Buses are full of first year people, ensure that they are happy """ # Add a lot of users - for i in range(95): + for i in range(80): user = User.objects.create(username=f"user{i}") registration = WEIRegistration.objects.create( user=user, @@ -90,6 +90,7 @@ class TestWEIAlgorithm(TestCase): information.step = len(WORDS['questions']) + 1 information.save(registration) registration.save() + survey = WEISurvey2025(registration) # Run algorithm WEISurvey2025.get_algorithm_class()().run_algorithm() @@ -104,8 +105,9 @@ class TestWEIAlgorithm(TestCase): survey = WEISurvey2025(r) chosen_bus = survey.information.get_selected_bus() buses = survey.ordered_buses() - score = min(v for bus, v in buses if bus == chosen_bus) - max_score = buses[0][1] + self.assertIn(chosen_bus, [x[0] for x in buses]) + score = min(v for bus, (v, __) in buses if bus == chosen_bus) + max_score = buses[0][1][0] penalty += (max_score - score) ** 2 self.assertLessEqual(max_score - score, 1) # Always less than 25 % of tolerance