-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathdistributions_by_n_cands.py
117 lines (87 loc) · 3.81 KB
/
distributions_by_n_cands.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
"""
Show how the winner distribution and bias of a voting method change with
number of candidates.
"""
import random
from collections import defaultdict
import matplotlib.pyplot as plt
import numpy as np
from joblib import Parallel, delayed
from seaborn import histplot, kdeplot
from elsim.elections import normal_electorate, normed_dist_utilities
from elsim.methods import black, fptp, irv, star, utility_winner
from elsim.strategies import honest_normed_scores, honest_rankings
n_elections = 20_000 # Roughly 30 seconds on a 2019 6-core i7-9750H
n_voters = 1_000
n_cands_list = [2, 3, 4, 5, 6, 7, 11, 15, 25]
cand_dist = 'normal'
u_width = 10
disp = 1.0
# Simulate more than just one election per worker to improve efficiency
batch_size = 100
n_batches = n_elections // batch_size
assert n_batches * batch_size == n_elections
def human_format(num):
for unit in ['', 'k', 'M', 'B', 'T']:
if abs(num) < 1000:
return f"{num:.3g}{unit}"
num /= 1000.0
method = 'STAR'
def simulate_batch():
winners = defaultdict(list)
for n_cands in n_cands_list:
for iteration in range(batch_size):
v, c = normal_electorate(n_voters, n_cands, dims=1, disp=disp)
if cand_dist == 'uniform':
# Replace with uniform distribution of candidates of same shape
c = np.random.uniform(-u_width/2, +u_width/2, n_cands)
c = np.atleast_2d(c).T
if 'Random' not in method:
utilities = normed_dist_utilities(v, c)
if method in {'FPTP', 'Hare RCV', 'Condorcet RCV (Black)'}:
rankings = honest_rankings(utilities)
if method == 'Random Winner': # Votes don't matter at all.
winner = random.sample(range(n_cands), 1)[0]
# Pick one voter and go with their choice.
if method == 'Random Ballot':
winning_voter = random.sample(range(n_voters), 1)[0]
dists = abs(v[winning_voter] - c)
winner = np.argmin(dists)
if method == 'FPTP':
winner = fptp(rankings, tiebreaker='random')
if method == 'Hare RCV':
winner = irv(rankings, tiebreaker='random')
if method == 'STAR':
ballots = honest_normed_scores(utilities)
winner = star(ballots, tiebreaker='random')
if method == 'Condorcet RCV (Black)':
winner = black(rankings, tiebreaker='random')
# (on normalized utilities though, so STAR can do better)
if method == 'Utility Winner':
winner = utility_winner(utilities, tiebreaker='random')
winners[n_cands].append(c[winner][0])
return winners
jobs = [delayed(simulate_batch)()] * n_batches
print(f'{len(jobs)} tasks total:')
results = Parallel(n_jobs=-3, verbose=5)(jobs)
winners = {k: [v for d in results for v in d[k]] for k in results[0]}
title = f'{method}, {human_format(n_elections)} 1D elections, '
title += f'{human_format(n_voters)} voters, '
title += cand_dist + 'ly-distributed candidates'
# For plotting only
v, c = normal_electorate(n_voters, 1000, dims=1)
fig, ax = plt.subplots(nrows=len(winners), num=title, sharex=True,
constrained_layout=True, figsize=(7.5, 9.5))
fig.suptitle(title)
for n, n_cands in enumerate(winners):
histplot(winners[n_cands], ax=ax[n], label=f'{n_cands} cands',
stat='density')
ax[n].set_yticklabels([]) # Don't care about numbers
ax[n].set_ylabel("") # No "density"
tmp = ax[n].twinx()
kdeplot(v[:, 0], ax=tmp, ls=':', label='Voters') # Label doesn't work
ax[n].plot([], [], ls=':', label='Voters') # Dummy label hack
tmp.set_yticklabels([]) # Don't care about numbers
tmp.set_ylabel("") # No "density"
ax[n].legend()
ax[0].set_xlim(-2.5, 2.5)