Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add "winner take one" method #10

Merged
merged 16 commits into from
Mar 25, 2024
Merged

add "winner take one" method #10

merged 16 commits into from
Mar 25, 2024

Conversation

polettif
Copy link
Owner

@polettif polettif commented Mar 24, 2024

This PR adds the winner take one (WTO) method to biproportional() and pukelsheim(). It adds a condition that the party that got the most votes within a district must get at least one seat in that district ("Majorzbedingung" in German). Literature in English is somewhat sparse, I took the implementation and name mainly from Doppelproporz bei Parlamentswahlen - Ein Rück- und Ausblick (Friedrich Pukelsheim and Christian Schumacher, October 2011).

New parameters in biproporz() and pukelsheim()

The method is used by setting the parameters biproporz(..., method = "wto") or pukelsheim(..., winner_take_one = TRUE). Here's an example:

(vm = matrix(c(200,100,10,11), 2, dimnames = list(c("Party A", "Party B"), c("I", "II"))))
#>           I II
#> Party A 200 10
#> Party B 100 11
district_seats = setNames(c(2,1), c("I", "II"))

biproporz(vm, district_seats, method = "wto")
#>         I II
#> Party A 2  0
#> Party B 0  1

biproporz(vm, district_seats, method = "round")
#>         I II
#> Party A 1  1
#> Party B 1  0

Grossratswahl Graubünden 2022

I tested the method with the cantonal parliament election in the canton of the Grisons 2022. Interestingly, there was a tied majority in one district (Rheinwald), where two parties got 97 votes for one seat. There was some debate on how this should be handled, the election office chose to suspend the WTO condition in this district. Another way would have been to randomly choose a winner in this district.

I generally prefer to throw an error if behavior is undefined, especially with ties. However, in this case it's more complicated to actually model the behavior of said election if biproporz() throws an error. If you modify the votes matrix beforehand (to break the tie), the WTO condition in previously-tied districts can lead to ripple effects in other districts which goes beyond simply "breaking a tie". If WTO is not applied in a tied district, the seat actually goes to the party where the lower apportionment satisfies the upper apportionment conditions best. It is unlikely that the seat goes to a third party (i.e. not one of the tied parties).

The package handles this issue in each district as follows:

  • Is there only one party with the most votes?
    • Yes: Use wto and carry on
    • No: Are there enough seats in this district to give a seat to each of the tied parties?
      • Yes: Use wto and carry on
      • No: Issue a warning and don't apply wto in this district. Standard rounding is used, as if there's no winner in this district (which in a way describes the issue correctly).
GR_2022 = proporz:::testdata$GR_2022
x = biproporz(t(GR_2022$votes_matrix), GR_2022$district_seats_df, 
              method = "wto", quorum = quorum_all(total = 0.03))
#> Warning: Not enough seats for tied parties with the most votes in: 'Rheinwald'
#> Winner take one condition is not applied in this district.

As this is a rather special case, the data set is not exported and only used for unit tests.

Election system in Zug

While working on this I also realized that Zug actually uses the WTO method. However, applying this method does not change the seat distribution for the zug2018 data set that's why I didn't realize it sooner. I updated the readme and examples accordingly. That way we also don't need an additional exported dataset to illustrate using the method.

Minor changes

This PR closes #6. Other changes that came up while working on this:

  • added two more test for real life data: Zurich 2019 and Aargau 2020, both kept internal
  • fixed pivot_to_df() if row and colnames are set but header names (names(dimnames(...))) are missing
  • rounding functions in lower_apportionment now get the district and party names via matrix dimnames. This is necessary to know who the district winner is. That's also why unnamed matrices don't work with method="wto"
  • separated prettify divisor functions
  • updated documentation, refactored tests and functions

@polettif polettif merged commit b46e7f4 into master Mar 25, 2024
1 check passed
@polettif polettif deleted the dev/winner_take_one branch March 25, 2024 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

add feature "winner takes at least one"
1 participant