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)
|
text = models.TextField(_("texte"), blank=False)
|
||||||
|
|
||||||
|
winner = models.BooleanField(_("option gagnante"), default=False)
|
||||||
voters = models.ManyToManyField(
|
voters = models.ManyToManyField(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
related_name="votes",
|
related_name="votes",
|
||||||
|
|
|
@ -135,22 +135,21 @@
|
||||||
{% for o in q.options.all %}
|
{% for o in q.options.all %}
|
||||||
<div class="panel-block">
|
<div class="panel-block">
|
||||||
{% if election.tallied and election.results_public %}
|
{% if election.tallied and election.results_public %}
|
||||||
|
<span class="tag {% if o.winner %}is-success{% else %}is-primary{% endif %}">
|
||||||
{% if q.vote_type == "select" %}
|
{% if q.vote_type == "select" %}
|
||||||
<span class="tag {% if o.nb_votes == q.max_votes %}is-success{% else %}is-primary{% endif %}">
|
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fas fa-vote-yea"></i>
|
<i class="fas fa-vote-yea"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>{{ o.nb_votes }}</span>
|
<span>{{ o.nb_votes }}</span>
|
||||||
</span>
|
|
||||||
|
|
||||||
{% elif q.vote_type == "rank" %}
|
{% elif q.vote_type == "rank" %}
|
||||||
<span class="tag is-primary">
|
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fas fa-layer-group"></i>
|
<i class="fas fa-layer-group"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>{{ forloop.counter }}</span>
|
<span>{{ forloop.counter }}</span>
|
||||||
</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<span class="ml-2">{{ o.text }}</span>
|
<span class="ml-2">{{ o.text }}</span>
|
||||||
|
|
|
@ -139,32 +139,34 @@
|
||||||
|
|
||||||
{# Liste des options possibles #}
|
{# Liste des options possibles #}
|
||||||
{% for o in q.options.all %}
|
{% for o in q.options.all %}
|
||||||
<div class="panel-block" id="o_{{ o.pk }}">
|
<div class="panel-block">
|
||||||
{% if election.start_date > current_time %}
|
{% if election.tallied %}
|
||||||
<span class="tags has-addons mb-0">
|
<span class="tag {% if o.winner %}is-success{% else %}is-primary{% endif %}">
|
||||||
<a class="tag is-danger mb-0" title="{% trans "Supprimer" %}" href="{% url 'election.del-option' o.pk %}">
|
{% if q.vote_type == "select" %}
|
||||||
<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 %}">
|
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fas fa-vote-yea"></i>
|
<i class="fas fa-vote-yea"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>{{ o.nb_votes }}</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>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<span class="ml-2">{{ o.text }}</span>
|
<span class="ml-2">{{ o.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{# Affiche plus d'informations sur le résultat #}
|
||||||
|
{% if election.tallied %}
|
||||||
|
{{ q.get_results_data }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{# Rajout d'une option #}
|
{# Rajout d'une option #}
|
||||||
{% if election.start_date > current_time %}
|
{% if election.start_date > current_time %}
|
||||||
<form action="{% url 'election.add-option' q.pk %}" method="post">
|
<form action="{% url 'election.add-option' q.pk %}" method="post">
|
||||||
|
|
|
@ -2,7 +2,9 @@ import csv
|
||||||
import io
|
import io
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
import networkx as nx
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from networkx.algorithms.dag import ancestors, descendants
|
||||||
|
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
@ -85,14 +87,17 @@ class TallyFunctions:
|
||||||
max_votes = max(max_votes, o.nb_votes)
|
max_votes = max(max_votes, o.nb_votes)
|
||||||
options.append(o)
|
options.append(o)
|
||||||
|
|
||||||
|
for o in options:
|
||||||
|
o.winner = o.nb_votes == max_votes
|
||||||
|
|
||||||
question.max_votes = max_votes
|
question.max_votes = max_votes
|
||||||
question.save()
|
question.save()
|
||||||
|
|
||||||
Option.objects.bulk_update(options, ["nb_votes"])
|
Option.objects.bulk_update(options, ["nb_votes", "winner"])
|
||||||
|
|
||||||
def tally_rank(question):
|
def tally_rank(question):
|
||||||
"""On dépouille un vote par classement et on crée la matrice des duels"""
|
"""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):
|
def duel(a, b):
|
||||||
# Renvoie 1 si a est classé avant b, -1 si c'est l'inverse et
|
# 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]
|
votes = ranks_by_user[user]
|
||||||
ballots.append(np.array([[duel(x, y) for x in votes] for y in votes]))
|
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)
|
duels = np.maximum(sum(ballots), 0)
|
||||||
|
|
||||||
|
# Configuration du graphe
|
||||||
|
graph = nx.DiGraph()
|
||||||
|
|
||||||
|
graph.add_nodes_from(options)
|
||||||
|
|
||||||
cells = []
|
cells = []
|
||||||
|
|
||||||
# On enregistre la matrice
|
# On enregistre la matrice
|
||||||
for i, line in enumerate(duels):
|
for i, line in enumerate(duels):
|
||||||
for j, cell in enumerate(line):
|
for j, cell in enumerate(line):
|
||||||
if cell > 0:
|
if cell > 0:
|
||||||
|
graph.add_edge(options[j], options[i], weight=cell)
|
||||||
cells.append(
|
cells.append(
|
||||||
Duel(
|
Duel(
|
||||||
question=question,
|
question=question,
|
||||||
|
@ -137,6 +150,36 @@ class TallyFunctions:
|
||||||
)
|
)
|
||||||
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:
|
class ValidateFunctions:
|
||||||
"""Classe pour valider les formsets selon le type de question"""
|
"""Classe pour valider les formsets selon le type de question"""
|
||||||
|
|
Loading…
Reference in a new issue