Skip to content

Commit

Permalink
Added vote method elimination decorator,
Browse files Browse the repository at this point in the history
made vote method outputs more consistent.
  • Loading branch information
johnh865 committed Mar 2, 2021
1 parent e134afb commit cd9a92e
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 18 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.5', # Feb 26, 2021 update - fix to STAR
version='1.0.6', # Mar 1, 2021 update
# version='1.0.5', # Feb 26, 2021 update - fix to STAR
# version='1.0.4', # Feb 26, 2021 update
# version='1.0.3', # Feb 25, 2021 update
# version='1.0.2', # Feb 23, 2021 update
Expand Down
19 changes: 12 additions & 7 deletions votesim/votemethods/condorcet.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
"""
import logging
import numpy as np
from votesim.votemethods.tools import winner_check
from votesim.votemethods.tools import (
winner_check,
multi_win_eliminate_decorator,
)
import votesim.votemethods.condcalcs as condcalcs
from votesim.votemethods.ranked import borda
from votesim.votemethods.condcalcs import (smith_set,
Expand Down Expand Up @@ -72,16 +75,18 @@ def smith_minimax(ranks=None, numwin=1, matrix=None):
#imax = np.argmax(min_losses[s])
#winner = candidates[s][imax]

winners, ties = winner_check(min_losses, numwin=numwin)
winners, ties = winner_check(min_losses, numwin=1)

output = {}
if m is not None:
output['margin_matrix'] = win_losses
output['vote_matrix'] = m
output['tally'] = min_losses
output['smith_set'] = s
return winners, ties, output


@multi_win_eliminate_decorator
def ranked_pairs(ranks, numwin=1,):
"""Ranked-pairs or Tideman election system.
Expand Down Expand Up @@ -165,8 +170,8 @@ def ranked_pairs(ranks, numwin=1,):
# Find the condorcet winner in the locked candidate pairs
winners, ties, scores = condorcet_winners_check(
pairs=locked_pairs,
numwin=numwin,
)
numwin=1,
)

# Handle some additional output
output = {}
Expand Down Expand Up @@ -243,7 +248,7 @@ def ranked_pairs_test2(ranks, numwin=1):
#



@multi_win_eliminate_decorator
def smith_score(data, numwin=1,):
"""Smith then score voting variant.
Expand Down Expand Up @@ -288,7 +293,7 @@ def smith_score(data, numwin=1,):
in_smith[smith] = True
sums[~in_smith] = 0

winners, ties = winner_check(sums, numwin=numwin)
winners, ties = winner_check(sums, numwin=1)

output = {}
output['sums'] = sums
Expand All @@ -299,7 +304,7 @@ def smith_score(data, numwin=1,):




@multi_win_eliminate_decorator
def black(ranks):
"""Condorcet-black."""
m = pairwise_rank_matrix(ranks)
Expand Down
2 changes: 1 addition & 1 deletion votesim/votemethods/irv.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def irv(data, numwin=1, seed=None):
output['loser_history'] = np.array(loser_log)
# output['data'] = data

return winners, ties, output
return winners, ties.astype(int), output


def irv_eliminate(data):
Expand Down
17 changes: 12 additions & 5 deletions votesim/votemethods/score.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import logging
from votesim.votemethods import tools
from votesim.votemethods import condcalcs
from votesim.votemethods.tools import multi_win_eliminate_decorator


logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -86,6 +88,7 @@ def score10(data, numwin=1):
return score(data, numwin=numwin)


@multi_win_eliminate_decorator
def majority_judgment(data, numwin=1, remove_nulls=True, maxiter=1e5):
"""Majority judgment (median score).
Expand Down Expand Up @@ -283,11 +286,13 @@ def reweighted_range(data, numwin=1, C_ratio=1.0, maxscore=None):
winners = np.array(winners)
logger.info('winners = %s' % winners)
logger.info('ties = %s' % ties)
return winners, ties, np.array(round_history)

output = {}
output['round_history'] = np.array(round_history)
return winners, ties, output



@multi_win_eliminate_decorator
def star(data, numwin=1):
"""STAR voting (Score then Automatic Runoff)
Expand Down Expand Up @@ -318,8 +323,7 @@ def star(data, numwin=1):
runoff_sums : (c,) array
Votes for each candidate in runoff
"""
if numwin > 1:
raise NotImplementedError('Multi-winner STAR not available.' )

### First Round -- Score Voting
data = np.array(data)
sums = np.sum(data, axis=0)
Expand Down Expand Up @@ -507,7 +511,10 @@ def sequential_monroe(data, numwin=1, maxscore=None ):

winners = np.array(winners, dtype=int)
ties = np.array([], dtype=int)
return winners, ties, np.array(mean_scores_record)

output = {}
output['round_history'] = np.array(mean_scores_record)
return winners, ties, output


def distributed(data, numwin=1):
Expand Down
33 changes: 33 additions & 0 deletions votesim/votemethods/tests/test_multi_eliminate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from votesim.models import spatial
from votesim.votemethods import irv, tools, score
import numpy as np

seed = 1
v = spatial.Voters(seed=seed)
v.add_random(40, 3)
c = spatial.Candidates(v, seed=seed)
c.add_random(10)

e = spatial.Election(v, c, scoremax=5)
ranks = e.ballotgen.honest_ballot_dict['rank']
scores = e.ballotgen.honest_ballot_dict['score']



def test_multi_win_score():

w1, t1, o1 = score.score(scores, numwin=3)
w2, t2, o2 = tools.multi_win_eliminate(score.score, scores, numwin=3)

assert np.all(w1 == w2)
assert np.all(t1 == t2)
return






if __name__ == '__main__':
test_multi_win_score()
13 changes: 9 additions & 4 deletions votesim/votemethods/tests/test_score.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def test_result(self):
numwin=2,
C_ratio=1,
maxscore=99)
hist = np.round(hist)
hist = np.round(hist['round_history'])

# specify correct answer from warren smith's website
hist_right = [[6110, 7050, 7070],
Expand Down Expand Up @@ -81,7 +81,7 @@ def test_result2(self):
wtrue = [0, 3, 1]

w = np.round(w)
h = np.round(h)
h = np.round(h['round_history'])

print('winners', w)
print('history', h)
Expand Down Expand Up @@ -183,12 +183,17 @@ def test_result(self):
#
if __name__ == '__main__':
# unittest.main(exit=False)
t = TestStar()
t = TestScore()
t.test_result()
t.test_result2()


# t = TestStar()
# t.test_tie()
# t.test_tie3()
# t.test_wiki()
# t.test_zeros()
t.test_tally()
# t.test_tally()

# t = TestStar()
# t.test_zeros()
Expand Down
116 changes: 116 additions & 0 deletions votesim/votemethods/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,124 @@ def winner_check_named(results, candidates: list, numwin: int=1):
win_candidates = candidates[winners]
tie_candidates = candidates[ties]
return win_candidates, tie_candidates




def run_with_eliminated(
func,
eliminated: list,
data: np.ndarray,
numwin: int=1,
**kwargs):
"""Run election method with list of eliminated candidates.
Parameters
----------
func : function
Election method
eliminated: list[int]
List of eliminated candidate indices
data : ndarray (a, b)
Ballot data for `a` voters and `b` candidates
numwin : int
Number of winners to find.
Returns
-------
winners : ndarray (c,)
Winner candidate indices
ties : ndarray (t,)
Tie candidates indices
outputs : dict
data output dict
"""
num_candidates = data.shape[1]

cbools = np.ones(num_candidates, dtype=bool)
cbools[eliminated] = False
cindex = np.where(cbools)[0]

data1 = data[:, cindex]
winners1, ties1, output1 = func(data1, numwin=numwin, **kwargs)

winners = cindex[winners1]
ties = cindex[ties1]
return winners, ties, output1




def multi_win_eliminate(func, data, numwin=1, **kwargs):
"""Convert single winner method to multi-winner method,
using candidate elimination."""
winners = []
num_left = numwin
data = data.copy()
outputs = []
ties = None
while num_left > 0:

winners_ii, ties_ii, output_ii = run_with_eliminated(
func,
winners,
data,
numwin=1,
**kwargs)

outputs.append(output_ii)
winner_len = len(winners_ii)
tie_len = len(ties_ii)

# Check if single winner has been found
if winner_len == 1:
winners_ii = winners_ii[0]
winners.append(winners_ii)

elif winner_len == 0:
# Check if ties have been found, but there are enough
# slots left to fill them in as winners.
if num_left >= tie_len:
winners.extend(ties_ii)

# Check for ties, but there are no slots left for them all.
elif tie_len > 1:
ties = ties_ii
break

num_left = numwin - len(winners)

winners = np.array(winners, dtype=int)
if ties:
ties = ties.asarray(dtype=int)
else:
ties = np.array([], dtype=int)
return winners, ties, outputs


def multi_win_eliminate_decorator(func):
"""Decorator to convert single winner method to multi-winner method,
using candidate elimination."""

def func2(data, numwin=1, **kwargs):
if numwin == 1:
return func(data, numwin=numwin, **kwargs)

winners, ties, outputs = multi_win_eliminate(
func,
data,
numwin = numwin,
**kwargs
)
output = outputs[0]
output['elimination_rounds'] = outputs
return winners, ties, outputs
return func2





def rcv_reorder(data):
"""Make sure rankings are sequential integers from [1 to b],
with 0 meaning eliminated or unranked.
Expand Down

0 comments on commit cd9a92e

Please sign in to comment.