Merge branch 'wei' into 'beta'

[WEI] Correction de l'algorithme et tests unitaires

See merge request bde/nk20!173
This commit is contained in:
ynerant 2021-09-02 18:53:21 +00:00
commit 7b809ff3a6
5 changed files with 119 additions and 11 deletions

View File

@ -53,7 +53,8 @@ class WEIBusInformation:
def free_seats(self, surveys: List["WEISurvey"] = None): def free_seats(self, surveys: List["WEISurvey"] = None):
size = self.bus.size size = self.bus.size
already_occupied = WEIMembership.objects.filter(bus=self.bus).count() already_occupied = WEIMembership.objects.filter(bus=self.bus).count()
valid_surveys = sum(1 for survey in surveys if survey.information.valid) if surveys else 0 valid_surveys = sum(1 for survey in surveys if survey.information.valid
and survey.information.get_selected_bus() == self.bus) if surveys else 0
return size - already_occupied - valid_surveys return size - already_occupied - valid_surveys
def has_free_seats(self, surveys=None): def has_free_seats(self, surveys=None):

View File

@ -16,7 +16,7 @@ WORDS = [
'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno', 'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno',
'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit', 'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit',
'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic', 'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic',
'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi', 'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi',
'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane', 'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane',
] ]
@ -45,9 +45,9 @@ class WEISurveyForm2021(forms.Form):
rng = Random(information.seed) rng = Random(information.seed)
words = [] words = []
for _ in range(information.step + 1): for _ignored in range(information.step + 1):
# Generate N times words # Generate N times words
words = [rng.choice(WORDS) for _ in range(10)] words = [rng.choice(WORDS) for _ignored2 in range(10)]
words = [(w, w) for w in words] words = [(w, w) for w in words]
if self.data: if self.data:
self.fields["word"].choices = [(w, w) for w in WORDS] self.fields["word"].choices = [(w, w) for w in WORDS]
@ -162,7 +162,7 @@ class WEISurveyAlgorithm2021(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, _ in buses: for bus, _ignored in buses:
if self.get_bus_information(bus).has_free_seats(surveys): if self.get_bus_information(bus).has_free_seats(surveys):
# 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)
@ -190,6 +190,9 @@ class WEISurveyAlgorithm2021(WEISurveyAlgorithm):
# If it does not exist, choose the next bus. # If it does not exist, choose the next bus.
least_preferred_survey.free() least_preferred_survey.free()
least_preferred_survey.save() least_preferred_survey.save()
free_surveys.append(least_preferred_survey)
survey.select_bus(bus) survey.select_bus(bus)
survey.save() survey.save()
break break
else:
raise ValueError(f"User {survey.registration.user} has no free seat")

View File

@ -5,7 +5,7 @@ from argparse import ArgumentParser, FileType
from django.core.management import BaseCommand from django.core.management import BaseCommand
from django.db import transaction from django.db import transaction
from wei.forms import CurrentSurvey from ...forms import CurrentSurvey
class Command(BaseCommand): class Command(BaseCommand):

View File

@ -0,0 +1,109 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import random
from django.contrib.auth.models import User
from django.test import TestCase
from ..forms.surveys.wei2021 import WEIBusInformation2021, WEISurvey2021, WORDS, WEISurveyInformation2021
from ..models import Bus, WEIClub, WEIRegistration
class TestWEIAlgorithm(TestCase):
"""
Run some tests to ensure that the WEI algorithm is working well.
"""
fixtures = ('initial',)
def setUp(self):
"""
Create some test data, with one WEI and 10 buses with random score attributions.
"""
self.wei = WEIClub.objects.create(
name="WEI 2021",
email="wei2021@example.com",
date_start='2021-09-17',
date_end='2021-09-19',
)
self.buses = []
for i in range(10):
bus = Bus.objects.create(wei=self.wei, name=f"Bus {i}", size=10)
self.buses.append(bus)
information = WEIBusInformation2021(bus)
for word in WORDS:
information.scores[word] = random.randint(0, 101)
information.save()
bus.save()
def test_survey_algorithm_small(self):
"""
There are only a few people in each bus, ensure that each person has its best bus
"""
# Add a few users
for i in range(10):
user = User.objects.create(username=f"user{i}")
registration = WEIRegistration.objects.create(
user=user,
wei=self.wei,
first_year=True,
birth_date='2000-01-01',
)
information = WEISurveyInformation2021(registration)
for j in range(1, 21):
setattr(information, f'word{j}', random.choice(WORDS))
information.step = 20
information.save(registration)
registration.save()
# Run algorithm
WEISurvey2021.get_algorithm_class()().run_algorithm()
# Ensure that everyone has its first choice
for r in WEIRegistration.objects.filter(wei=self.wei).all():
survey = WEISurvey2021(r)
preferred_bus = survey.ordered_buses()[0][0]
chosen_bus = survey.information.get_selected_bus()
self.assertEqual(preferred_bus, chosen_bus)
def test_survey_algorithm_full(self):
"""
Buses are full of first year people, ensure that they are happy
"""
# Add a lot of users
for i in range(95):
user = User.objects.create(username=f"user{i}")
registration = WEIRegistration.objects.create(
user=user,
wei=self.wei,
first_year=True,
birth_date='2000-01-01',
)
information = WEISurveyInformation2021(registration)
for j in range(1, 21):
setattr(information, f'word{j}', random.choice(WORDS))
information.step = 20
information.save(registration)
registration.save()
# Run algorithm
WEISurvey2021.get_algorithm_class()().run_algorithm()
penalty = 0
# Ensure that everyone seems to be happy
# We attribute a penalty for each user that didn't have its first choice
# The penalty is the square of the distance between the score of the preferred bus
# and the score of the attributed bus
# We consider it acceptable if the mean of this distance is lower than 5 %
for r in WEIRegistration.objects.filter(wei=self.wei).all():
survey = WEISurvey2021(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]
penalty += (max_score - score) ** 2
self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance
self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %

View File

@ -3,7 +3,6 @@
import subprocess import subprocess
from datetime import timedelta, date from datetime import timedelta, date
from unittest import skip
from api.tests import TestAPI from api.tests import TestAPI
from django.conf import settings from django.conf import settings
@ -813,10 +812,6 @@ class TestWEISurveyAlgorithm(TestCase):
) )
CurrentSurvey(self.registration).save() CurrentSurvey(self.registration).save()
@skip # FIXME Write good unit tests
def test_survey_algorithm(self):
CurrentSurvey.get_algorithm_class()().run_algorithm()
class TestWeiAPI(TestAPI): class TestWeiAPI(TestAPI):
def setUp(self) -> None: def setUp(self) -> None: