Skip to content

Instantly share code, notes, and snippets.

@endolith
Last active December 27, 2020 18:57
Show Gist options
  • Save endolith/fa1d19767e5c2e9d4bd15e391ba79f91 to your computer and use it in GitHub Desktop.
Save endolith/fa1d19767e5c2e9d4bd15e391ba79f91 to your computer and use it in GitHub Desktop.
Election simulator
# -*- coding: utf-8 -*-
"""
Created on Sat Dec 17 11:33:35 2016
"""
from __future__ import division, print_function
from numpy.random import multivariate_normal
import matplotlib.pyplot as plt
from scipy.spatial.distance import cdist
import numpy as np
# Polarization between the two main factions
pol = np.random.randn(1)[0]
voters = np.concatenate((multivariate_normal(mean=(-pol, -pol),
cov=((1, 0), (0, 1)), size=500),
multivariate_normal(mean=(+pol, +pol),
cov=((1, 0), (0, 1)), size=500)
))
# Candidates distributed similarly to voters
candidates = multivariate_normal(mean=(0, 0),
cov=np.cov(voters, rowvar=False),
size=9
)
plt.figure()
plt.scatter(voters[:, 0], voters[:, 1], color='b', marker='.', alpha=0.2,
label='voters')
plt.scatter(candidates[:, 0], candidates[:, 1], color='r', marker='o',
alpha=0.7, label='candidates')
centroid = np.mean(voters, axis=0)
plt.plot(centroid[0], centroid[1], color='k', marker='+', markersize=10,
ls='none', label='centroid')
dists = cdist(voters, candidates)
def plurality(dists):
"""
Honest plurality voting
"""
# Voters choose the nearest candidate
ballots = np.argmin(dists, axis=1)
# Winner is the candidate with the most votes
winner = np.argmax(np.bincount(ballots))
return ballots, winner
def approval_nearest(dists, n=3):
"""
Honest approval voting of the nearest `n` candidates
"""
# Voters choose the n nearest candidates
ballots = np.argsort(dists, axis=1)[:, :n]
# Winner is the candidate with the most votes
winner = np.argmax(np.bincount(ballots.flat))
return ballots, winner
def approval_half(dists):
"""
Honest approval voting of the nearest 50% of candidates
"""
n_cands = dists.shape[1]
return approval_nearest(dists, n=int(0.5 * n_cands))
def score_normed(dists, levels=None):
"""
Honest score voting, normalized to min and max for worst and best,
optionally quantized to number of `levels`. All candidates are scored, so
total vs average is irrelevant.
"""
# Normalize from 0 to 1 for nearest to farthest
normed = dists - np.amin(dists, axis=1)[:, np.newaxis]
normed /= np.amax(normed, axis=1)[:, np.newaxis]
# Farthest candidates get the lowest scores
normed = 1 - normed
# Optionally quantize to discrete scale
if levels is not None:
normed = np.around(normed * levels)
# Total scores for each candidate
scores = np.sum(normed, axis=0)
# Winner is the candidate with the highest total/average score
winner = np.argmax(scores)
return ballots, winner
def next_color():
ax = plt.gca()
while True:
color = next(ax._get_lines.prop_cycler)['color']
if color not in {'r', }: # , 'b'}:
return color
def next_size():
s = 6
while True:
yield s
s += 2
sizes = next_size()
for func in plurality, approval_nearest, approval_half, score_normed:
name = func.__name__.replace('_', ' ')
ballots, winner = func(dists)
winner_loc = candidates[winner]
plt.plot(winner_loc[0], winner_loc[1], marker='o', markersize=sizes.next(),
ls='none', markeredgewidth=1, markeredgecolor=next_color(),
color='none', # don't increment color cycle for invisible face
markerfacecolor='none', label=name)
plt.legend(loc='lower right', numpoints=1, fontsize='small')
plt.axis('square')
plt.grid(True)
plt.tight_layout()
@endolith
Copy link
Author

endolith commented Jan 6, 2018

Examples:

Utilitarian Winner = B.
FPTP chooses C because of vote-splitting.
IRV chooses A because of center-squeeze.

37390245712_8f6fdd227b_o

Normalized score voting often works well:

32049928782_43b5772b7a_b

but not for very polarized distributions:

32049929012_ca54532e82_b

How close are the winners to the utilitarian ideal in each system?

34421142013_1fce22a20d_o

@endolith
Copy link
Author

More:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment