Skip to content

Commit

Permalink
Fix bugs in borda count and STV.
Browse files Browse the repository at this point in the history
Added new general votemethod unit test.
  • Loading branch information
johnh865 committed Mar 4, 2021
1 parent ffa224c commit 45a9267
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 12 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.9', # Mar 3, 2021 update, Seq Monroe Update
version='1.0.10', # Mar 4, 2021 update
# version='1.0.9', # Mar 3, 2021 update, Seq Monroe Update
# version='1.0.8', # Mar 3, 2021 update
# version='1.0.7', # Mar 2, 2021 update
# version='1.0.6', # Mar 1, 2021 update
Expand Down
29 changes: 28 additions & 1 deletion votesim/votemethods/condcalcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def smith_set(ranks=None, vm=None, wl=None):
return smith_set1


def condorcet_check_one(ranks=None, scores=None):
def condorcet_check_one(ranks=None, scores=None, matrix=None):
"""Calculate condorcet winner from ranked data if winner exists.
Partial election method; function does not handle outcomes when
Expand Down Expand Up @@ -201,6 +201,8 @@ def condorcet_check_one(ranks=None, scores=None):
m = pairwise_rank_matrix(ranks)
elif scores is not None:
m = pairwise_scored_matrix(scores)
elif matrix is not None:
m = np.asarray(matrix)
else:
raise ValueError('You must set either argument ranks or scores.'
'Both are currently not set as None.')
Expand All @@ -217,6 +219,31 @@ def condorcet_check_one(ranks=None, scores=None):
return i[0]
else:
raise RuntimeError('Something went wrong that should not have happened')


def check_win_loss_matrix(wl: np.ndarray):
"""Given win-loss matrix, find condorcet winner.
Parameters
----------
wl : array shape (a, a)
Win loss matrix
Returns
-------
out : int
Winner index location, -1 if no winner found.
"""
beats = wl > 0
cnum = len(wl) - 1
beatnum = np.sum(beats, axis=1)
ii = np.where(beatnum == cnum)[0]
if len(ii) == 0:
return -1
elif len(ii) == 1:
return ii[0]
else:
raise RuntimeError('Something went wrong that should not have happened')


def condorcet_winners_check(ranks=None, matrix=None, pairs=None, numwin=1,
Expand Down
14 changes: 11 additions & 3 deletions votesim/votemethods/condorcet.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
has_cycle,
pairwise_rank_matrix,
condorcet_winners_check,
check_win_loss_matrix,
VoteMatrix
)

Expand Down Expand Up @@ -341,11 +342,18 @@ def black(data, numwin=1):
"""Condorcet-black."""
m = pairwise_rank_matrix(data)
win_losses = m - m.T
winners, ties, scores = condorcet_winners_check(matrix=win_losses)

winner = check_win_loss_matrix(win_losses)

# winners, ties, scores = condorcet_winners_check(matrix=win_losses)
output = {}
output['margin_matrix'] = win_losses
output['tally'] = None
if len(winners) > 0:


if winner >= 0:
winners = np.array([winner])
ties = np.array([], dtype=int)
return winners, ties, output
else:
winners, ties, b_output = borda(data, numwin=1)
Expand All @@ -360,7 +368,7 @@ def copeland(data, numwin=1):
win_losses = m - m.T

# Create copeland results matrix
r_matrix = np.zeros(m.size)
r_matrix = np.zeros(m.shape)

# Give score 1 if more voters prefer candidate to other.
r_matrix[win_losses > 0] = 1
Expand Down
11 changes: 7 additions & 4 deletions votesim/votemethods/irv.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ def irv_eliminate(data):
round_history = []

logger.debug('irv elimination start.')
logger.debug('start losers = %s', start_losers)
logger.debug('# losers = %s', losernum)

active_bool = np.ones(candidate_num, dtype=bool)
Expand Down Expand Up @@ -369,6 +370,7 @@ def irv_stv(data, numwin=1, reallocation='hare',
num_ranked, num_candidates = data.shape

quota = droop_quota(num_ranked, numwin)
logger.info('\n\n------ STARTING STV ----------------------')
logger.info('stv droop quota = %s', quota)

if reallocation=='hare':
Expand Down Expand Up @@ -403,7 +405,7 @@ def irv_stv(data, numwin=1, reallocation='hare',


logger.info('\n\nstv round #%d', ii)
logger.info('stv votes = %s', tally)
logger.info('stv tally = %s', tally)
logger.info('stv winners = %s', winners)

# Break if we've gotten all winners
Expand Down Expand Up @@ -435,7 +437,8 @@ def irv_stv(data, numwin=1, reallocation='hare',

# Check if there are too many ties to complete STV
if winner_count + tienum > numwin + survivor_count - tienum:
logger.warning('Ties %s too close to winner. Outputting ties', ties)
logger.warning(
'Ties %s too close to winner. Outputting ties', ties)
break
else:
jj = rstate.randint(0, tienum)
Expand Down Expand Up @@ -528,12 +531,12 @@ def hare_reallocation(data, tally, winners, quota, weights, rstate=None):
win_voter_locs = data[:, winner] == 1
win_voter_index = np.flatnonzero(win_voter_locs)

vote_num = tally[ii]
# vote_num = int(tally[ii])

# Remove ballots the size of the quota
remove_index = rstate.choice(
win_voter_index,
size = min(vote_num, quota),
size = quota,
replace = False,
)

Expand Down
3 changes: 3 additions & 0 deletions votesim/votemethods/tests/test_condorcet_minimax.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ def tests_minimax_condorcet(self):






# def test_wiki(self):
# """
# Test example from wikipedia, retrieved Dec 19, 2019.
Expand Down
53 changes: 53 additions & 0 deletions votesim/votemethods/tests/test_general.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""
Test every voting method.
"""
import pdb
import logging

import votesim
from votesim import votemethods
from votesim.votemethods import scored_methods, ranked_methods, eRunner
from votesim.models import spatial


def test_all():
numvoters = 50
num_candidates = 6
numwinners_list = [1, 3, 5]

for ii in range(50):
v = spatial.Voters(seed=ii,)
v.add_random(numvoters=numvoters, ndim=2, )
c = spatial.Candidates(voters=v, seed=0)
c.add_random(cnum=num_candidates, sdev=1.0)
e = spatial.Election(voters=v, candidates=c)

scores = e.ballotgen.get_honest_ballots(
etype=votesim.votemethods.SCORE
)
ranks = e.ballotgen.get_honest_ballots(
etype=votesim.votemethods.IRV
)

for etype in ranked_methods:
for numwinners in numwinners_list:
runner = eRunner(etype=etype,
ballots=ranks,
numwinners=numwinners)
assert len(runner.winners) == numwinners, etype

for etype in scored_methods:
for numwinners in numwinners_list:
runner = eRunner(etype=etype,
ballots=scores,
numwinners=numwinners)
assert len(runner.winners) == numwinners, etype


if __name__ == '__main__':
# logging.basicConfig()
# logger = logging.getLogger('votesim.votemethods.irv')
# logger.setLevel(logging.DEBUG)

test_all()
6 changes: 3 additions & 3 deletions votesim/votemethods/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def multi_win_eliminate(func, data, numwin=1, **kwargs):
num_left = numwin
data = data.copy()
outputs = []
ties = None
ties = []
while num_left > 0:

winners_ii, ties_ii, output_ii = run_with_eliminated(
Expand Down Expand Up @@ -269,8 +269,8 @@ def multi_win_eliminate(func, data, numwin=1, **kwargs):
num_left = numwin - len(winners)

winners = np.array(winners, dtype=int)
if ties:
ties = ties.asarray(dtype=int)
if len(ties) > 0:
ties = np.asarray(ties, dtype=int)
else:
ties = np.array([], dtype=int)
return winners, ties, outputs
Expand Down

0 comments on commit 45a9267

Please sign in to comment.