1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-08-02 21:54:24 +02:00

Algorithm changed again

This commit is contained in:
Ehouarn
2025-08-01 11:56:34 +02:00
parent 97597eb103
commit d50bb2134a
2 changed files with 79 additions and 59 deletions

View File

@@ -30,117 +30,117 @@ WORDS = {
'Description 1', 'Description 1',
{ {
3: 'Réponse 1 Madagas[car]', 3: 'Réponse 1 Madagas[car]',
43: 'Réponse 1 Y2[KAR]', 4: 'Réponse 1 Y2[KAR]',
2: 'Réponse 1 Tcherno[bus]', 2: 'Réponse 1 Tcherno[bus]',
45: 'Réponse 1 [Kar]tier', 5: 'Réponse 1 [Kar]tier',
1: 'Réponse 1 [Car]cassonne', 1: 'Réponse 1 [Car]cassonne',
47: 'Réponse 1 O[car]ina', 6: 'Réponse 1 O[car]ina',
48: 'Réponse 1 Show[bus]', 7: 'Réponse 1 Show[bus]',
49: 'Réponse 1 [Car]ioca' 8: 'Réponse 1 [Car]ioca'
} }
], ],
'Question 2': [ 'Question 2': [
'Description 2', 'Description 2',
{ {
3: 'Réponse 2 Madagas[car]', 3: 'Réponse 2 Madagas[car]',
43: 'Réponse 2 Y2[KAR]', 4: 'Réponse 2 Y2[KAR]',
2: 'Réponse 2 Tcherno[bus]', 2: 'Réponse 2 Tcherno[bus]',
45: 'Réponse 2 [Kar]tier', 5: 'Réponse 2 [Kar]tier',
1: 'Réponse 2 [Car]cassonne', 1: 'Réponse 2 [Car]cassonne',
47: 'Réponse 2 O[car]ina', 6: 'Réponse 2 O[car]ina',
48: 'Réponse 2 Show[bus]', 7: 'Réponse 2 Show[bus]',
49: 'Réponse 2 [Car]ioca' 8: 'Réponse 2 [Car]ioca'
} }
], ],
'Question 3': [ 'Question 3': [
'Description 3', 'Description 3',
{ {
3: 'Réponse 3 Madagas[car]', 3: 'Réponse 3 Madagas[car]',
43: 'Réponse 3 Y2[KAR]', 4: 'Réponse 3 Y2[KAR]',
2: 'Réponse 3 Tcherno[bus]', 2: 'Réponse 3 Tcherno[bus]',
45: 'Réponse 3 [Kar]tier', 5: 'Réponse 3 [Kar]tier',
1: 'Réponse 3 [Car]cassonne', 1: 'Réponse 3 [Car]cassonne',
47: 'Réponse 3 O[car]ina', 6: 'Réponse 3 O[car]ina',
48: 'Réponse 3 Show[bus]', 7: 'Réponse 3 Show[bus]',
49: 'Réponse 3 [Car]ioca' 8: 'Réponse 3 [Car]ioca'
} }
], ],
'Question 4': [ 'Question 4': [
'Description 4', 'Description 4',
{ {
3: 'Réponse 4 Madagas[car]', 3: 'Réponse 4 Madagas[car]',
43: 'Réponse 4 Y2[KAR]', 4: 'Réponse 4 Y2[KAR]',
2: 'Réponse 4 Tcherno[bus]', 2: 'Réponse 4 Tcherno[bus]',
45: 'Réponse 4 [Kar]tier', 5: 'Réponse 4 [Kar]tier',
1: 'Réponse 4 [Car]cassonne', 1: 'Réponse 4 [Car]cassonne',
47: 'Réponse 4 O[car]ina', 6: 'Réponse 4 O[car]ina',
48: 'Réponse 4 Show[bus]', 7: 'Réponse 4 Show[bus]',
49: 'Réponse 4 [Car]ioca' 8: 'Réponse 4 [Car]ioca'
} }
], ],
'Question 5': [ 'Question 5': [
'Description 5', 'Description 5',
{ {
3: 'Réponse 5 Madagas[car]', 3: 'Réponse 5 Madagas[car]',
43: 'Réponse 5 Y2[KAR]', 4: 'Réponse 5 Y2[KAR]',
2: 'Réponse 5 Tcherno[bus]', 2: 'Réponse 5 Tcherno[bus]',
45: 'Réponse 5 [Kar]tier', 5: 'Réponse 5 [Kar]tier',
1: 'Réponse 5 [Car]cassonne', 1: 'Réponse 5 [Car]cassonne',
47: 'Réponse 5 O[car]ina', 6: 'Réponse 5 O[car]ina',
48: 'Réponse 5 Show[bus]', 7: 'Réponse 5 Show[bus]',
49: 'Réponse 5 [Car]ioca' 8: 'Réponse 5 [Car]ioca'
} }
], ],
'Question 6': [ 'Question 6': [
'Description 6', 'Description 6',
{ {
3: 'Réponse 6 Madagas[car]', 3: 'Réponse 6 Madagas[car]',
43: 'Réponse 6 Y2[KAR]', 4: 'Réponse 6 Y2[KAR]',
2: 'Réponse 6 Tcherno[bus]', 2: 'Réponse 6 Tcherno[bus]',
45: 'Réponse 6 [Kar]tier', 5: 'Réponse 6 [Kar]tier',
1: 'Réponse 6 [Car]cassonne', 1: 'Réponse 6 [Car]cassonne',
47: 'Réponse 6 O[car]ina', 6: 'Réponse 6 O[car]ina',
48: 'Réponse 6 Show[bus]', 7: 'Réponse 6 Show[bus]',
49: 'Réponse 6 [Car]ioca' 8: 'Réponse 6 [Car]ioca'
} }
], ],
'Question 7': [ 'Question 7': [
'Description 7', 'Description 7',
{ {
3: 'Réponse 7 Madagas[car]', 3: 'Réponse 7 Madagas[car]',
43: 'Réponse 7 Y2[KAR]', 4: 'Réponse 7 Y2[KAR]',
2: 'Réponse 7 Tcherno[bus]', 2: 'Réponse 7 Tcherno[bus]',
45: 'Réponse 7 [Kar]tier', 5: 'Réponse 7 [Kar]tier',
1: 'Réponse 7 [Car]cassonne', 1: 'Réponse 7 [Car]cassonne',
47: 'Réponse 7 O[car]ina', 6: 'Réponse 7 O[car]ina',
48: 'Réponse 7 Show[bus]', 7: 'Réponse 7 Show[bus]',
49: 'Réponse 7 [Car]ioca' 8: 'Réponse 7 [Car]ioca'
} }
], ],
'Question 8': [ 'Question 8': [
'Description 8', 'Description 8',
{ {
3: 'Réponse 8 Madagas[car]', 3: 'Réponse 8 Madagas[car]',
43: 'Réponse 8 Y2[KAR]', 4: 'Réponse 8 Y2[KAR]',
2: 'Réponse 8 Tcherno[bus]', 2: 'Réponse 8 Tcherno[bus]',
45: 'Réponse 8 [Kar]tier', 5: 'Réponse 8 [Kar]tier',
1: 'Réponse 8 [Car]cassonne', 1: 'Réponse 8 [Car]cassonne',
47: 'Réponse 8 O[car]ina', 6: 'Réponse 8 O[car]ina',
48: 'Réponse 8 Show[bus]', 7: 'Réponse 8 Show[bus]',
49: 'Réponse 8 [Car]ioca' 8: 'Réponse 8 [Car]ioca'
} }
], ],
'Question 9': [ 'Question 9': [
'Description 9', 'Description 9',
{ {
3: 'Réponse 9 Madagas[car]', 3: 'Réponse 9 Madagas[car]',
43: 'Réponse 9 Y2[KAR]', 4: 'Réponse 9 Y2[KAR]',
2: 'Réponse 9 Tcherno[bus]', 2: 'Réponse 9 Tcherno[bus]',
45: 'Réponse 9 [Kar]tier', 5: 'Réponse 9 [Kar]tier',
1: 'Réponse 9 [Car]cassonne', 1: 'Réponse 9 [Car]cassonne',
47: 'Réponse 9 O[car]ina', 6: 'Réponse 9 O[car]ina',
48: 'Réponse 9 Show[bus]', 7: 'Réponse 9 Show[bus]',
49: 'Réponse 9 [Car]ioca' 8: 'Réponse 9 [Car]ioca'
} }
] ]
} }
@@ -366,24 +366,41 @@ class WEISurvey2025(WEISurvey):
@lru_cache() @lru_cache()
def score(self, bus): 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(): if not self.is_complete():
raise ValueError("Survey is not ended, can't calculate score") raise ValueError("Survey is not ended, can't calculate score")
bus_info = self.get_algorithm_class().get_bus_information(bus) 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. # 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))] 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 - self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 1 + NB_WORDS))
s += sum(1 for q in WORDS['questions'] if getattr(self.information, q) == str(bus.pk))
return s return s
@lru_cache() @lru_cache()
def scores_per_bus(self): 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() @lru_cache()
def ordered_buses(self): 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 = 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 return values
@classmethod @classmethod
@@ -421,6 +438,7 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm):
""" """
Gale-Shapley algorithm implementation. Gale-Shapley algorithm implementation.
We modify it to allow buses to have multiple "weddings". 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 = 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 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 while free_surveys: # Some students are not affected
survey = free_surveys[0] survey = free_surveys[0]
buses = survey.ordered_buses() # Preferences of the student 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): if self.get_bus_information(bus).has_free_seats(surveys, quotas):
# Selected bus has free places. Put student in the bus # Selected bus has free places. Put student in the bus
survey.select_bus(bus) survey.select_bus(bus)
@@ -491,17 +509,17 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm):
else: else:
# Current bus has not enough places. Remove the least preferred student from the bus if existing # Current bus has not enough places. Remove the least preferred student from the bus if existing
least_preferred_survey = None 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 # Find the least student in the bus that has a lower score than the current student
for survey2 in surveys: for survey2 in surveys:
if not survey2.information.valid or survey2.information.get_selected_bus() != bus: if not survey2.information.valid or survey2.information.get_selected_bus() != bus:
continue continue
score2 = survey2.score(bus) scores2 = survey2.score(bus), survey2.score_words(bus)
if current_score <= score2: # Ignore better students if current_scores <= scores2: # Ignore better students
continue 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_preferred_survey = survey2
least_score = score2 least_scores = scores2
if least_preferred_survey is not None: if least_preferred_survey is not None:
# Remove the least student from the bus and put the current student in. # Remove the least student from the bus and put the current student in.

View File

@@ -30,7 +30,7 @@ class TestWEIAlgorithm(TestCase):
) )
self.buses = [] 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) bus = Bus.objects.create(wei=self.wei, name=f"Bus {i}", size=10)
self.buses.append(bus) self.buses.append(bus)
information = WEIBusInformation2025(bus) information = WEIBusInformation2025(bus)
@@ -74,7 +74,7 @@ class TestWEIAlgorithm(TestCase):
Buses are full of first year people, ensure that they are happy Buses are full of first year people, ensure that they are happy
""" """
# Add a lot of users # Add a lot of users
for i in range(95): for i in range(80):
user = User.objects.create(username=f"user{i}") user = User.objects.create(username=f"user{i}")
registration = WEIRegistration.objects.create( registration = WEIRegistration.objects.create(
user=user, user=user,
@@ -90,6 +90,7 @@ class TestWEIAlgorithm(TestCase):
information.step = len(WORDS['questions']) + 1 information.step = len(WORDS['questions']) + 1
information.save(registration) information.save(registration)
registration.save() registration.save()
survey = WEISurvey2025(registration)
# Run algorithm # Run algorithm
WEISurvey2025.get_algorithm_class()().run_algorithm() WEISurvey2025.get_algorithm_class()().run_algorithm()
@@ -104,8 +105,9 @@ class TestWEIAlgorithm(TestCase):
survey = WEISurvey2025(r) survey = WEISurvey2025(r)
chosen_bus = survey.information.get_selected_bus() chosen_bus = survey.information.get_selected_bus()
buses = survey.ordered_buses() buses = survey.ordered_buses()
score = min(v for bus, v in buses if bus == chosen_bus) self.assertIn(chosen_bus, [x[0] for x in buses])
max_score = buses[0][1] score = min(v for bus, (v, __) in buses if bus == chosen_bus)
max_score = buses[0][1][0]
penalty += (max_score - score) ** 2 penalty += (max_score - score) ** 2
self.assertLessEqual(max_score - score, 1) # Always less than 25 % of tolerance self.assertLessEqual(max_score - score, 1) # Always less than 25 % of tolerance