Czech 2021 parliamentary elections

This example shows a complicated multi-constituency proportional system that was newly introduced the same year.

[1]:
%load_ext autoreload
%autoreload 2
[2]:
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.

The country is divided into fourteen regions that form separate constituencies with separate regional lists. The number of seats distributed within each region is determined from the national total of 200 seats by the Hare quota computed on the national vote count total. There is also a national 5% electoral threshold.

[3]:
eliminator = votelib.evaluate.threshold.RelativeThreshold(
    decimal.Decimal('.05'), accept_equal=True
)
apportioner = votelib.evaluate.proportional.LargestRemainder('hare')

The evaluator has two stages. In the first stage, seats are allocated within regions using the Imperiali quota. The awarded seat counts are rounded down - there is no largest remainder closeup, and so not all seats are awarded; these are transferred to the second stage.

[64]:
regional_evaluator = votelib.evaluate.proportional.QuotaDistributor('imperiali', on_overaward='subtract')

stage1_evaluator = votelib.evaluate.core.ByConstituency(regional_evaluator)

In the second stage, the votes “unused” in the regional stage are summed across regions and the remaining seats allocated according to them using the Droop quota, now using the largest remainder rule to distribute all seats. The second stage seats are redistributed back to the regions according to the regional remainders and the candidates elected are determined through open lists, but we will not go into that detail here.

[52]:
stage2_evaluator = votelib.evaluate.core.RemovedApportionment(
    votelib.evaluate.core.ByParty(
        votelib.evaluate.proportional.LargestRemainder('droop')
    )
)

evaluator = votelib.evaluate.core.FixedSeatCount(
    votelib.evaluate.core.PreApportioned(
        evaluator=votelib.evaluate.core.Conditioned(
            evaluator=votelib.evaluate.core.UnusedVotesDistributor(
                [stage1_evaluator, stage2_evaluator],
                quota_functions=[regional_evaluator.quota_function],
                depth=2
            ),
            eliminator=eliminator,
            depth=2,
        ),
        apportioner=apportioner,
    ),
    200
)

country_evaluator = votelib.evaluate.core.PostConverted(evaluator, votelib.convert.MergedDistributions())

Vote loading

Now we load the vote counts, per region.

[6]:
fpath = os.path.join('..', '..', 'tests', 'real', 'data', 'cz_psp_2021.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)))
{'SPOLU': 84166, 'ANO': 79463, 'Piráti+STAN': 44551, 'SPD': 26661, 'ČSSD': 14534, 'PŘÍSAHA': 13930, 'KSČM': 10150, 'TSS': 8663, 'Volný blok': 3941, 'Zelení': 3185, 'OtČe': 1736, 'Švýcar. demokr.': 981, 'APB': 692, 'PRAMENY': 621, 'Monarchiste.cz': 438, 'Nevolte Urza.cz': 430, 'ANS': 273, 'PB': 0, 'Levice': 0, 'SENIOŘI': 0, 'MZH': 0, 'Moravané': 0}

Performing the evaluation

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

[65]:
evaluator.evaluate(votes)
[65]:
{'Středočeský kraj': {'SPD': 2, 'SPOLU': 10, 'Piráti+STAN': 6, 'ANO': 8},
 'Jihomoravský kraj': {'SPD': 2, 'SPOLU': 9, 'Piráti+STAN': 4, 'ANO': 8},
 'Olomoucký kraj': {'SPD': 1, 'SPOLU': 4, 'Piráti+STAN': 2, 'ANO': 5},
 'Karlovarský kraj': {'SPD': 1, 'SPOLU': 1, 'Piráti+STAN': 1, 'ANO': 2},
 'Moravskoslezský kraj': {'SPD': 3, 'SPOLU': 6, 'Piráti+STAN': 3, 'ANO': 10},
 'Kraj Vysočina': {'SPD': 1, 'SPOLU': 4, 'Piráti+STAN': 1, 'ANO': 4},
 'Plzeňský kraj': {'SPD': 1, 'SPOLU': 4, 'Piráti+STAN': 2, 'ANO': 4},
 'Pardubický kraj': {'SPD': 1, 'SPOLU': 4, 'Piráti+STAN': 2, 'ANO': 3},
 'Liberecký kraj': {'SPD': 1, 'SPOLU': 2, 'Piráti+STAN': 2, 'ANO': 3},
 'Jihočeský kraj': {'SPD': 1, 'SPOLU': 5, 'Piráti+STAN': 2, 'ANO': 5},
 'Ústecký kraj': {'SPD': 2, 'SPOLU': 3, 'Piráti+STAN': 2, 'ANO': 7},
 'Zlínský kraj': {'SPD': 2, 'SPOLU': 4, 'Piráti+STAN': 2, 'ANO': 4},
 'Královéhradecký kraj': {'SPD': 1, 'SPOLU': 4, 'Piráti+STAN': 2, 'ANO': 4},
 'Hlavní město Praha': {'SPD': 1, 'SPOLU': 11, 'Piráti+STAN': 6, 'ANO': 5}}
[66]:
print(country_evaluator.evaluate(votes))
{'SPD': 20, 'SPOLU': 71, 'Piráti+STAN': 37, 'ANO': 72}