Skip to content

Commit

Permalink
Fixed unit tests, enhanced STAR voting tie handling, added new Condorcet
Browse files Browse the repository at this point in the history
cycle detection function.
  • Loading branch information
johnh865 committed Feb 26, 2021
1 parent d01a566 commit f09d72c
Show file tree
Hide file tree
Showing 16 changed files with 891 additions and 795 deletions.
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
#
setup(
name='votesim',
version='1.0.3', # Feb 25, 2021 update
version='1.0.4', # Feb 26, 2021 update
# version='1.0.3', # Feb 25, 2021 update
# version='1.0.2', # Feb 23, 2021 update
# version='1.0.1',
packages=find_packages(),
Expand Down
49 changes: 47 additions & 2 deletions votesim/votemethods/condcalcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ def condorcet_winners_check(ranks=None, matrix=None, pairs=None, numwin=1,
- column 3 = votes for winner
full_ranking : bool (default False)
If True evaluate entire ranking of candidates for score output
If True evaluate entire ranking of candidates for scored output
(ie a metric to tally the degree by which each candidate has won by).
Returns
-------
Expand All @@ -265,7 +266,8 @@ def condorcet_winners_check(ranks=None, matrix=None, pairs=None, numwin=1,
ties : (e,) array
Index locations of `e` number of ties
scores : (b,) array
Rankings of all candidates
Rankings of all candidates (The strength of candidates' wins).
Only calculated if full_ranking=True.
"""
if ranks is not None:
vm = VoteMatrix(ranks=ranks)
Expand Down Expand Up @@ -372,6 +374,46 @@ def pairs(self):
d = [i, j, winlosses, votes]
pairs.append(d)
return np.array(pairs)


@lazy_property
def worst_margins(self):
"""Worst win-loss margin for each candidate."""
cnum = self.cnum
matrix = self.margin_matrix.copy()
indexer_diagonal = np.arange(cnum)
matrix[indexer_diagonal, indexer_diagonal] = np.nan
tallies = np.nanmin(matrix, axis=1)
return tallies


@lazy_property
def worst_votes(self):
"""Worst votes recieved for each candidate."""
cnum = self.cnum
matrix = self.matrix.copy()
indexer_diagonal = np.arange(cnum)
matrix[indexer_diagonal, indexer_diagonal] = np.nan
tallies = np.nanmin(matrix, axis=1)
return tallies



@lazy_property
def winner(self):
"""Return Condorcet winner. Return -1 if no winner found."""
tallies = self.worst_margins
winners = np.where(tallies > 0)[0]
if len(winners) == 0:
return -1
else:
return winners[0]


@lazy_property
def has_cycle(self):
return np.all(self.worst_margins <= 0)



class VotePairsError(Exception):
Expand Down Expand Up @@ -501,6 +543,9 @@ def has_cycle(self):
flag = False
vpairs = self
cnum = len(vpairs.pairs)
if cnum == 0:
return True

while cnum > 1:

try:
Expand Down
46 changes: 27 additions & 19 deletions votesim/votemethods/score.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import logging
from votesim.votemethods import tools
from votesim.votemethods import condcalcs

logger = logging.getLogger(__name__)

__all__ = [
Expand Down Expand Up @@ -328,31 +330,37 @@ def star(data, numwin=1):
runoff_data = data[:, runoff_candidates]

### Second Round -- Automatic majoritarian runoff
# The candidate that beats the most head-to-head competitions wins!
vote_matrix = []

# Calculate number of positive votes for candidate head to head
for icandidate in runoff_candidates:
iscores = data[:, icandidate : (icandidate+1)]
votes_for = (iscores > runoff_data).sum(axis=0)
votes_against = (iscores < runoff_data).sum(axis=0)
votes = votes_for - votes_against
vote_matrix.append(votes)

vote_matrix = np.array(vote_matrix)
win_matrix = vote_matrix > 0
win_array = np.sum(win_matrix, axis=1)

# # The candidate that beats the most head-to-head competitions wins!
# vote_matrix = []

# # Calculate number of positive votes for candidate head to head
# for icandidate in runoff_candidates:
# iscores = data[:, icandidate : (icandidate+1)]
# votes_for = (iscores > runoff_data).sum(axis=0)
# # votes_against = (iscores < runoff_data).sum(axis=0)
# vote_matrix.append(votes_for)
# # votes = votes_for - votes_against
# # vote_matrix.append(votes)

# vote_matrix = np.array(vote_matrix)
# # win_matrix = vote_matrix > 0
# vote_array = np.sum(vote_matrix, axis=1)

matrix = condcalcs.pairwise_scored_matrix(runoff_data)
vm = condcalcs.VoteMatrix(matrix=matrix)
j_runoff_tally = vm.worst_margins
jwinner, jties = tools.winner_check(j_runoff_tally)

# Calculate winner
jwinners, jties = tools.winner_check(win_array, numwin=1)
winners = runoff_candidates[jwinners]
# jwinners, jties = tools.winner_check(vote_array, numwin=1)
winner = runoff_candidates[jwinner]
ties = runoff_candidates[jties]

details = {}
details['tally'] = sums
details['runoff_candidates'] = runoff_candidates
details['runoff_matrix'] = vote_matrix
details['runoff_tally'] = win_array
details['runoff_matrix'] = matrix
# details['runoff_tally'] = j_runoff_tally

return winners, ties, details

Expand Down
18 changes: 11 additions & 7 deletions votesim/votemethods/tests/test_borda.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# -*- coding: utf-8 -*-
import numpy as np
from votesim.votemethods.ranked import borda
ranks = [[3, 2, 1],
[0, 1, 0],
[1, 3, 2],
[0, 1, 0],
[1, 3, 2],]

ranks = np.array(ranks)
w,t, o = borda(ranks)
def test():
ranks = [[3, 2, 1],
[0, 1, 0],
[1, 3, 2],
[0, 1, 0],
[1, 3, 2],]

ranks = np.array(ranks)
w,t, o = borda(ranks)


42 changes: 23 additions & 19 deletions votesim/votemethods/tests/test_condorcet_detect_cycle1.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,31 @@
from votesim.votemethods.condcalcs import (
pairwise_rank_matrix,
has_cycle,
VoteMatrix,
)

pairs =[
[0, 1, 10],
[1, 2, 10],
[2, 3, 10],
[3, 1, 10]
]

# Sort the pairs with highest margin of victory first.
pairs = np.array(pairs)
#c = _CycleDetector(pairs)
assert has_cycle(pairs)
def test1():
pairs =[
[0, 1, 10],
[1, 2, 10],
[2, 3, 10],
[3, 1, 10]
]

# Sort the pairs with highest margin of victory first.
pairs = np.array(pairs)
#c = _CycleDetector(pairs)
assert has_cycle(pairs)



pairs = [
[0, 1, 10],
[1, 2, 10],
[2, 3, 10],
]
pairs = np.array(pairs)

assert not has_cycle(pairs)
def test2():
pairs = [
[0, 1, 10],
[1, 2, 10],
[2, 3, 10],
]
pairs = np.array(pairs)

assert not has_cycle(pairs)

0 comments on commit f09d72c

Please sign in to comment.