Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion elsim/strategies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
ballots that voters cast for a voting method.
"""
from .strategies import (approval_optimal, honest_normed_scores,
honest_rankings, vote_for_k)
honest_rankings, vote_for_k, vote_for_or_against_k)
69 changes: 69 additions & 0 deletions elsim/strategies/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,72 @@ def vote_for_k(utilities, k):
# TODO: Not sure if this is the most efficient way
approvals[np.arange(len(approvals))[:, np.newaxis], top_k] = 1
return approvals


def vote_for_or_against_k(utilities, k, rng=None):
"""
Convert utilities to combined-approval ballots (vote-for-or-against-k).

Weber (*Comparison of Public Choice Systems*, Cowles Discussion Paper 498)
defines ``2 * (m choose k)`` strategic types: for each cardinality-``k`` set
``S``, types **vote for** ``S`` (``+1`` on ``S``) and **vote against** ``S``
(``-1`` on ``S``), each with probability ``1 / (2 * (m choose k))``. The
effectiveness formulas follow from the resulting reproducing scores
``u_t(c)`` over regions of the preference cube. [1]_

For **simulation**, each voter independently flips a fair coin. **Vote for**
puts ``+1`` on that voter's ``k`` **highest**-utility candidates (ties broken
with noise). **Vote against** puts ``-1`` on their ``k`` **lowest**-utility
candidates---not on their favorites. The unused candidates stay at ``0``.
When ``k <= n_cands // 2`` the top and bottom blocks are disjoint, so each
ballot lies in ``{-1, 0, +1}`` with exactly ``k`` nonzero entries.

Monte Carlo Social Utility Efficiency under impartial culture may or may not
track ``eff_vote_for_or_against_k`` from the paper's infinite-voter analysis;
see ``examples/weber_1977_effectiveness_table.py``.

Parameters
----------
utilities : array_like
A 2D collection of utilities. Rows are voters; columns are candidates.
k : int
Must satisfy ``0 < k <= n_cands // 2`` (Weber allows ``k = m/2`` when
``m`` is even).
rng : numpy.random.Generator, optional
Random number generator. If omitted, ``numpy.random.default_rng()``
is used.

Returns
-------
election : ndarray
A 2D collection of combined approval ballots (``int8``).

References
----------
.. [1] Weber, Robert J. (1978). "Comparison of Public Choice Systems".
Cowles Foundation Discussion Papers. Cowles Foundation for Research in
Economics. No. 498. https://cowles.yale.edu/publications/cfdp/cfdp-498

"""
utilities = np.asarray(utilities)
n_voters, n_cands = utilities.shape
if not 0 < k <= n_cands // 2:
raise ValueError(
f'k of {k} not possible for vote-for-or-against-k with '
f'{n_cands} candidates (require 0 < k <= n_cands // 2)'
)

rng = np.random.default_rng(rng)
ballots = np.zeros((n_voters, n_cands), dtype=np.int8)
rows = np.arange(n_voters)[:, np.newaxis]

u = utilities.astype(np.float64, copy=False)
u_j = u + rng.random(u.shape) * (np.finfo(np.float64).eps * 64)
top_k = np.argpartition(u_j, -k, axis=1)[:, -k:]
bot_k = np.argpartition(u_j, k - 1, axis=1)[:, :k]
choice = rng.integers(2, size=n_voters, dtype=np.int8)
vote_for = (choice == 0)[:, np.newaxis]
target = np.where(vote_for, top_k, bot_k)
vals = np.where(vote_for, np.int8(1), np.int8(-1))
ballots[rows, target] = vals
return ballots
54 changes: 37 additions & 17 deletions examples/weber_1977_effectiveness_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,27 @@
Cowles Foundation Discussion Papers. Cowles Foundation for Research in
Economics. No. 498. https://cowles.yale.edu/publications/cfdp/cfdp-498

Typical result with n_elections = 100_000:

| | Standard | Vote-for-half | Borda |
|----:|-----------:|----------------:|--------:|
| 2 | 81.37 | 81.71 | 81.41 |
| 3 | 75.10 | 75.00 | 86.53 |
| 4 | 69.90 | 79.92 | 89.47 |
| 5 | 65.02 | 79.09 | 91.34 |
| 6 | 61.08 | 81.20 | 92.61 |
| 10 | 50.78 | 82.94 | 95.35 |
| 255 | 12.78 | 86.37 | 99.80 |
The dashed lines are Weber's infinite-voter closed forms. The solid curves
from this script are **not** asserted to coincide with them for
vote-for-or-against-k; compare visually or numerically as you like.

Typical Monte Carlo Social Utility Efficiency (``n_elections`` = 100_000)
with ``combined_approval``. Best Vote-for-or-against-k uses
``best_vote_for_or_against_k(m)`` and ``vote_for_or_against_k``: each voter
flips a fair coin, then either ``+1`` on their ``k`` best candidates by utility
or ``-1`` on their ``k`` worst (never disapproving favorites alone).

| | Standard | Vote-for-half | Best Vote-for-or-against-k | Borda |
|----:|-----------:|----------------:|-----------------------------:|--------:|
| 2 | 81.37 | 81.71 | (see simulation) | 81.41 |
| 3 | 75.10 | 75.00 | (see simulation) | 86.53 |
| 4 | 69.90 | 79.92 | (see simulation) | 89.47 |
| 5 | 65.02 | 79.09 | (see simulation) | 91.34 |
| 6 | 61.08 | 81.20 | (see simulation) | 92.61 |
| 10 | 50.78 | 82.94 | (see simulation) | 95.35 |
| 255 | 12.78 | 86.37 | (see simulation) | 99.80 |
"""
# TODO: Standard is consistently ~1% high, while Borda is very accurate
# TODO: Best Vote-for-or-against-k is not implemented yet
import time
from collections import Counter

Expand All @@ -29,9 +36,12 @@
from tabulate import tabulate

from elsim.elections import random_utilities
from elsim.methods import approval, borda, fptp, utility_winner
from elsim.strategies import honest_rankings, vote_for_k
from weber_1977_expressions import eff_borda, eff_standard, eff_vote_for_half
from elsim.methods import approval, borda, combined_approval, fptp, utility_winner
from elsim.strategies import (honest_rankings, vote_for_k,
vote_for_or_against_k)
from weber_1977_expressions import (eff_best_vote_for_or_against_k, eff_borda,
eff_standard, eff_vote_for_half,
best_vote_for_or_against_k)

n_elections = 2_000 # Roughly 60 seconds on a 2019 6-core i7-9750H
n_voters = 1_000
Expand All @@ -43,7 +53,8 @@
approval(vote_for_k(utilities, 'half'), tiebreaker)}

utility_sums = {key: Counter() for key in (ranked_methods.keys() |
rated_methods.keys() | {'UW'})}
rated_methods.keys() |
{'Best Vote-for-or-against-k', 'UW'})}

start_time = time.monotonic()

Expand All @@ -59,6 +70,12 @@
winner = method(utilities, tiebreaker='random')
utility_sums[name][n_cands] += utilities.sum(axis=0)[winner]

k_voa = best_vote_for_or_against_k(n_cands)
winner = combined_approval(
vote_for_or_against_k(utilities, k_voa), tiebreaker='random')
utility_sums['Best Vote-for-or-against-k'][n_cands] += (
utilities.sum(axis=0)[winner])

rankings = honest_rankings(utilities)
for name, method in ranked_methods.items():
winner = method(rankings, tiebreaker='random')
Expand All @@ -71,6 +88,8 @@
plt.title('The Effectiveness of Several Voting Systems')
for name, method in (('Standard', eff_standard),
('Vote-for-half', eff_vote_for_half),
('Best Vote-for-or-against-k',
eff_best_vote_for_or_against_k),
('Borda', eff_borda)):
plt.plot(n_cands_list, method(np.array(n_cands_list))*100, ':', lw=0.8)

Expand All @@ -82,7 +101,8 @@
# Calculate Social Utility Efficiency from summed utilities
x_uw, y_uw = zip(*sorted(utility_sums['UW'].items()))
average_utility = n_voters * n_elections / 2
for method in ('Standard', 'Vote-for-half', 'Borda'):
for method in ('Standard', 'Vote-for-half', 'Best Vote-for-or-against-k',
'Borda'):
x, y = zip(*sorted(utility_sums[method].items()))
SUE = (np.array(y) - average_utility)/(np.array(y_uw) - average_utility)
plt.plot(x, SUE*100, '-', label=method)
Expand Down
62 changes: 43 additions & 19 deletions examples/weber_1977_expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
| 10 | 49.79% | 82.99% | 88.09% | 95.35% |
| ∞ | 0.00% | 86.60% | 92.25% | 100.00% |
"""
from numpy import round, sqrt
from numpy import sqrt
from numpy.testing import assert_, assert_almost_equal


Expand Down Expand Up @@ -86,15 +86,19 @@ def eff_vote_for_or_against_k(m, k):
"""
Calculate effectiveness of the "vote-for-or-against-k" voting system.

This is a variant of combined approval voting (CAV) in which every voter
approves or disapproves of `k` candidates.
Weber's closed-form value for impartial culture in the infinite-voter
limit (Cowles DP 498). It is not asserted here to coincide with finite
Monte Carlo Social Utility Efficiency for any particular ballot generator;
compare against :func:`elsim.strategies.vote_for_or_against_k` in
``examples/weber_1977_effectiveness_table.py`` if desired.

Parameters
----------
m : int
Total number of candidates.
k : int
Number of candidates that each voter approves or disapproves of.
Number of candidates approved (``+1``) or disapproved (``-1``) on each
ballot in the vote-for-or-against-``k`` system analyzed in the paper.

Returns
-------
Expand All @@ -120,11 +124,18 @@ def best_vote_for_or_against_k(m):

Returns
-------
k : float
Number of candidates for every voter to approve or disapprove.
k : int
Number of candidates in each voter's for- or against-set (Weber allows
``k = m/2`` when ``m`` is even; otherwise ``1 <= k <= m // 2``).
"""
alpha = (9 - sqrt(21))/12
return round(alpha * m)
best_k = 1
best_eff = eff_vote_for_or_against_k(m, 1)
for k in range(2, m // 2 + 1):
e = eff_vote_for_or_against_k(m, k)
if e > best_eff:
best_eff = e
best_k = k
return best_k


def eff_best_vote_for_or_against_k(m):
Expand Down Expand Up @@ -203,22 +214,35 @@ def test_cases():
assert_almost_equal(eff_best_vote_for_or_against_k(4), 80.83/100, 4)
assert_almost_equal(eff_borda(6), 92.58/100, decimal=4)

# Discrete optimum can differ from round(alpha * m); e.g. m == 91.
assert best_vote_for_or_against_k(91) == 34


if __name__ == '__main__':
test_cases()

from numpy import array
from numpy import array, concatenate, sqrt
from tabulate import tabulate

m_finite = (2, 3, 4, 5, 6, 10)
m_arr = array(m_finite, dtype=float)
table = {}
m_cands_list = (2, 3, 4, 5, 6, 10, 1e30)
for m in m_cands_list:
for name, method in (('Standard', eff_standard),
('Vote-for-half', eff_vote_for_half),
('Best Vote-for-or-against-k',
eff_best_vote_for_or_against_k),
('Borda', eff_borda)):
table.update({name: method(array(m_cands_list))})

print(tabulate(table, 'keys', showindex=m_cands_list[:-1] + ('∞',),
for name, method in (('Standard', eff_standard),
('Vote-for-half', eff_vote_for_half),
('Borda', eff_borda)):
table[name] = method(m_arr)
table['Best Vote-for-or-against-k'] = array(
[eff_best_vote_for_or_against_k(m) for m in m_finite])

lim_best = float((42 * sqrt(21) - 138)**0.5 / 8)
inf_values = {
'Standard': 0.0,
'Vote-for-half': float(sqrt(3) / 2),
'Borda': 1.0,
'Best Vote-for-or-against-k': lim_best,
}
for name in table:
table[name] = concatenate([table[name], [inf_values[name]]])

print(tabulate(table, 'keys', showindex=m_finite + ('∞',),
tablefmt="pipe", floatfmt='.2%'))
Loading
Loading