Czech 2017 parliamentary elections

This example shows proportional elections in the presence of multiple constituencies, where seats are allocated to constituencies on-line by the number of votes cast in them.

[1]:
import sys
import os
import csv
import decimal

sys.path.append(os.path.join('..', '..'))
import votelib.candidate
import votelib.evaluate.core
import votelib.evaluate.threshold
import votelib.evaluate.proportional

Evaluator construction

First, we construct the evaluator.

Czechia uses the d’Hondt highest averages proportional system in fourteen regions that form separate constituencies with separate regional lists. There is a national 5% electoral threshold.

[2]:
eliminator = votelib.evaluate.threshold.RelativeThreshold(
    decimal.Decimal('.05'), accept_equal=True
)
regional_evaluator = votelib.evaluate.proportional.HighestAverages('d_hondt')

The number of seats for each region is determined by the total number of votes cast in the region by the Hare quota from a total of 200 seats. The candidates elected are determined through open lists, but we will not go into that detail here.

[3]:
apportioner = votelib.evaluate.proportional.LargestRemainder('hare')
combined_evaluator = votelib.evaluate.core.ByConstituency(
    regional_evaluator, apportioner, preselector=eliminator
)
evaluator = votelib.evaluate.core.FixedSeatCount(
    votelib.evaluate.core.PostConverted(
        combined_evaluator,
        votelib.convert.VoteTotals(),
    ),
    200
)

Vote loading

Now we load the vote counts, per region.

[4]:
fpath = os.path.join('..', '..', 'tests', 'real', 'data', 'cz_psp_2017.csv')
with open(fpath, encoding='utf8') as infile:
    rows = list(csv.reader(infile, delimiter=';'))
region_names = rows[0][1:]
votes = {region: {} for region in region_names}
for row in rows[1:]:
    party = row[0]
    for regname, n_votes in zip(region_names, row[1:]):
        votes[regname][party] = int(n_votes)
print(dict(sorted(votes['Královéhradecký kraj'].items(), key=lambda x: x[1], reverse=True)))
{'ANO': 88551, 'ODS': 32242, 'Piráti': 29932, 'SPD': 28038, 'KSČM': 19792, 'ČSSD': 18128, 'KDU-ČSL': 16294, 'TOP 09': 14308, 'STAN': 14184, 'Svobodní': 4310, 'Zelení': 3650, 'Rozumní': 2388, 'REAL': 2037, 'SPO': 851, 'SPRRSČ M.Sládka': 619, 'DSSS': 600, 'ŘN - VU': 531, 'SPORTOVCI': 431, 'DV 2016': 380, 'ODA': 322, 'CESTA': 297, 'BPI': 284, 'Referendum o EU': 276, 'RČ': 275, 'PB': 0, 'SPDV': 0, 'OBČANÉ 2011': 0, 'Unie H.A.V.E.L.': 0, 'ČNF': 0, 'ČSNS': 0, 'NáS': 0}

Performing the evaluation

When the evaluator is set up correctly, obtaining the result is simple.

[5]:
print(evaluator.evaluate(votes))
{'ANO': 78, 'Piráti': 22, 'ODS': 25, 'TOP 09': 7, 'SPD': 22, 'ČSSD': 15, 'STAN': 6, 'KDU-ČSL': 10, 'KSČM': 15}

We can also check the results for individual regions by taking the outputs of combined_evaluator.

[6]:
print(combined_evaluator.evaluate(votes, 200)['Královéhradecký kraj'])
{'ANO': 5, 'ODS': 1, 'Piráti': 1, 'SPD': 1, 'KSČM': 1, 'ČSSD': 1, 'KDU-ČSL': 1}

Alternative systems

In this section, we examine the results that would be achieved under different alternative voting systems.

Sainte-Laguë divisor

Using the Sainte-Laguë divisor helps smaller and mid-sized parties that d’Hondt tends to disfavor; however, the small size of some regions will still impact them. We construct the evaluator analogously, just using the different divisor.

[7]:
sainte_lague_evaluator = votelib.evaluate.core.PostConverted(
    votelib.evaluate.core.ByConstituency(
        votelib.evaluate.proportional.HighestAverages('sainte_lague'),
        apportioner,
        preselector=eliminator
    ),
    votelib.convert.VoteTotals(),
)
print(sainte_lague_evaluator.evaluate(votes, 200))
{'ANO': 63, 'Piráti': 22, 'ODS': 24, 'TOP 09': 11, 'SPD': 23, 'ČSSD': 16, 'STAN': 14, 'KDU-ČSL': 11, 'KSČM': 16}

Abolishing the electoral threshold

If we abolish the electoral threshold and keep the d’Hondt divisor, nothing happens in this particular case - no party apart from those over 5% is strong enough to get hold of a single seat. (Under Sainte-Laguë, Zelení and Svobodní would each get a single seat.)

[8]:
no_threshold_evaluator = votelib.evaluate.core.PostConverted(
    votelib.evaluate.core.ByConstituency(
        votelib.evaluate.proportional.HighestAverages('d_hondt'),
        apportioner,
    ),
    votelib.convert.VoteTotals(),
)
print(no_threshold_evaluator.evaluate(votes, 200))
{'ANO': 78, 'Piráti': 22, 'ODS': 25, 'TOP 09': 7, 'SPD': 22, 'ČSSD': 15, 'STAN': 6, 'KDU-ČSL': 10, 'KSČM': 15}

Biproportional apportionment

Applying biproportional apportionment results in a similar result to applying the Sainte-Laguë divisor in this case even with the d’Hondt divisor, since we honor nationwide party vote fractions.

Since BiproportionalEvaluator removes the need for ByConstituency, if we want to keep the electoral threshold, we need to introduce it using a separate Conditioned composite .

[9]:
biprop_evaluator = votelib.evaluate.core.PostConverted(
    votelib.evaluate.Conditioned(
        votelib.evaluate.PreConverted(votelib.convert.VoteTotals(), eliminator),
        votelib.evaluate.proportional.BiproportionalEvaluator('d_hondt'),
        # depth=1 because the votes are nested by region and we want to subset parties
        subsetter=votelib.convert.SubsettedVotes(depth=1),
    ),
    votelib.convert.VoteTotals(),
)
print(biprop_evaluator.evaluate(votes, 200))
{'ANO': 64, 'ODS': 24, 'Piráti': 23, 'SPD': 23, 'KSČM': 17, 'ČSSD': 15, 'KDU-ČSL': 12, 'TOP 09': 11, 'STAN': 11}