On utilise un champ pour stocker l'information de l'option gagnante, et on implémente l'algorithme de schwartz pour dépouiller les votes de condorcet avec schultze

This commit is contained in:
Tom Hubrecht 2021-04-04 23:38:21 +02:00
parent 4d221047c3
commit 4a973d41b1
5 changed files with 102 additions and 39 deletions

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.19 on 2021-04-04 16:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("elections", "0018_auto_20210331_1317"),
]
operations = [
migrations.AddField(
model_name="option",
name="winner",
field=models.BooleanField(default=False, verbose_name="option gagnante"),
),
]

View file

@ -123,6 +123,7 @@ class Option(models.Model):
)
text = models.TextField(_("texte"), blank=False)
winner = models.BooleanField(_("option gagnante"), default=False)
voters = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name="votes",

View file

@ -135,22 +135,21 @@
{% for o in q.options.all %}
<div class="panel-block">
{% if election.tallied and election.results_public %}
<span class="tag {% if o.winner %}is-success{% else %}is-primary{% endif %}">
{% if q.vote_type == "select" %}
<span class="tag {% if o.nb_votes == q.max_votes %}is-success{% else %}is-primary{% endif %}">
<span class="icon">
<i class="fas fa-vote-yea"></i>
</span>
<span>{{ o.nb_votes }}</span>
</span>
{% elif q.vote_type == "rank" %}
<span class="tag is-primary">
<span class="icon">
<i class="fas fa-layer-group"></i>
</span>
<span>{{ forloop.counter }}</span>
</span>
{% endif %}
</span>
{% endif %}
<span class="ml-2">{{ o.text }}</span>

View file

@ -139,32 +139,34 @@
{# Liste des options possibles #}
{% for o in q.options.all %}
<div class="panel-block" id="o_{{ o.pk }}">
{% if election.start_date > current_time %}
<span class="tags has-addons mb-0">
<a class="tag is-danger mb-0" title="{% trans "Supprimer" %}" href="{% url 'election.del-option' o.pk %}">
<span class="icon">
<i class="fas fa-times"></i>
</span>
</a>
<a class="tag is-info mb-0" title="{% trans "Modifier" %}" href="{% url 'election.mod-option' o.pk %}">
<span class="icon">
<i class="fas fa-edit"></i>
</span>
</a>
</span>
{% elif election.tallied %}
<span class="tag {% if o.nb_votes == q.max_votes %}is-success{% else %}is-primary{% endif %}">
<div class="panel-block">
{% if election.tallied %}
<span class="tag {% if o.winner %}is-success{% else %}is-primary{% endif %}">
{% if q.vote_type == "select" %}
<span class="icon">
<i class="fas fa-vote-yea"></i>
</span>
<span>{{ o.nb_votes }}</span>
{% elif q.vote_type == "rank" %}
<span class="icon">
<i class="fas fa-layer-group"></i>
</span>
<span>{{ forloop.counter }}</span>
{% endif %}
</span>
{% endif %}
<span class="ml-2">{{ o.text }}</span>
</div>
{% endfor %}
{# Affiche plus d'informations sur le résultat #}
{% if election.tallied %}
{{ q.get_results_data }}
{% endif %}
{# Rajout d'une option #}
{% if election.start_date > current_time %}
<form action="{% url 'election.add-option' q.pk %}" method="post">

View file

@ -2,7 +2,9 @@ import csv
import io
import random
import networkx as nx
import numpy as np
from networkx.algorithms.dag import ancestors, descendants
from django.contrib.auth.hashers import make_password
from django.core.exceptions import ValidationError
@ -85,14 +87,17 @@ class TallyFunctions:
max_votes = max(max_votes, o.nb_votes)
options.append(o)
for o in options:
o.winner = o.nb_votes == max_votes
question.max_votes = max_votes
question.save()
Option.objects.bulk_update(options, ["nb_votes"])
Option.objects.bulk_update(options, ["nb_votes", "winner"])
def tally_rank(question):
"""On dépouille un vote par classement et on crée la matrice des duels"""
from .models import Duel, Rank
from .models import Duel, Option, Rank
def duel(a, b):
# Renvoie 1 si a est classé avant b, -1 si c'est l'inverse et
@ -118,15 +123,23 @@ class TallyFunctions:
votes = ranks_by_user[user]
ballots.append(np.array([[duel(x, y) for x in votes] for y in votes]))
# On additionne les classements de tout le monde
if ballots != []:
# On additionne les classements de tout le monde pour avoir la matrice
# des duels
duels = np.maximum(sum(ballots), 0)
# Configuration du graphe
graph = nx.DiGraph()
graph.add_nodes_from(options)
cells = []
# On enregistre la matrice
for i, line in enumerate(duels):
for j, cell in enumerate(line):
if cell > 0:
graph.add_edge(options[j], options[i], weight=cell)
cells.append(
Duel(
question=question,
@ -137,6 +150,36 @@ class TallyFunctions:
)
Duel.objects.bulk_create(cells)
# On utilise la méthode de schwartz pour trouver les options gagnantes
while graph.edges():
losers = set() # Les options qui se font battre strictement
for n in graph.nodes():
losers |= set(descendants(graph, n)) - set(ancestors(graph, n))
if losers:
# On supprime les options perdantes
for n in losers:
graph.remove_node(n)
else:
# On n'a pas d'options perdantes, on supprime les arêtes de poids
# le plus faible
min_weight = min(nx.get_edge_attributes(graph, "weight").values())
min_edges = []
for (u, v) in graph.edges():
if graph[u][v]["weight"] == min_weight:
min_edges.append((u, v))
for (u, v) in min_edges:
graph.remove_edge(u, v)
# Les options gagnantes sont celles encore présentes dans le graphe
winners = graph.nodes()
for o in options:
o.winner = o in winners
Option.objects.bulk_update(options, ["winner"])
class ValidateFunctions:
"""Classe pour valider les formsets selon le type de question"""