Method to proportionally allocate seats among parties/lists and districts/regions/entities ('Doppelter Pukelsheim').

  new_seats_col = "seats",
  use_list_votes = TRUE,
  winner_take_one = FALSE



data.frame (long format) with 3 columns (actual colnames can differ):

  • party id/name

  • district id/name

  • votes


data.frame with 2 columns (actual colnames can differ):

  • district id/name

  • number of seats for a district


Optional list of functions which take the votes_matrix and return a logical vector that denotes for each list/party whether they reached the quorum (i.e. are eligible for seats). The easiest way to do this is via quorum_any() or quorum_all(), see examples. Alternatively you can pass a precalculated logical vector. No quorum is applied if parameter is missing or NULL.


name of the new column


By default (TRUE) it's assumed that each voter in a district has as many votes as there are seats in a district. Set to FALSE if votes_df shows the number of voters (e.g. they can only vote for one party).


Set to TRUE if the party that got the most votes in a district must get at least one seat ('Majorzbedingung') in this district. Default is FALSE.


A data.frame like votes_df with a new column denoting the number seats per party and district. Party and district divisors stored in attributes in attributes (hidden from print, see get_divisors()).


Each party nominates a candidate list for every district. The voters vote for the parties of their district. The seat allocation is calculated in two steps:

  1. In the so called upper apportionment the number of seats for each party (over all districts) is determined.

  2. In the so called lower apportionment the seats are distributed to the regional party list respecting the results from the upper apportionment.

Parties failing to reach quorums cannot get seats. This function does not handle seat assignment to candidates.

If you want to use other apportion methods than Sainte-Laguë use biproporz().

See also

This function calls biproporz() after preparing the input data.


# Zug 2018
votes_df = unique(zug2018[c("list_id", "entity_id", "list_votes")])
district_seats_df = unique(zug2018[c("entity_id", "election_mandates")])

seats_df = pukelsheim(votes_df,
                      quorum_any(any_district = 0.05, total = 0.03),
                      winner_take_one = TRUE)

#>   list_id entity_id list_votes seats
#> 1       2      1701       8108     2
#> 2       1      1701       2993     0
#> 3       3      1701      19389     3
#> 4       4      1701      14814     2
#> 5       5      1701       4486     1
#> 6       6      1701      15695     3

# Finland 2019
finland19_result = pukelsheim(finland2019$votes_df,
                             new_seats_col = "mandates",
                             use_list_votes = FALSE)
#>   party_name district_name  votes mandates
#> 4         PS           UUS  86691        5
#> 5        KOK           HEL  84141        5
#> 7       VIHR           UUS  73626        5
#> 2        SDP           UUS  97107        6
#> 6       KESK           OUL  78486        6
#> 1        KOK           UUS 114243        7