Skip to content

Commit

Permalink
Merge pull request #36 from simberaj/more-proportionality-measures
Browse files Browse the repository at this point in the history
More proportionality measures
  • Loading branch information
simberaj authored Nov 7, 2020
2 parents a862c68 + c030897 commit 39f8cdb
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 18 deletions.
57 changes: 57 additions & 0 deletions tests/test_measure.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import sys
import os

import pytest

sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import votelib.measure

Expand All @@ -21,8 +23,63 @@
'Green': 1,
}

EQUALS_VALUES = {
'loosemore_hanby': 0,
'rae': 0,
'gallagher': 0,
'regression': 1,
'rose': 1,
'sainte_lague': 0,
'lijphart': 0,
'd_hondt': 1,
}


@pytest.mark.parametrize('index_name', list(EQUALS_VALUES.keys()))
def test_perfect(index_name):
equals = {'A': 7, 'B': 5, 'C': 3}
index_fx = getattr(votelib.measure, index_name)
assert index_fx(equals, equals) == EQUALS_VALUES[index_name]


def test_canada_gallagher():
# taken from https://iscanadafair.ca/gallagher-index/
assert abs(votelib.measure.gallagher(CANADA_2015_VOTES, CANADA_2015_SEATS) - .12) < .001


def test_rae_kalogirou_1():
assert round(votelib.measure.rae(
{'A': 6996, 'B': 3004}, {'A': 53, 'B': 25, 'C': 22}
), 1) == 7.4


def test_lh_kalogirou_1():
assert round(votelib.measure.loosemore_hanby({'A': 68, 'B': 22}, {'A': 2}), 2) == .24


def test_lh_kalogirou_2():
assert round(votelib.measure.loosemore_hanby(
{'A': 68, 'B': 22, 'C': 10},
{'A': 1, 'B': 1}
), 2) == .28


def test_d_hondt_kalogirou_italy_1983():
assert round(votelib.measure.d_hondt(
{'Other': 99924, 'dAosta': 76},
{'Other': 99841, 'dAosta': 159}
), 3) == 2.092


def test_regression_largeparty_bias():
assert votelib.measure.regression(
{'A': 7, 'B': 5, 'C': 3},
{'A': 7, 'B': 5, 'C': 2}
) > 1


def test_regression_smallparty_bias():
assert votelib.measure.regression(
{'A': 7, 'B': 5, 'C': 3},
{'A': 7, 'B': 5, 'C': 4}
) < 1
152 changes: 134 additions & 18 deletions votelib/measure.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
"""Measure proportionality of election results.
These functions evaluate how proportionally the seats are allocated to parties
according to their votes received. For a review of such indicators, see
[#polrep]_ or [#kalog]_.
The functions only accept votes in simple format. To evaluate
disproportionality for elections using other vote types, you must use an
appropriate converter first; however, using the index for those election
systems might not be meaningful.
The seat counts must be in a dictionary format as returned by votelib's
distributors.
.. [#polrep] "polrep: Calculate Political Representation Scores", Didier
Ruedin. https://rdrr.io/rforge/polrep/
.. [#kalog] "Measures of disproportionality", Kalogirou.
http://www2.stat-athens.aueb.gr/~jpan/diatrives/Kalogirou/chapter5.pdf
"""


import math
from typing import Dict, Tuple
from numbers import Number
Expand All @@ -17,16 +38,8 @@ def gallagher(votes: Dict[Candidate, Number],
Compared to the Loosemore–Hanby index, it highlights large deviations
rather than small ones.
:param votes: Numbers of votes for each candidate. This function only
accepts votes in a simple format. To evaluate disproportionality
for elections using other vote types, you must use an appropriate
converter first; however, using the index for those election systems
might not be meaningful.
Gallagher also merges or excludes smaller parties or blank votes;
this function does none of that. Apply an appropriate vote filter
first.
:param results: Seat counts awarded to each candidate. This is the
distribution evaluator output format.
:param votes: Numbers of votes for each candidate.
:param results: Seat counts awarded to each candidate.
.. [#lsq] "Gallagher index", Wikipedia.
https://en.wikipedia.org/wiki/Gallagher_index
Expand All @@ -50,24 +63,127 @@ def loosemore_hanby(votes: Dict[Candidate, Number],
Compared to the Gallagher index, it does not diminish the effect of smaller
deviations.
:param votes: Numbers of votes for each candidate. This function only
accepts votes in a simple format. To evaluate disproportionality
for elections using other vote types, you must use an appropriate
converter first; however, using the index for those election systems
might not be meaningful.
:param results: Seat counts awarded to each candidate. This is the
distribution evaluator output format.
:param votes: Numbers of votes for each candidate.
:param results: Seat counts awarded to each candidate.
.. [#lhind] "Loosemore–Hanby index", Wikipedia.
https://en.wikipedia.org/wiki/Loosemore%E2%80%93Hanby_index
"""
paired_fractions = _vote_seat_fractions(votes, results)
return .5 * sum(
vote_frac - seat_frac
abs(vote_frac - seat_frac)
for vote_frac, seat_frac in paired_fractions.values()
)


def rose(votes: Dict[Candidate, Number],
results: Dict[Candidate, Number],
) -> float:
"""Compute the Rose disproportionality index (inverse LH). [#kalog]_
This is an inverted version of the Loosemore-Hanby index which ranges from
1 (no disproportionality) to 0 (total disproportionality).
:param votes: Numbers of votes for each candidate.
:param results: Seat counts awarded to each candidate.
"""
return 1 - loosemore_hanby(votes, results)


def rae(votes: Dict[Candidate, Number],
results: Dict[Candidate, Number],
) -> float:
"""Compute Rae's index of disproportionality. [#kalog]_
Rae's index is the earliest known disproportionality measure. It is known
to underestimate disproportionality in the presence of small parties.
:param votes: Numbers of votes for each candidate.
:param results: Seat counts awarded to each candidate.
"""
paired_fractions = _vote_seat_fractions(votes, results)
print(paired_fractions)
return sum(
abs(vote_frac - seat_frac)
for vote_frac, seat_frac in paired_fractions.values()
) / len(paired_fractions)


def lijphart(votes: Dict[Candidate, Number],
results: Dict[Candidate, Number],
) -> float:
"""Compute the Lijphart's index of disproportionality. [#kalog]_
Lijphart's index takes the single largest difference between vote and seat
fractions.
:param votes: Numbers of votes for each candidate.
:param results: Seat counts awarded to each candidate.
"""
paired_fractions = _vote_seat_fractions(votes, results)
return max(
abs(vote_frac - seat_frac)
for vote_frac, seat_frac in paired_fractions.values()
)


def sainte_lague(votes: Dict[Candidate, Number],
results: Dict[Candidate, Number],
) -> float:
"""Compute the Sainte-Laguë index of disproportionality. [#kalog]_
Sainte-Laguë index takes fractional differences.
:param votes: Numbers of votes for each candidate.
:param results: Seat counts awarded to each candidate.
"""
paired_fractions = _vote_seat_fractions(votes, results)
return sum(
(vote_frac - seat_frac) ** 2 / vote_frac
for vote_frac, seat_frac in paired_fractions.values()
)


def d_hondt(votes: Dict[Candidate, Number],
results: Dict[Candidate, Number],
) -> float:
"""Compute the D'Hondt index of disproportionality. [#kalog]_
This is the index of disproportionality minimized by the D'Hondt highest
averages evaluator. It uses the maximum ratio of seats to votes across
parties.
:param votes: Numbers of votes for each candidate.
:param results: Seat counts awarded to each candidate.
"""
paired_fractions = _vote_seat_fractions(votes, results)
return max(
seat_frac / vote_frac
for vote_frac, seat_frac in paired_fractions.values()
)


def regression(votes: Dict[Candidate, Number],
results: Dict[Candidate, Number],
) -> float:
"""Compute the regression index of disproportionality. [#kalog]_
This is obtained by performing linear regression to predict seat fractions
from vote fractions. If the index is one, the allocation is perfectly
proportional; values below one signal preference of the system for smaller
parties, while values above one signal preference for larger parties.
:param votes: Numbers of votes for each candidate.
:param results: Seat counts awarded to each candidate.
"""
num = 0
denom = 0
for vote_frac, seat_frac in _vote_seat_fractions(votes, results).values():
num += vote_frac * seat_frac
denom += vote_frac ** 2
return num / denom


def _vote_seat_fractions(votes: Dict[Candidate, Number],
results: Dict[Candidate, Number],
) -> Dict[Candidate, Tuple[Number, Number]]:
Expand Down

0 comments on commit 39f8cdb

Please sign in to comment.