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:
parent
4d221047c3
commit
4a973d41b1
5 changed files with 102 additions and 39 deletions
18
elections/migrations/0019_option_winner.py
Normal file
18
elections/migrations/0019_option_winner.py
Normal 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"),
|
||||
),
|
||||
]
|
|
@ -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",
|
||||
|
|
|
@ -135,23 +135,22 @@
|
|||
{% for o in q.options.all %}
|
||||
<div class="panel-block">
|
||||
{% if election.tallied and election.results_public %}
|
||||
{% if q.vote_type == "select" %}
|
||||
<span class="tag {% if o.nb_votes == q.max_votes %}is-success{% else %}is-primary{% endif %}">
|
||||
<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>
|
||||
</span>
|
||||
|
||||
{% elif q.vote_type == "rank" %}
|
||||
<span class="tag is-primary">
|
||||
{% elif q.vote_type == "rank" %}
|
||||
|
||||
<span class="icon">
|
||||
<i class="fas fa-layer-group"></i>
|
||||
</span>
|
||||
<span>{{ forloop.counter }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<span class="ml-2">{{ o.text }}</span>
|
||||
</div>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,24 +123,62 @@ 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
|
||||
duels = np.maximum(sum(ballots), 0)
|
||||
if ballots != []:
|
||||
# On additionne les classements de tout le monde pour avoir la matrice
|
||||
# des duels
|
||||
duels = np.maximum(sum(ballots), 0)
|
||||
|
||||
cells = []
|
||||
# Configuration du graphe
|
||||
graph = nx.DiGraph()
|
||||
|
||||
# On enregistre la matrice
|
||||
for i, line in enumerate(duels):
|
||||
for j, cell in enumerate(line):
|
||||
if cell > 0:
|
||||
cells.append(
|
||||
Duel(
|
||||
question=question,
|
||||
winner=options[j],
|
||||
loser=options[i],
|
||||
amount=cell,
|
||||
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,
|
||||
winner=options[j],
|
||||
loser=options[i],
|
||||
amount=cell,
|
||||
)
|
||||
)
|
||||
)
|
||||
Duel.objects.bulk_create(cells)
|
||||
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:
|
||||
|
|
Loading…
Reference in a new issue