[MJ] fix how MJ is computed

guhur-fix-ci
Pierre-Louis Guhur 4 years ago
parent 1f5147ae12
commit faa61a0459

@ -83,18 +83,15 @@ class VoteSerializer(serializers.ModelSerializer):
# See https://github.com/MieuxVoter/mvapi/pull/5#discussion_r291891403 for explanations
class Candidate:
def __init__(self, name, idx, profile, grade, score, num_votes):
def __init__(self, name, idx, profile, grade):
self.name = name
self.id = idx
self.score = score
self.profile = profile
self.grade = grade
self.num_votes = num_votes
class CandidateSerializer(serializers.Serializer):
name = serializers.CharField()
id = serializers.IntegerField(min_value=0)
score = serializers.FloatField(min_value=0, max_value=1)
profile = serializers.ListField(child=serializers.IntegerField())
profile = serializers.DictField(child=serializers.IntegerField())
grade = serializers.IntegerField(min_value=0, max_value=settings.MAX_NUM_GRADES)
num_votes = serializers.IntegerField(min_value=0)

@ -1,5 +1,5 @@
import os
from typing import Optional, Dict
from typing import Optional, Dict, Tuple, List
from time import time
from django.db import IntegrityError
from django.conf import settings
@ -27,6 +27,8 @@ UNKNOWN_TOKEN_ERROR = "E7: Wrong token"
USED_TOKEN_ERROR = "E8: Token already used"
WRONG_ELECTION_ERROR = "E9: Parameters for the election are incorrect"
# A Grade is always given a int
Grade = int
def send_mail_invitation(
email: str, election: str, token_id: int
@ -200,21 +202,25 @@ class ResultAPIView(APIView):
status=status.HTTP_400_BAD_REQUEST,
)
profiles, scores, grades, sorted_indexes = mj.compute_votes(
[v.grades_by_candidate for v in votes],
election.num_grades
votes: List[List[Grade]] = [v.grades_by_candidate for v in votes]
merit_profiles: List[Dict[Grade, int]] = mj.votes_to_merit_profiles(
votes, range(election.num_grades)
)
indexed_values: List[Tuple[int, mj.MajorityValue]] = mj.sort_by_value_with_index([
mj.MajorityValue(profil) for profil in merit_profiles
])
print(len(indexed_values))
candidates = [
serializers.Candidate(
election.candidates[idx],
idx,
profiles[idx],
grades[idx],
scores[idx],
len(votes)
merit_profiles[idx],
value.grade,
)
for idx in sorted_indexes
# for idx in sorted_indexes
for idx, value in indexed_values
]
serializer = serializers.CandidateSerializer(candidates, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)

@ -1,67 +1,14 @@
from typing import List
from typing import List, Tuple, Dict
from dataclasses import dataclass, field
import math
def majority_grade(scores: List[int]) -> int:
mid = math.ceil(sum(scores)/2.0)
acc = 0
for i, score in enumerate(scores[::-1]):
acc += score
if acc >= mid:
return len(scores) - 1 - i
def majority_score(profile: List[int], grade: int) -> float:
total = sum(profile)
left = sum(profile[:grade]) / total
right = sum(profile[1 + grade:]) / total
return -right if left < right else left
def tie_breaking(a: List[int], b: List[int]):
''' algorithm to divide out candidates with the same median grade.
Return True if a < b (or if b has a better ranking than a)'''
med_a = majority_grade(a)
med_b = majority_grade(b)
while med_a == med_b:
a[med_a] -= 1
b[med_b] -= 1
if a[med_a] < 0:
return True
if b[med_b] < 0:
return False
med_a = majority_grade(a)
med_b = majority_grade(b)
return med_a < med_b
def compute_votes(votes: List[List[int]], num_grades: int):
merit_profiles = votes_to_merit_profiles(votes, num_grades)
scores = []
grades = []
ranking = []
Grade = int
for profile in merit_profiles:
grade = majority_grade(profile)
score = majority_score(profile, grade)
scores.append(score)
grades.append(grade)
num_candidates = len(merit_profiles)
tuples = [(num_grades - g, s) for g, s in zip(grades, scores)]
ranking = sorted(range(num_candidates), reverse=True, key=tuples.__getitem__)
return merit_profiles, scores, grades, ranking
def votes_to_merit_profiles(votes: List[List[int]], num_grades: int):
def votes_to_merit_profiles(
votes: List[List[Grade]],
grades: List[Grade]
) -> List[Dict[Grade, int]]:
"""
Convert a list of votes into a matrix containing the number of grades for
each candidate
@ -69,12 +16,107 @@ def votes_to_merit_profiles(votes: List[List[int]], num_grades: int):
assert len(votes) > 0, "Empty list of votes"
grades = [0] * num_grades
num_candidates = len(votes[0])
profiles = [grades.copy() for _ in range(num_candidates)]
num_candidates: int = len(votes[0])
profiles: List[Dict[Grade, int]] = [
dict.fromkeys(grades, 0) for _ in range(num_candidates)
]
for vote in votes:
for i, grade in enumerate(vote):
profiles[i][grade] += 1
for candidate_i, grade in enumerate(vote):
profiles[candidate_i][grade] += 1
return profiles
def majority_grade(scores: List[int]) -> int:
mid = math.ceil(sum(scores)/2.0)
acc = 0
for i, score in enumerate(scores[::-1]):
acc += score
if acc >= mid:
return len(scores) - 1 - i
# Using gauge for a fast algorithm
# However, it creates paradoxical results with a lower number of votes
@dataclass
class MajorityGauge:
profile: List[int]
above: float = 0.
below: float = 0.
grade: int = 0.
sign: int = 0
gauge: float = 0.
def __post_init__(self):
self.grade = majority_grade(self.profile)
total: int = sum(self.profile)
self.above = sum(self.profile[:self.grade]) / total
self.below = sum(self.profile[1 + self.grade:]) / total
self.sign = 1 if self.above > self.below else -1
self.gauge = max(self.above, self.below)
def sort_by_gauge(gauges: List[MajorityGauge]) -> List[MajorityGauge]:
by_gauge: List[MajorityGauge] = sorted(gauges, key=attrgetter("gauge"))
by_sign: List[MajorityGauge] = sorted(by_gauge, key=attrgetter("sign"))
return sorted(by_sign, key=attrgetter("grade"), reverse=True)
def sort_by_gauge_with_index(gauges: List[MajorityGauge]
) -> List[Tuple[int, MajorityGauge]]:
by_gauge: List[Tuple[int, MajorityGauge]] = sorted(
enumerate(gauges), key= lambda x: getattr(x[1], "gauge")
)
by_sign: List[Tuple[int, MajorityGauge]] = sorted(
by_gauge, key= lambda x: getattr(x[1], "sign")
)
return sorted(
by_sign, key= lambda x: getattr(x[1], "grade")
)
# Long way but no ambiguity
def majority_grade_from_votes(votes: List[int]):
"""
Each vote is a grade.
>>> majority_grade_from_votes([15, 16, 17, 18])
16
>>> majority_grade_from_votes([15, 17, 17, 18])
17
>>> majority_grade_from_votes([15, 16, 17, 17, 18])
17
"""
votes = sorted(votes, reverse=False)
return votes[(len(votes) - 1) // 2]
@dataclass
class MajorityValue:
profile: Dict[Grade, int]
values: List[int] = field(default_factory=list)
grade: int = 0
def __post_init__(self):
if self.values == []:
votes = [
i for grade, num in self.profile.items()
for i in [grade] * num
]
for _ in range(len(votes)):
grade: int = majority_grade_from_votes(votes)
self.values.append(grade)
votes.remove(grade)
self.grade = self.values[0]
def sort_by_value_with_index(
values: List[MajorityValue]
) -> List[Tuple[int, MajorityGauge]]:
return sorted(
enumerate(values),
key=lambda x: getattr(x[1], "values"),
reverse=True
)

Loading…
Cancel
Save