Slovak 2020 parliamentary elections

A simple example: evaluating the result of the 2020 parliamentary elections in the Central European country of Slovakia. We choose it because its electoral system is quite simple, as is described below, yet it is not trivial to get the result.

[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.

All of Slovakia forms a single constituency so the votes may be summed over the whole country and there is a single evaluation taking place.

Slovakia uses the Hagenbach-Bischoff largest remainder system (rounded mathematically) to allocate the 150 seats in its National Council to parties. The candidates elected are determined through open lists, but we will not go into that detail here. The evaluator is simple to construct:

[2]:
core_evaluator = votelib.evaluate.proportional.LargestRemainder(
    'hagenbach_bischoff_rounded'
)

Slovakia also uses a minimum vote threshold (as a fraction of national level votes) to exclude small parties. The threshold is 5% for single parties, 7% for two- and three-member coalitions and 10% for four- and more member coalitions. We thus need to construct a pre-selection evaluator that selects only parties over the threshold:

[3]:
# using decimals to be precise
standard_elim = votelib.evaluate.threshold.RelativeThreshold(
    decimal.Decimal('.05'), accept_equal=True # reaching the exact count is sufficient
)
mem_2_3_elim = votelib.evaluate.threshold.RelativeThreshold(
    decimal.Decimal('.07'), accept_equal=True
)
mem_4plus_elim = votelib.evaluate.threshold.RelativeThreshold(
    decimal.Decimal('.1'), accept_equal=True
)
# combine the individual evaluators into a dispatcher based on coalition member count
preselector = votelib.evaluate.threshold.CoalitionMemberBracketer(
    {1: standard_elim, 2: mem_2_3_elim, 3: mem_2_3_elim},
    default=mem_4plus_elim
)

Now, we set up the pre-selector to be a condition for entering the evaluation (all votes for other parties will be simply discarded) and fix the number of seats (the Slovak National Council has 150 seats):

[4]:
evaluator = votelib.evaluate.core.FixedSeatCount(
    votelib.evaluate.core.Conditioned(preselector, core_evaluator), 150
)

Vote loading

Now we load the vote counts. Since all of Slovakia forms a single constituency the votes are already summed over the whole country.

[5]:
fpath = os.path.join('..', '..', 'tests', 'real', 'data', 'sk_nr_2020.csv')
with open(fpath, encoding='utf8') as infile:
    rows = list(csv.reader(infile, delimiter=';'))
    party_names, coalition_flags, votes, seats = [list(x) for x in zip(*rows)]
print(votes)
['721166', '527172', '237531', '229660', '200780', '179246', '166325', '134099', '112662', '91171', '88220', '84507', '59174', '15925', '9260', '8191', '4194', '3296', '2018', '1966', '1887', '1261', '991', '809']

Since the CoalitionMemberBracketer needs ElectionParty objects as candidates to determine coalition sizes, we will need to construct those from the party names and coalition flags (1 if the election party is a coalition).

[6]:
parties = [
    votelib.candidate.Coalition(name=name, parties=[
        votelib.candidate.PoliticalParty(pname)
        for pname in name.split('-')
    ])
    if int(coalflag) else votelib.candidate.PoliticalParty(name)
    for name, coalflag in zip(party_names, coalition_flags)
]

Now, we assemble the votes into a dictionary to be accepted by the evaluator.

[7]:
votes = dict(zip(parties, [int(v) for v in votes]))
votes
[7]:
{<PoliticalParty(OĽANO)>: 721166,
 <PoliticalParty(Smer)>: 527172,
 <PoliticalParty(Sme rodina)>: 237531,
 <PoliticalParty(ĽSNS)>: 229660,
 Coalition([<PoliticalParty(Progresívne Slovensko)>, <PoliticalParty(SPOLU)>]): 200780,
 <PoliticalParty(Sloboda a Solidarita)>: 179246,
 <PoliticalParty(Za ľudí)>: 166325,
 <PoliticalParty(Kresťanskodemokratické hnutie)>: 134099,
 <PoliticalParty(MKÖ-MKS)>: 112662,
 <PoliticalParty(SNS)>: 91171,
 <PoliticalParty(Dobrá voľba)>: 88220,
 <PoliticalParty(Vlasť)>: 84507,
 <PoliticalParty(Most-Híd)>: 59174,
 <PoliticalParty(Socialisti.sk)>: 15925,
 <PoliticalParty(Máme toho dosť!)>: 9260,
 <PoliticalParty(Slovenská ľudová strana Andreja Hlinku)>: 8191,
 <PoliticalParty(Demokratická strana)>: 4194,
 <PoliticalParty(Solidarita-HPCh)>: 3296,
 <PoliticalParty(STAROSTOVIA A NEZÁVISLÍ KANDIDÁTI)>: 2018,
 <PoliticalParty(Slovenské Hnutie Obrody)>: 1966,
 <PoliticalParty(Hlas ľudu)>: 1887,
 <PoliticalParty(Práca slovenského národa)>: 1261,
 <PoliticalParty(99 % – občiansky hlas)>: 991,
 <PoliticalParty(Slovenská liga)>: 809}

Performing the evaluation

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

[8]:
evaluated = evaluator.evaluate(votes)
for party, mandates in evaluated.items():
    print(party.name.ljust(30), mandates)
OĽANO                          53
Smer                           38
Sme rodina                     17
ĽSNS                           17
Sloboda a Solidarita           13
Za ľudí                        12

We see that six parties were elected; although PS-Spolu had more votes than either SaS or Za ľudí, it was excluded by the higher threshold for coalitions.