This document describes the pre-processing and exploratory analysis of the data set that is the basis of article So haben die Schweizer Gemeinden in den letzten 40 Jahren gewählt published on srf.ch.
SRF Data attaches importance to the fact that the data pre-processing and analysis can be reproduced and checked. SRF Data believes in the principle of open data, but also open and comprehensible methods. On the other hand, it should be possible for third parties to build on this preparatory work and thus generate further evaluations or applications.
The preprocessing and analysis of the data was conducted in the R project for statistical computing. The RMarkdown script used to generate this document and all the resulting data can be downloaded under this link. Through executing main.Rmd
, the herein described process can be reproduced and this document can be generated. In the course of this, data from the folder input
will be processed and results will be written to output
.
SRF Data uses Timo Grossenbacher’s rddj-template as the basis for its R scripts. If you have problems executing this script, it may help to study the instructions from the rddj-template.
This report was generated on 2019-06-27 17:06:03. R version: 3.5.3 on x86_64-apple-darwin15.6.0. For this report, CRAN packages as of 2019-03-01 were used.
The code for the herein described process can also be freely downloaded from https://github.com/srfdata/2019-06-party-history.
2019-06-party-history by SRF Data is licensed under a Creative Commons Namensnennung - Attribution ShareAlike 4.0 International License.
Code and data by SRF Data are available on https://srfdata.github.io.
The published information has been carefully compiled, but does not claim to be up-to-date, complete or correct. No liability is assumed for damages arising from the use of this script or the information drawn from it. This also applies to contents of third parties which are accessible via this offer.
The following sections describe the results of the data preprocessing as stored in the output
folder.
output/municipalities.csv
Contains metadata for all 2’240 municipalities as of April 2nd, 2017.
Attribute | Type | Description |
---|---|---|
id | Numeric | Official BFS-number |
name | String | Official name of the municipality |
name_de | String | Contains the “common” German translation if it exists |
name_fr | String | Contains the “common” French translation if it exists |
name_it | String | Contains the “common” Italian translation if it exists |
output/parties.csv
Contains party classifications made by SRF Data with the help of political scientists, used throughout all projects related to elections.
Attribute | Type | Description |
---|---|---|
abbr | String | Abbreviations in D/F |
group_id | Numeric | Unique ID for bigger parties and non-unique identifier for groups |
bloc | Enum | Either “left”, “center” or “right” classifying the political views of this party |
group_name | String | Description for groups (corresponding to group_id) |
output/dominant_party.csv
Party strengths 1975-2015 with only the “dominant” party per year-municipality combination (dominating = the one with the highest support)
Attribute | Type | Description |
---|---|---|
year | Numeric | Election year |
support | Double | Party strength in fraction of 1 (e.g. 0.2 = 20%) |
municipality | Numeric | BFS ID of municipality in question |
canton | Numeric | Cantonal abbreviation (two letters) |
party | Numeric | Party or party grouping, referencing ID in output/parties.csv |
output/national.csv
Party strengths 1971-2011 on the national level.
Note: Might equal to zero if party did not have any candidates that year.
Attribute | Type | Description |
---|---|---|
year | Numeric | Election year |
party | Numeric | Party or party grouping, referencing ID in output/parties.csv |
support | Numeric | Party strength in fraction of 1 (e.g. 0.2 = 20%) |
output/by_municipality/municipality_{id}.csv
Party strengths 1971-2011 in municipality with BFS-number id.
Note: Only those year-party combinations are contained where the party actually had candidates.
Attribute | Type | Description |
---|---|---|
year | Numeric | Election year |
party | Numeric | Party or party grouping, referencing ID in output/parties.csv |
support | Numeric | Party strength in fraction of 1 (e.g. 0.2 = 20%) |
output/by_party/party_{id}.csv
Party strengths 1971-2011 in all municipalities for party with id as defined in output/parties.csv
.
Note: Only those year-municipality combinations are contained where the party actually had candidates.
Attribute | Type | Description |
---|---|---|
municipality | Numeric | Unique identifier, referencing ID in output/municipalities.csv |
year | Numeric | Election year |
party | Numeric | Party or party grouping, referencing ID in output/parties.csv |
support | Numeric | Party strength in fraction of 1 (e.g. 0.2 = 20%) |
We configured the following data cubes from the Federal Statistics Office and downloaded the result as a comma separated file without head:
input/px-x-1702020000_104.csv
input/px-x-1702020000_105.csv
From: https://www.pxweb.bfs.admin.ch/pxweb/de/px-x-1702020000_105/-/px-x-1702020000_105.px
The following meta information was provided by the Statistics Office: Letzte Änderungen (12.01.2018): neuer Gemeindestand Erhebungsstichtag: Wahldatum Stand der Datenbank: 2015 Raumbezug: Gemeinden per 02.04.2017, inkl. Spezialgemeinden der politischen Statistik Erhebung: Statistik der Wahlen und Abstimmungen In verschiedenen Kantonen werden für gewisse Wahlen Spezialgemeinden /-bezirke («extraterritoriale» Gemeinden/Bezirke) ausgewiesen. Es handelt sich dabei vor allem um Auslandschweizer (Ausland-CH), die von mehreren Kantonen separat ausgewiesen werden und um (einzelne) Stimmen die vom jeweiligen Kanton keiner offiziellen Gemeinde zugeordnet wurden (Andere, Korrespondenzweg). Gemeinden mit so genannt «gemeinsamer Urne»: Dabei werden zwei oder mehrere Gemeinden zusammengefasst, deren Stimmzettel gemeinsam ausgezählt werden. 2009: Fusion von FDP und LPS auf nationaler Ebene unter der Bezeichnung «FDP.Die Liberalen». In der Waadt fusionierten FDP und LP im Jahr 2012, in Basel-Stadt haben FDP und LP nicht fusioniert. Da die LP-BS Mitglied der «FDP.Die Liberalen Schweiz» ist, wird sie zur FDP gezählt. Verwendete Zeichen: ‘…’ : Zahl unbekannt, weil (noch) nicht erhoben oder (noch) nicht berechnet, d.h. keine Kandidatur, stille Wahl oder Gemeinden mit gemeinsamen Urnen.
-> input/su-d-17.02.02.03.02_SPR_SRF-Zehr.xlsx
We downloaded the table from the BFS website but realized that the values for 2015 were only temporary, so we asked poku@bfs.admin.ch
for the updated data that we got per mail in mid June 2019.
It contained the following meta information: 1) Die Ergebnisse nach Sprachregion werden aufgrund der Gemeindeergebnisse bzw. der Zugehörigkeit der Gemeinden zu einer Sprachregion berechnet. Bis 2000 diente die Volkszählung als Grundlage der Zuteilung einer Gemeinde zu einem Sprachgebiet, danach wird die (kumulierte) Strukturerhebung verwendet. 2) 2009: Fusion von FDP und LPS auf nationaler Ebene unter der Bezeichnung “FDP.Die Liberalen”. Fusion von FDP und LP im Kanton Genf im Jahr 2010 und im Kanton Waadt im Jahr 2012. Im Kanton Basel-Stadt haben FDP und LP nicht fusioniert. Da die LP-BS Mitglied der „FDP.Die Liberalen Schweiz“ ist, wird die LP-BS auf gesamtschweizerischer Ebene der FDP zugeteilt.
-> input/Raumgliederungen.xlsx
The classification of each municipality to a language region was extracted from the Application of Swiss Municipalities. The status of the data is the same as in the election data: 2nd of April 2017.
-> input/gd-b-00.03-875-gg17
The shapefiles for municipalities, cantons etc. we downloaded from Generalisierte Gemeindegrenzen. We worked with the files in the folder ggg_2017vz
which contain the status of 31st of December 2017 (which is the same as it was on the 2nd of April).
## [1] "package package:rmarkdown detached"
## Loading required package: knitr
## Loading required package: rstudioapi
# from https://mran.revolutionanalytics.com/web/packages/checkpoint/vignettes/using-checkpoint-with-knitr.html
# if you don't need a package, remove it from here (commenting is probably not sufficient)
# tidyverse: see https://blog.rstudio.org/2016/09/15/tidyverse-1-0-0/
cat("
library(rstudioapi)
library(tidyverse) # ggplot2, dplyr, tidyr, readr, purrr, tibble
library(ggrepel) # repelling texts for ggplot
library(glue) # easier templating output
library(magrittr) # pipes
library(lintr) # code linting
library(sf) # mapping
library(rmarkdown) # needed for automated knitting",
file = "manifest.R")
# if checkpoint is not yet installed, install it (for people using this
# system for the first time)
if (!require(checkpoint)) {
if (!require(devtools)) {
install.packages("devtools", repos = "http://cran.us.r-project.org")
require(devtools)
}
devtools::install_github("RevolutionAnalytics/checkpoint",
ref = "v0.3.2", # could be adapted later,
# as of now (beginning of July 2017
# this is the current release on CRAN)
repos = "http://cran.us.r-project.org")
require(checkpoint)
}
# nolint start
if (!dir.exists("~/.checkpoint")) {
dir.create("~/.checkpoint")
}
# nolint end
# install packages for the specified CRAN snapshot date
checkpoint(snapshotDate = package_date,
project = path_to_wd,
verbose = T,
scanForPackages = T,
use.knitr = F,
R.version = R_version)
rm(package_date)
source("manifest.R")
unlink("manifest.R")
sessionInfo()
## R version 3.5.3 Patched (2019-03-11 r76221)
## Platform: x86_64-apple-darwin15.6.0 (64-bit)
## Running under: macOS Sierra 10.12.6
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/3.5/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/3.5/Resources/lib/libRlapack.dylib
##
## locale:
## [1] de_CH.UTF-8/de_CH.UTF-8/de_CH.UTF-8/C/de_CH.UTF-8/de_CH.UTF-8
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] rmarkdown_1.11 sf_0.7-3 lintr_1.0.3 magrittr_1.5
## [5] glue_1.3.0 ggrepel_0.8.0 forcats_0.4.0 stringr_1.4.0
## [9] dplyr_0.8.0.1 purrr_0.3.0 readr_1.3.1 tidyr_0.8.2
## [13] tibble_2.0.1 ggplot2_3.1.0 tidyverse_1.2.1 checkpoint_0.4.0
## [17] rstudioapi_0.9.0 knitr_1.21
##
## loaded via a namespace (and not attached):
## [1] tidyselect_0.2.5 xfun_0.5 haven_2.1.0 lattice_0.20-38
## [5] colorspace_1.4-0 generics_0.0.2 htmltools_0.3.6 yaml_2.2.0
## [9] rlang_0.3.1 e1071_1.7-0.1 pillar_1.3.1 withr_2.1.2
## [13] DBI_1.0.0 modelr_0.1.4 readxl_1.3.0 plyr_1.8.4
## [17] munsell_0.5.0 gtable_0.2.0 cellranger_1.1.0 rvest_0.3.2
## [21] evaluate_0.13 rex_1.1.2 class_7.3-15 broom_0.5.1
## [25] Rcpp_1.0.0 classInt_0.3-1 scales_1.0.0 backports_1.1.3
## [29] jsonlite_1.6 hms_0.4.2 digest_0.6.18 stringi_1.3.1
## [33] grid_3.5.3 cli_1.0.1 tools_3.5.3 lazyeval_0.2.1
## [37] crayon_1.3.4 pkgconfig_2.0.2 xml2_1.2.0 lubridate_1.7.4
## [41] assertthat_0.2.0 httr_1.4.0 R6_2.4.0 units_0.6-2
## [45] nlme_3.1-137 compiler_3.5.3
# clean up
rm(detach_all_packages, path_to_wd, project_name, R_version, user_name)
# prepare helper function that converts names to url-safe strings
slugify <- function(string) {
string %>%
tolower() %>%
str_replace_all(c(
"ü" = "ue", "[öœ]" = "oe", "[äæ]" = "ae", "ç" = "c", "[ôò]" = "o",
"[éèêë]" = "e", "[àáâ]" = "a", "[îïì]" = "i", "[ùû]" = "u"
)) %>%
str_replace_all("[^\\w]+", "-") %>%
str_replace_all("[^\\w]$", "")
}
# read election data
data <- read_csv(
"input/px-x-1702020000_105.csv"
) %>%
rename(
municipality = 1,
year = 2,
party = 3,
support = 4
) %>%
# convert do number (decimals)
mutate(support = ifelse(support == "...", NA, as.numeric(support) / 100)) %>%
# keep municipalities only
filter(str_detect(municipality, "^...... ")) %>%
# remove dots
mutate(municipality = str_replace(municipality, "^...... ", "")) %>%
# remove (Gemeinsame Urne) from end of municipality names
mutate(municipality = str_replace(municipality, " \\(Gemeinsame Urne\\)", ""))
## Parsed with column specification:
## cols(
## `Bezirk (>>) / Gemeinde (......)` = col_character(),
## Jahr = col_double(),
## Partei = col_character(),
## `Parteistärke in %` = col_character()
## )
## Warning in ifelse(support == "...", NA, as.numeric(support)/100): NAs durch
## Umwandlung erzeugt
In our data we have 11 election years (every four years from 1975 to 2015) for 2266 municipalities.
From the 24 parties that we encounter in the data set, we form the following groups:
# read party classification
parties <- read_csv("input/parties.csv")
## Parsed with column specification:
## cols(
## abbr = col_character(),
## group_id = col_double()
## )
# output
knitr::kable(
parties %>%
group_by(group_id) %>%
summarise(abbr = glue_collapse(abbr, sep = ", "))
)
group_id | abbr |
---|---|
1 | FGA/AVF, PdA/PST, POCH, PSA, Sol. |
2 | SP/PS |
3 | GPS/PES |
4 | EVP/PEV |
5 | GLP/PVL |
6 | BDP/PBD |
7 | CSP/PCS, LdU/AdI, Sep./Sép., Übrige/Autres |
8 | CVP/PDC |
9 | FDP/PLR (PRD), LPS/PLS |
10 | Lega |
11 | SVP/UDC |
12 | EDU/UDF, FPS/PSL, MCR, Rep./Rép., SD/DS |
For some plots and also for the BFS IDs of the municipalities we read in the municipality boundaries that we got from the Federal Statistics Office at https://www.bfs.admin.ch/bfs/de/home/dienstleistungen/geostat/geodaten-bundesstatistik/administrative-grenzen/generalisierte-gemeindegrenzen.assetdetail.4342877.html.
municipality_geo <- read_sf(
"input/gd-b-00.03-875-gg17/ggg_2017vz/shp/LV95/g2g17vz.shp",
options = "ENCODING=ISO-8859-1"
)
# join bfs_id into municipality data if not done yet
if (!"bfs_id" %in% colnames(data)) {
data %<>%
left_join(
municipality_geo %>%
# remove geometry from join
st_drop_geometry() %>%
select(bfs_id = GMDNR, GMDNAME, KTNR),
by = c("municipality" = "GMDNAME")
) %>%
# replace the canton ids with canton names
left_join(
read_csv("input/canton_names.csv") %>%
select(id, canton = abbr),
by = c("KTNR" = "id")
) %>%
select(-KTNR)
}
## Parsed with column specification:
## cols(
## id = col_double(),
## abbr = col_character(),
## name = col_character(),
## regi = col_character()
## )
# check if any municipalities still miss their id
knitr::kable(
data %>%
distinct(municipality, bfs_id) %>%
filter(is.na(bfs_id))
)
municipality | bfs_id |
---|---|
Niederösch | NA |
Oberösch | NA |
ZH-Ausland-CH | NA |
LU-Ausland-CH | NA |
UR-Ausland-CH | NA |
FR-CH de l’étranger | NA |
BS-Ausland-CH | NA |
AI-Ausland-CH | NA |
SG-Ausland-CH | NA |
AG-Ausland-CH | NA |
TG-Ausland-CH | NA |
VD-CH de l’étranger | NA |
VS-CH de l’étranger | NA |
GE-CH de l’étranger | NA |
AI-Korrespondenzweg | NA |
AI-Anderes | NA |
TI-voto per corrispondenza | NA |
TI-altri | NA |
ZH-Anderes | NA |
BE-Anderes | NA |
SZ-Anderes | NA |
SO-Anderes | NA |
GR-Anderes | NA |
VD-autres | NA |
VS-autres | NA |
GE-autres | NA |
The only entries thate are missing a bfs_id are now entries from places not in Switzerland. And Niederösch and Oberösch, but we’ll just ignore these as their values are NA anyway. (Since 2016 they belong to Ersigen). Per mail we got confirmed that these two entries should not be in the data at all.
Let’s check if there are municipalities on our map that are missing from the data:
knitr::kable(
municipality_geo %>%
anti_join(
data,
by = c("GMDNAME" = "municipality")
) %>%
select(GMDNR, GMDNAME, BZNR, KTNR) %>%
st_drop_geometry()
)
GMDNR GMDNAME BZNR KTNR —— ——– —– —–
That’s not the case, that’s good.
First: let’s try to find a meaningful cut off for elections. Those cantons that only have 1 national council will yield some extreme results. To answer the questions below we’ll need to exclude them from some of the analysis.
What’s the mean number of parties running for seats per canton?
# calculate number of parties per year and canton and election
parties_per_canton_and_year <- data %>%
filter(!is.na(support) & !is.na(canton)) %>%
group_by(canton, year, municipality) %>%
tally(name = "n_parties") %>%
select(-municipality) %>%
slice(1) %>%
ungroup()
# view min, max, mean, median
parties_per_canton <- parties_per_canton_and_year %>%
group_by(canton) %>%
summarise(
min = min(n_parties),
max = max(n_parties),
median = median(n_parties),
mean = mean(n_parties)
) %>%
arrange(desc(mean)) %>%
ungroup()
# output
knitr::kable(
parties_per_canton
)
canton | min | max | median | mean |
---|---|---|---|---|
ZH | 11 | 14 | 13.0 | 12.909091 |
JU | 11 | 14 | 13.0 | 12.818182 |
BE | 10 | 14 | 12.0 | 12.454546 |
VD | 9 | 15 | 12.0 | 11.636364 |
BS | 9 | 13 | 10.0 | 10.636364 |
AG | 8 | 12 | 10.0 | 10.181818 |
GE | 8 | 12 | 9.0 | 9.636364 |
BL | 8 | 11 | 9.0 | 9.090909 |
TG | 7 | 10 | 9.0 | 9.000000 |
SG | 5 | 11 | 9.0 | 8.727273 |
FR | 4 | 11 | 8.0 | 7.727273 |
TI | 6 | 9 | 8.0 | 7.727273 |
LU | 5 | 10 | 7.0 | 7.454546 |
NE | 5 | 9 | 7.0 | 7.272727 |
SO | 5 | 10 | 7.0 | 7.272727 |
GR | 4 | 8 | 6.0 | 6.000000 |
VS | 4 | 8 | 5.0 | 5.727273 |
SZ | 3 | 8 | 5.0 | 5.272727 |
ZG | 4 | 7 | 5.0 | 5.181818 |
SH | 3 | 7 | 5.0 | 4.909091 |
AR | 2 | 6 | 4.0 | 3.888889 |
UR | 2 | 4 | 3.0 | 2.727273 |
NW | 2 | 4 | 2.5 | 2.600000 |
OW | 2 | 4 | 2.0 | 2.500000 |
GL | 2 | 3 | 2.0 | 2.363636 |
AI | 2 | 3 | 2.0 | 2.272727 |
top_20_cantons <- parties_per_canton %>%
top_n(20, wt = median) %>%
pull(canton)
If we would for example look at the 20 “biggest” cantons when looking at the numbers of parties that run, the followin 6 would drop out: AR, UR, NW, OW, GL, AI These are also the cantons that currently have only one seat in the national council. So this selection seems to make sense.
To analyze political shifts from left to right and vice-versa, we’ll classify the 12 parties into 3 simple blocs: left, center, right.
if (!"bloc" %in% colnames(parties)) {
parties %<>%
mutate(bloc = case_when(
group_id <= 3 ~ "left",
group_id <= 8 ~ "center",
TRUE ~ "right"
)) %>%
# remove others from classifications
mutate(bloc = ifelse(abbr == "Übrige/Autres", NA, bloc)) %>%
# and also the separatists in BE/JU/BL
mutate(bloc = ifelse(abbr == "Sep./Sép.", NA, bloc)) %>%
mutate(bloc = factor(bloc, levels = c("left", "center", "right")))
}
# output
knitr::kable(
parties %>%
group_by(bloc) %>%
summarise(parties = glue_collapse(abbr, ", "))
)
## Warning: Factor `bloc` contains implicit NA, consider using
## `forcats::fct_explicit_na`
bloc | parties |
---|---|
left | FGA/AVF, PdA/PST, POCH, PSA, Sol., SP/PS, GPS/PES |
center | EVP/PEV, GLP/PVL, BDP/PBD, CSP/PCS, LdU/AdI, CVP/PDC |
right | FDP/PLR (PRD), LPS/PLS, Lega, SVP/UDC, EDU/UDF, FPS/PSL, MCR, Rep./Rép., SD/DS |
NA | Sep./Sép., Übrige/Autres |
Now we can list the municipalities where left parties lost most and right parties won most. By subracting the support of the left bloc from the support of the right bloc we get negative values where the left is bigger than the right. This model is potentially oversimplifying the political system of Switzerland where the centrist parties have quite an important role.
Let’s look at two examples how the position
would look like for a municipality: * Left: 15% and Right: 40% equals position
= 0.25 * Left: 30% and Right: 25% equals position
= -0.05
left_right_positioned_municipalities <- data %>%
# filter out smallest cantons
# temporarily disabled
# filter(canton %in% top_20_cantons) %>%
# join blocs
left_join(
parties %>%
select(abbr, bloc),
by = c("party" = "abbr")
) %>%
# filter out others and those that vote in majoritarian style
filter(!is.na(bloc) & canton %in% top_20_cantons) %>%
group_by(municipality, canton, bfs_id, year, bloc) %>%
summarise(support = sum(support, na.rm = TRUE)) %>%
ungroup() %>%
# disabled: keep only left and right
# filter(bloc != "center") %>%
spread(bloc, support) %>%
mutate(position = right - left)
The most right (first 10) and most left (last 10) towns in the latest year:
# output, try to get a feeling for this classification
knitr::kable(
bind_rows(
left_right_positioned_municipalities %>%
filter(year == max(year)) %>%
top_n(10, wt = position) %>%
arrange(desc(position)),
# inser empty row for clearer distinction
c("municipality" = "---"),
left_right_positioned_municipalities %>%
filter(year == max(year)) %>%
top_n(-10, wt = position) %>%
arrange(desc(position))
)
)
municipality | canton | bfs_id | year | left | center | right | position |
---|---|---|---|---|---|---|---|
Horrenbach-Buchen | BE | 932 | 2015 | 0.021 | 0.072 | 0.902 | 0.881 |
Riemenstalden | SZ | 1369 | 2015 | 0.027 | 0.125 | 0.849 | 0.822 |
Ederswiler | JU | 6713 | 2015 | 0.000 | 0.200 | 0.800 | 0.800 |
Alpthal | SZ | 1361 | 2015 | 0.034 | 0.136 | 0.830 | 0.796 |
Unteriberg | SZ | 1375 | 2015 | 0.040 | 0.144 | 0.815 | 0.775 |
Teuffenthal (BE) | BE | 940 | 2015 | 0.063 | 0.102 | 0.828 | 0.765 |
Schangnau | BE | 906 | 2015 | 0.046 | 0.136 | 0.805 | 0.759 |
St. Stephan | BE | 793 | 2015 | 0.062 | 0.102 | 0.820 | 0.758 |
Rossenges | VD | 5684 | 2015 | 0.099 | 0.051 | 0.849 | 0.750 |
Habkern | BE | 579 | 2015 | 0.067 | 0.103 | 0.816 | 0.749 |
— | NA | NA | NA | NA | NA | NA | NA |
Lausanne | VD | 5586 | 2015 | 0.537 | 0.089 | 0.345 | -0.192 |
Courchapoix | JU | 6706 | 2015 | 0.382 | 0.403 | 0.184 | -0.198 |
Le Locle | NE | 6436 | 2015 | 0.577 | 0.048 | 0.354 | -0.223 |
Fribourg | FR | 2196 | 2015 | 0.461 | 0.311 | 0.228 | -0.233 |
La Chaux-de-Fonds | NE | 6421 | 2015 | 0.581 | 0.050 | 0.340 | -0.241 |
Vellerat | JU | 6728 | 2015 | 0.522 | 0.227 | 0.239 | -0.283 |
Delémont | JU | 6711 | 2015 | 0.507 | 0.261 | 0.216 | -0.291 |
Bern | BE | 351 | 2015 | 0.531 | 0.217 | 0.233 | -0.298 |
Lajoux (JU) | JU | 6750 | 2015 | 0.543 | 0.254 | 0.174 | -0.369 |
Fontenais | JU | 6790 | 2015 | 0.557 | 0.261 | 0.180 | -0.377 |
Let’s further look at how the pricipal towns evolved over time:
principal_towns <- read_csv("input/principal_towns.csv")
## Parsed with column specification:
## cols(
## canton = col_character(),
## value = col_double(),
## name = col_character()
## )
ggplot(
left_right_positioned_municipalities %>%
filter(municipality %in% principal_towns$name),
aes(
x = year,
y = position,
color = municipality,
label = municipality
)
) +
geom_hline(yintercept = 0, size = 0.2) +
geom_line() +
geom_text_repel(
data = left_right_positioned_municipalities %>%
filter(municipality %in% principal_towns$name) %>%
filter(year == max(year)),
direction = "y",
nudge_x = 1,
hjust = 0,
segment.size = 0.2
) +
xlim(min(data$year), max(data$year) + 4) +
theme_minimal() +
theme(legend.position = "none") +
labs(
title = "Principal Towns and their political position from right (top) to left (bottom)",
subtitle = "The value on the y axis can be read as \"how much bigger is the right bloc as the left bloc.\"",
x = NULL,
y = NULL
)
There are several observations which reveal the weaknesses of this system:
Never the less, let’s inspect which municipalities moved from left to right the strongest and vice-versa. At first let’s just look at the first and last election:
left_right_swings_municipalities <- left_right_positioned_municipalities %>%
ungroup() %>%
# filter out silent elections
filter(right > 0 | left > 0) %>%
# keep only first and last year
filter(year == min(year) | year == max(year)) %>%
# remove left and right numbers
select(-left, -right, -center) %>%
spread(year, position) %>%
# replace years in column names with start and end
rename(
start = as.character(min(data$year)),
end = as.character(max(data$year))
) %>%
mutate(delta = end - start)
# output extremes (10 each)
knitr::kable(
bind_rows(
left_right_swings_municipalities %>%
top_n(10, wt = delta) %>%
arrange(desc(delta)),
# inser empty row for clearer distinction
c("municipality" = "---"),
left_right_swings_municipalities %>%
top_n(-10, wt = delta) %>%
arrange(delta)
)
)
municipality | canton | bfs_id | start | end | delta |
---|---|---|---|---|---|
Vorderthal | SZ | 1348 | -0.487 | 0.676 | 1.163 |
Schübelbach | SZ | 1346 | -0.569 | 0.483 | 1.052 |
Dorénaz | VS | 6212 | -0.510 | 0.517 | 1.027 |
Reichenburg | SZ | 1345 | -0.521 | 0.487 | 1.008 |
Wangen (SZ) | SZ | 1349 | -0.585 | 0.422 | 1.007 |
Galgenen | SZ | 1342 | -0.454 | 0.549 | 1.003 |
Illgau | SZ | 1363 | -0.222 | 0.626 | 0.848 |
Alpthal | SZ | 1361 | -0.048 | 0.796 | 0.844 |
Riemenstalden | SZ | 1369 | -0.017 | 0.822 | 0.839 |
Innerthal | SZ | 1343 | -0.333 | 0.478 | 0.811 |
— | NA | NA | NA | NA | NA |
Dizy | VD | 5481 | 0.937 | 0.034 | -0.903 |
Lohn (GR) | GR | 3707 | 0.943 | 0.050 | -0.893 |
Tschappina | GR | 3669 | 0.973 | 0.170 | -0.803 |
Mathon | GR | 3708 | 0.924 | 0.159 | -0.765 |
Safiental | GR | 3672 | 0.873 | 0.120 | -0.753 |
Chavannes-le-Veyron | VD | 5475 | 0.874 | 0.129 | -0.745 |
Kilchberg (BL) | BL | 2851 | 0.713 | 0.005 | -0.708 |
Masein | GR | 3663 | 0.770 | 0.066 | -0.704 |
Urmein | GR | 3670 | 0.990 | 0.292 | -0.698 |
Schelten | BE | 708 | 0.844 | 0.180 | -0.664 |
Not very surprisingly these municipalities are all pretty small - nothing unusual when you look at the most extreme changes.
Let’s visualize the swing data on a map with arrows. Blue will be used for arrows leaning to the right, red for the ones leaning to the left.
# get country borders for quicker display of background
cantons_geo <- read_sf(
"input/gd-b-00.03-875-gg17/ggg_2017/shp/LV95/g2k17.shp",
options = "ENCODING=ISO-8859-1"
)
ggplot() +
# show map of switzerland as a light grey background
geom_sf(
data = cantons_geo,
aes(),
fill = "#f2f2f2",
color = "#FFFFFF"
) +
# add one small line for every municipality at it's center
geom_sf_text(
data = municipality_geo %>%
left_join(
left_right_swings_municipalities,
by = c("GMDNR" = "bfs_id")
) %>%
# filter out irrelevant cantons
filter(!is.na(delta)),
aes(
x = X_CNTR,
y = Y_CNTR,
angle = round(delta * -100),
color = round(delta * -100)
),
label = "↑",
size = 6
) +
scale_colour_gradient2(low = "#1a7ac5", mid = "#959595", high = "#e31f2b") +
theme_void() +
theme(
panel.grid.major = element_line("transparent"),
legend.position = "none"
)
This approach is interesting, but it has some weaknesses:
That’s why we focus on other approaches.
Let’s try different approaches to find out where the parties have their heartlands. First: Where over all years parties got the most number of votes. (in percent values summed)
# TO DO: ADD COLORS
big_7 <- tibble(
"2" = "SPS",
"3" = "GPS",
"5" = "GLP",
"6" = "BDP",
"8" = "CVP",
"9" = "FDP",
"11" = "SVP"
) %>% gather(id, abbr) %>%
mutate(id = as.numeric(id))
# reduce to biggest 7 parties and add LPS to FDP
data_big_7 <- data %>%
left_join(
parties %>%
select(abbr, group_id),
by = c("party" = "abbr")
) %>%
# remove non-municipalities (people abroad)
filter(!is.na(bfs_id)) %>%
# keep only big 7
left_join(big_7, by = c("group_id" = "id")) %>%
filter(!is.na(abbr)) %>%
# remove old notation (where FDP and LPS were separate)
select(-party) %>%
# convert to factors
mutate(
group_id = factor(group_id, levels = big_7$id),
abbr = factor(abbr, levels = big_7$abbr)
) %>%
rename(party = abbr)
# get number of elections in data
n_elections <- data %>%
distinct(year) %>%
nrow()
support_per_municipality_and_party <- data_big_7 %>%
# remove smallest 6 cantons
filter(canton %in% top_20_cantons) %>%
# calculate mean per party and municipality
group_by(municipality, bfs_id, canton, party) %>%
# instead of using mean, we'll divide ourselves by the number of
# elections because otherwise if a party runs once, this value is used
# which is not what we want, we want a real mean over all 40 years
summarise(mean_support = sum(support, na.rm = TRUE) / n_elections) %>%
# assign each municipality a rank per party
group_by(party) %>%
arrange(desc(mean_support)) %>%
mutate(rank = row_number())
# output top 10 per party
knitr::kable(
support_per_municipality_and_party %>%
# get top 10 municipalities per party
group_by(party) %>%
top_n(10, wt = mean_support) %>%
arrange(desc(mean_support)) %>%
ungroup() %>%
mutate(
name_with_value = glue("{municipality} ({scales::percent(mean_support)})")
) %>%
group_by(party) %>%
summarise(top_10 = glue_collapse(name_with_value, ", "))
)
party | top_10 |
---|---|
SPS | Schaffhausen (40.9%), Delémont (39.4%), Neuhausen am Rheinfall (39.1%), Obergerlafingen (38.7%), Lajoux (JU) (38.4%), Recherswil (38.0%), Roche (VD) (38.0%), Sils im Domleschg (37.5%), Gerlafingen (37.5%), Chavannes-près-Renens (37.1%) |
GPS | Nusshof (14.3%), Romainmôtier-Envy (13.4%), Sissach (13.3%), Fresens (12.7%), Tenniken (12.5%), Arlesheim (12.5%), Jouxtens-Mézery (12.4%), Oltingen (12.4%), Hersberg (12.3%), Ziefen (12.2%) |
GLP | Dübendorf (3.8%), Schwerzenbach (3.6%), Haldenstein (3.5%), Bonstetten (3.4%), Hettlingen (3.4%), Fällanden (3.2%), Greifensee (3.1%), Schleinikon (3.1%), Hirzel (3.0%), Madulain (3.0%) |
BDP | Hinterrhein (13.7%), Mathon (9.9%), Sufers (9.1%), Tschappina (8.8%), Urmein (8.0%), Donat (7.9%), Safiental (7.6%), Rüti bei Büren (7.6%), Ferrera (6.4%), Nufenen (6.4%), Zillis-Reischen (6.4%) |
CVP | Törbel (88.4%), Niedergesteln (85.0%), Eischoll (84.7%), Saas-Balen (83.5%), Blatten (83.4%), Wiler (Lötschen) (83.0%), Kippel (83.0%), Saas-Fee (82.6%), Eisten (82.5%), Zeneggen (82.5%) |
FDP | Yvorne (63.3%), Montalchez (63.3%), Arnex-sur-Nyon (62.1%), Brot-Plamboz (60.7%), Bourg-Saint-Pierre (60.1%), Vouvry (57.8%), Saint-Blaise (57.3%), Cologny (56.8%), La Côte-aux-Fées (56.8%), La Brévine (56.6%) |
SVP | Schangnau (86.0%), Eriz (82.5%), Berken (82.0%), Horrenbach-Buchen (79.6%), Rumendingen (79.0%), Wachseldorn (78.6%), Eggiwil (76.7%), Landiswil (76.6%), Trub (76.3%), Treiten (74.0%) |
Let’s visualize 100 municipalities per party where they made (over the last 40 years) the highest support (average per election that they put up candidates).
The rank is used for the opacity. Some municipalities might share multiple colors for multiple parties:
party_colors <- c(
"SPS" = "#F03E35",
"GPS" = "#7BB535",
"GLP" = "#C4C429",
"BDP" = "#E6C409",
"CVP" = "#D67C15",
"FDP" = "#2669B5",
"SVP" = "#3F8A30"
)
ggplot() +
# show map of switzerland as a light grey background
geom_sf(
data = cantons_geo,
aes(),
fill = "#FCFCFC",
color = "#666666",
size = 0.1
) +
geom_sf(
data = municipality_geo %>%
left_join(
support_per_municipality_and_party %>%
filter(rank <= 100),
by = c("GMDNR" = "bfs_id")
) %>%
filter(!is.na(party)),
aes(
fill = party,
alpha = rank
),
color = "white",
size = 0.1
) +
scale_fill_manual(values = party_colors) +
scale_alpha_continuous(range = c(1, 0.2)) +
theme_void() +
theme(
panel.grid.major = element_line("transparent"),
legend.position = "none"
)
# create new data frame summing all 11 elections
left_right_muns_over_all_years <- left_right_positioned_municipalities %>%
group_by(municipality, bfs_id, canton) %>%
summarise(
left = sum(left, na.rm = TRUE),
center = sum(center, na.rm = TRUE),
right = sum(right, na.rm = TRUE)
) %>%
gather(bloc, support, left:right)
ggplot(
left_right_muns_over_all_years %>%
left_join(
municipality_geo,
by = c("bfs_id" = "GMDNR")
),
aes(
fill = support
)
) +
geom_sf(color = "transparent") +
facet_wrap(~ bloc) +
scale_fill_distiller(palette = "Spectral") +
theme_void() +
theme(
panel.grid.major = element_line("transparent"),
legend.position = "none"
)
The above map shows where the three blocs had their strongest support over all 40 years (11 elections). Below you see one map where we only show the dominant bloc:
bloc_palette <- c(
"left" = "#c54a52",
"center" = "#feb24c",
"right" = "#81acaa"
)
ggplot(
left_right_muns_over_all_years %>%
filter(support == max(support)) %>%
left_join(
municipality_geo,
by = c("bfs_id" = "GMDNR")
),
aes(
fill = bloc
)
) +
geom_sf(color = "transparent") +
scale_fill_manual(values = bloc_palette) +
theme_void() +
theme(
panel.grid.major = element_line("transparent")
) +
labs(
title = "Left / Center / Right: which was the strongest over 11 elections?",
fill = NULL
)
It could be interesting to see whether we can find a municipality which is closest to the mean of the country. Instead of calculating the mean ourselves, let’s get the values for Switzerland from the BFS and read it in. In the second data set there is an entry with canton = Schweiz
. From there we get the values for the whole nation state.
# read cantonal and national data
cantonal_data <- read_csv(
"input/px-x-1702020000_104.csv"
) %>%
rename(
canton = 1,
year = 2,
party = 3,
support = 4
) %>%
# convert do number (decimals)
mutate(support = ifelse(support == "...", NA, as.numeric(support) / 100))
## Parsed with column specification:
## cols(
## Kanton = col_character(),
## Jahr = col_double(),
## Partei = col_character(),
## `Parteistärke in %` = col_character()
## )
## Warning in ifelse(support == "...", NA, as.numeric(support)/100): NAs durch
## Umwandlung erzeugt
national_data <- cantonal_data %>%
# keep only the sum of all of Switzerland
filter(canton == "Schweiz") %>%
select(-canton)
# country from cantonal data
cantonal_data %<>%
filter(canton != "Schweiz")
# read values per language region provided by bfs
language_data <- readxl::read_excel(
"input/su-d-17.02.02.03.02_SPR_SRF-Zehr.xlsx",
range = "A4:M69"
) %>%
rename(party = 1) %>%
# remove `=> FDP` from the last two columns and convert all but party to num
mutate_at(vars(-one_of("party")), as.numeric) %>%
mutate(
# pull out language region into separate column
lang = str_extract(party, "(\\w+)sprachige Schweiz"),
# and abbreviate to D, F, I
lang = factor(str_sub(lang, 0, 1))
) %>%
# fill up values downwards for all parties
fill(lang) %>%
# and remove the original column of the language region
filter(!str_detect(party, "(\\w+)sprachige Schweiz")) %>%
# replace with party names equal to the other data
mutate(party = str_replace_all(party, c(
"^BDP$" = "BDP/PBD",
"^CSP$" = "CSP/PCS",
"^CVP$" = "CVP/PDC",
"^EDU$" = "EDU/UDF",
"^EVP$" = "EVP/PEV",
"^FDP 2\\)$" = "FDP/PLR (PRD)",
"^FGA$" = "FGA/AVF",
"^FPS$" = "FPS/PSL",
"^GLP$" = "GLP/PVL",
"^GPS$" = "GPS/PES",
"^LdU$" = "LdU/AdI",
"^LPS 2\\)$" = "LPS/PLS",
"^PdA$" = "PdA/PST",
"^POCH$" = "POCH",
"^Rep\\.$" = "Rep./Rép.",
"^SD$" = "SD/DS",
"^SP$" = "SP/PS",
"^SVP$" = "SVP/UDC",
"^Übrige$" = "Übrige/Autres"
))) %>%
# make tidy
gather(year, support, -one_of(c("party", "lang"))) %>%
mutate(
# remove footnote and convert to numeric
year = as.numeric(str_sub(year, 0, 4)),
# and make decimal number out of support
support = support / 100
)
## Warning: NAs durch Umwandlung erzeugt
## Warning: NAs durch Umwandlung erzeugt
# calculate distance between each municipality and the nation state
data_with_deltas <- data %>%
# filter out non-municipalities (Swiss livign abroad)
filter(!is.na(bfs_id)) %>%
left_join(
national_data %>%
rename(support_national = support),
by = c("year", "party")
) %>%
# add language region from Raumgliederungen
left_join(
readxl::read_excel(
"input/Raumgliederungen.xlsx",
skip = 3,
col_names = FALSE
) %>%
select(bfs_id = 1, lang = 6),
by = "bfs_id"
) %>%
mutate(lang = case_when(
lang == 2 ~ "F",
lang == 3 ~ "I",
TRUE ~ "D" # 1 = German, 4 = Rhaeto-roman
)) %>%
# join language region data from above
left_join(
language_data %>%
rename(support_lang = support),
by = c("year", "party", "lang")
) %>%
# calculate absolute distances
mutate(
delta_national = abs(support - support_national),
delta_lang = abs(support - support_lang)
)
## New names:
## * `` -> `..1`
## * `` -> `..2`
## * `` -> `..3`
## * `` -> `..4`
## * `` -> `..5`
## * … and 1 more
## Warning: Column `lang` joining character vector and factor, coercing into
## character vector
knitr::kable(
data_with_deltas %>%
group_by(municipality, bfs_id, canton) %>%
summarise(mean_delta = mean(delta_national, na.rm = TRUE)) %>%
# filter out zeros that get introduced by shared urns
filter(mean_delta > 0) %>%
arrange(mean_delta) %>%
head(20)
)
municipality | bfs_id | canton | mean_delta |
---|---|---|---|
Frauenfeld | 4566 | TG | 0.0202465 |
Obersiggenthal | 4038 | AG | 0.0205429 |
Langnau am Albis | 136 | ZH | 0.0216070 |
Gebenstorf | 4029 | AG | 0.0223357 |
Brugg | 4095 | AG | 0.0223839 |
Stein (AG) | 4260 | AG | 0.0229214 |
Richterswil | 138 | ZH | 0.0230803 |
Klingnau | 4309 | AG | 0.0232500 |
Horgen | 133 | ZH | 0.0233042 |
Bad Zurzach | 4323 | AG | 0.0233786 |
Oberengstringen | 245 | ZH | 0.0233887 |
Baden | 4021 | AG | 0.0235839 |
Adliswil | 131 | ZH | 0.0235845 |
Riniken | 4111 | AG | 0.0238875 |
Untersiggenthal | 4044 | AG | 0.0239036 |
Flurlingen | 29 | ZH | 0.0239042 |
Wädenswil | 142 | ZH | 0.0240831 |
Steckborn | 4864 | TG | 0.0242444 |
Thalwil | 141 | ZH | 0.0243831 |
Reinach (BL) | 2773 | BL | 0.0250250 |
Above you see the municipalities which are closest to the national average. Below you find 5 municipalities per language region that are closest to it’s (langauge region) average:
knitr::kable(
data_with_deltas %>%
group_by(municipality, bfs_id, canton, lang) %>%
summarise(mean_delta = mean(delta_lang, na.rm = TRUE)) %>%
# filter out zeros that get introduced by shared urns
filter(mean_delta > 0) %>%
group_by(lang) %>%
top_n(-5, wt = mean_delta) %>%
arrange(lang, mean_delta)
)
municipality | bfs_id | canton | lang | mean_delta |
---|---|---|---|---|
Richterswil | 138 | ZH | D | 0.0184225 |
Langnau am Albis | 136 | ZH | D | 0.0184648 |
Wädenswil | 142 | ZH | D | 0.0187746 |
Gebenstorf | 4029 | AG | D | 0.0188750 |
Frauenfeld | 4566 | TG | D | 0.0189899 |
Echallens | 5518 | VD | F | 0.0214567 |
Montreux | 5886 | VD | F | 0.0261654 |
Crissier | 5583 | VD | F | 0.0264252 |
Cheseaux-sur-Lausanne | 5582 | VD | F | 0.0272598 |
La Tour-de-Peilz | 5889 | VD | F | 0.0280472 |
Gambarogno | 5398 | TI | I | 0.0152824 |
Locarno | 5113 | TI | I | 0.0156588 |
Caslano | 5171 | TI | I | 0.0163647 |
Bioggio | 5151 | TI | I | 0.0171765 |
Maggia | 5317 | TI | I | 0.0173412 |
We can apply the same logic to cantons
cantons_with_deltas <- cantonal_data %>%
left_join(
national_data %>%
rename(support_national = support),
by = c("year", "party")
) %>%
# calculate absolute distances
mutate(
delta_national = abs(support - support_national)
)
knitr::kable(
cantons_with_deltas %>%
group_by(canton) %>%
summarise(mean_delta = mean(delta_national, na.rm = TRUE)) %>%
arrange(mean_delta)
)
canton | mean_delta |
---|---|
Aargau | 0.0209866 |
Zürich | 0.0248528 |
Basel-Landschaft | 0.0279490 |
Solothurn | 0.0328662 |
Thurgau | 0.0355525 |
Bern / Berne | 0.0374723 |
St. Gallen | 0.0386990 |
Vaud | 0.0414795 |
Graubünden / Grigioni / Grischun | 0.0499606 |
Basel-Stadt | 0.0503379 |
Fribourg / Freiburg | 0.0509788 |
Genève | 0.0548774 |
Luzern | 0.0593098 |
Zug | 0.0627684 |
Neuchâtel | 0.0645263 |
Schwyz | 0.0647017 |
Ticino | 0.0792612 |
Schaffhausen | 0.0829315 |
Valais / Wallis | 0.0924079 |
Jura | 0.1058978 |
Appenzell Ausserrhoden | 0.1193371 |
Uri | 0.2453367 |
Nidwalden | 0.2722346 |
Obwalden | 0.2779680 |
Glarus | 0.3173654 |
Appenzell Innerrhoden | 0.3355120 |
As an integrity check we sum up all the values per year and check whether it is close to one:
# regroup by party id (collect small parties and FDP/LPS into one group)
national_export <- national_data %>%
# join and group by id
left_join(
parties,
by = c("party" = "abbr")
) %>%
group_by(year, party = group_id) %>%
summarise(support = sum(support, na.rm = TRUE)) %>%
ungroup()
knitr::kable(
national_export %>%
mutate(support = round(support, 3)) %>%
group_by(year) %>%
summarise(support = sum(support))
)
year | support |
---|---|
1975 | 1.001 |
1979 | 0.999 |
1983 | 0.998 |
1987 | 0.999 |
1991 | 1.000 |
1995 | 1.000 |
1999 | 1.000 |
2003 | 0.998 |
2007 | 1.000 |
2011 | 1.000 |
2015 | 1.000 |
This seems to be the case.
write_csv(
national_export %>%
mutate(support = round(support, 3)),
"output/national.csv"
)
# suppress warnings thrown by max function:
# no non-missing arguments to max; returning -Inf
# because max() sometimes returns integers, sometimes doubles
# https://github.com/tidyverse/dplyr/issues/489
dominant_party <- data %>%
group_by(municipality, year) %>%
filter(support == max(support, na.rm = TRUE)) %>%
# make sure it's not two parties with the same values left
slice(1) %>%
ungroup() %>%
# join and group by id
left_join(
parties,
by = c("party" = "abbr")
)
dominant_party_export <- dominant_party %>%
# replace party with group id in export
select(-party, party = group_id) %>%
# and municipality with municipality id
select(-municipality, municipality = bfs_id, -bloc) %>%
mutate(support = round(support, 3))
write_csv(
dominant_party_export,
"output/dominant_party.csv"
)
data_parties_grouped <- data %>%
# join and group by id
left_join(
parties,
by = c("party" = "abbr")
) %>%
group_by(year, bfs_id, party = group_id) %>%
summarise(support = sum(support, na.rm = TRUE)) %>%
ungroup()
# iterate over parties and export one file per party
data_parties_grouped %>%
distinct(party) %>%
pwalk(function(...) {
# save all arguments (columns of current iteration) into variable
current <- tibble(...)
# filter and keep only entries with current party
export_selection <- data_parties_grouped %>%
filter(party == current$party) %>%
# rename bfs_id to municipality
rename(municipality = bfs_id) %>%
mutate(support = round(support, 3))
# save as csv
write_csv(
export_selection,
glue("output/by_party/party_{current$party}.csv")
)
})
municipality_geo %>%
st_drop_geometry() %>%
pwalk(function(...) {
# save all arguments (columns of current iteration) into variable
current <- tibble(...)
# filter and keep only entries with current municipality
export_selection <- data %>%
filter(bfs_id == current$GMDNR) %>%
select(-bfs_id) %>%
# join and group by id
left_join(
parties,
by = c("party" = "abbr")
) %>%
group_by(year, party = group_id) %>%
summarise(support = sum(support, na.rm = TRUE)) %>%
ungroup() %>%
mutate(support = round(support, 3))
if (nrow(export_selection) > 0) {
# save as csv
write_csv(
export_selection,
glue("output/by_municipality/municipality_{current$GMDNR}.csv")
)
}
})
export_municipalities <- municipality_geo %>%
st_drop_geometry() %>%
select(id = GMDNR, name = GMDNAME)
write_csv(
export_municipalities %>%
# join translations of municipalities
# they might be useful for someone
left_join(
read_csv("input/municipality_translations.csv"),
by = c("name" = "original_name")
),
"output/municipalities.csv",
na = ""
)
## Parsed with column specification:
## cols(
## original_name = col_character(),
## name_de = col_character(),
## name_fr = col_character(),
## name_it = col_character()
## )
# clean up
rm(export_municipalities)
write_csv(
parties %>%
mutate(group_name = case_when(
group_id == 1 ~ "Small left",
group_id == 7 ~ "others",
group_id == 12 ~ "Small right",
TRUE ~ ""
)),
"output/parties.csv"
)