Preparing the setup

This vignette presents a step by step process to measure the latent ideological position of Subsets of the Swiss population based on the paper Walder, M.(2025) Latent ideological positions of swiss parties and subsets of the population. Swiss Political Science Review using the package RSwissPos.

The first requiers the installation and the loading of the RSwissPos package to get access to the relevant data and parameters, the devtools package that enables to install other packages, the lme4 package that provides mixed models framework, and the ggplot2, adn the RSwissMaps package to plot results.

library(lme4)
#> Loading required package: Matrix
library(ggplot2)
library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
library(RSwissPos)
#> Loading required package: pxR
#> Loading required package: stringr
#> Loading required package: reshape2
#> Loading required package: jsonlite
#> Loading required package: plyr
#> ------------------------------------------------------------------------------
#> You have loaded plyr after dplyr - this is likely to cause problems.
#> If you need functions from both plyr and dplyr, please load plyr first, then dplyr:
#> library(plyr); library(dplyr)
#> ------------------------------------------------------------------------------
#> 
#> Attaching package: 'plyr'
#> The following objects are masked from 'package:dplyr':
#> 
#>     arrange, count, desc, failwith, id, mutate, rename, summarise,
#>     summarize
#> Loading required package: rjson
#> 
#> Attaching package: 'rjson'
#> The following objects are masked from 'package:jsonlite':
#> 
#>     fromJSON, toJSON
library(RSwissMaps)

Loading the data

To replicate the estimation of municipal ideological position, we need to first upload the data on municipal results

mun_res <- getPopResDD(PlaceType = "Municipality")

head(mun_res)
#>                                                                                DateName
#> 1 1960-05-29 Bundesbeschluss über die Weiterführung befristeter Preiskontrollmassnahmen
#> 2 1960-05-29 Bundesbeschluss über die Weiterführung befristeter Preiskontrollmassnahmen
#> 3 1960-05-29 Bundesbeschluss über die Weiterführung befristeter Preiskontrollmassnahmen
#> 4 1960-05-29 Bundesbeschluss über die Weiterführung befristeter Preiskontrollmassnahmen
#> 5 1960-05-29 Bundesbeschluss über die Weiterführung befristeter Preiskontrollmassnahmen
#> 7 1960-05-29 Bundesbeschluss über die Weiterführung befristeter Preiskontrollmassnahmen
#>                Place YesPercent YesVote NoVote Voters ReceivedBallots
#> 1 ......Bischofszell   75.76854     419    134     NA              NA
#> 2      ......Eriswil   86.95652      40      6    507              46
#> 3   ......Wilen (TG)         NA      NA     NA     NA              NA
#> 4    ......Remaufens         NA      NA     NA     NA              NA
#> 5      ......Autigny         NA      NA     NA     NA              NA
#> 7 ......Mollens (VD)   80.00000      16      4    101              20
#>   Participation ValidBallots    PlaceType NumberMun  anr       Date Year
#> 1            NA          553 Municipality      4471 1930 1960-05-29 1960
#> 2      9.072978           46 Municipality      0953 1930 1960-05-29 1960
#> 3            NA           NA Municipality      4786 1930 1960-05-29 1960
#> 4            NA           NA Municipality      2333 1930 1960-05-29 1960
#> 5            NA           NA Municipality      2173 1930 1960-05-29 1960
#> 7     19.801980           20 Municipality      5431 1930 1960-05-29 1960
#>   anr.Swissvotes
#> 1            193
#> 2            193
#> 3            193
#> 4            193
#> 5            193
#> 7            193

Second, we need to upload the posterior parameter value of the discrimination of ballot proposals. For this exemple, we use the dynamic estimates to measure latent ideological position of municipalities over-time, thus we upload the dynamic estimates.

data("dynamic_estimates")
head(dynamic_estimates)
#>     anr Discrimination.2.5 Discrimination.25 Discrimination.50
#> 1   193          -6.677930         -1.473653          1.154748
#> 27  194          -6.741519         -1.539953          1.053752
#> 53  195          -6.950957         -1.507486          1.101109
#> 79  196          -6.954938         -1.495620          1.064237
#> 105 197         -13.013273         -8.013414         -5.533402
#> 131 198          -6.688203         -1.516881          1.053839
#>     Discrimination.75 Discrimination.97.5
#> 1            3.862583            9.782192
#> 27           3.863319            9.622087
#> 53           3.749832            9.751796
#> 79           3.838005            9.494572
#> 105         -3.272277            1.010002
#> 131          3.843238           10.136276

Finally, we need to merge the posterior estimates and the data on municipal results and recode the outcome variable, the percent of yes vote, into a share of yes vote

# First recode the merging variable
dynamic_estimates$anr.Swissvotes <- dynamic_estimates$anr

# Then we merge the datasets
data_muni <- merge(mun_res, dynamic_estimates, by = "anr.Swissvotes")

Recode data and prepare the model

To prepare the data for the model, we need to also collect and merge the national vote share for ballot proposals. This is requiered to compute the difference in the logit transformation of the support for direct democratic ballot proposals that is used as the dependent variable. We also need to merge the data with some variables of the Smartvote data to add the legislature during which the vote on the ballot proposal happened for the dynamic estimation.

# Load the data
CH_res <- getPopResDD(PlaceType = "Country")

# Get the national vote result and merging variable
CH_res_m <- as.data.frame(list(anr.Swissvotes = CH_res$anr.Swissvotes, 
                               CH_Yes = CH_res$YesPercent))

data_muni_CH <- merge(CH_res_m, data_muni, by = "anr.Swissvotes")

# Get swissvotes data with ballot number and legislature 
Swissvotes <- getSwissvotes(Column.names = c("anr", "legislatur"))
#> Please cite: Swissvotes (Year). Swissvotes – die Datenbank der eidgenössischen Volksabstimmungen.

# Rename ballot number variable 
colnames(Swissvotes)[1] <- "anr.Swissvotes"

# Merge the swissvotes data with the data with muncipal support for ballot proposals 

data_muni_CH <- merge(Swissvotes, data_muni_CH, by = "anr.Swissvotes")

Now that the support for direct democratic at the municipal and the national levels are embeeded into the same data frame, we need to compute the logit values of the share of support. The logit value cannot work for cases equal to 0% or 100%, thus we also need to recode these cases to allow a tiny probability of support or opposiion.

# Transform percent into shares
data_muni_CH$share_yes <- data_muni_CH$YesPercent/100
data_muni_CH$share_yes_CH <- data_muni_CH$CH_Yes/100

# Recode 1 and 0 to allow very low probability of opposition or support
data_muni_CH <- data_muni_CH %>% 
  mutate(share_yes = ifelse(share_yes==0, 0.0001,
                            ifelse(share_yes==1, 0.9999, share_yes)))

# Apply the logit transformation
data_muni_CH$log_share <- log(data_muni_CH$share_yes/(1 - data_muni_CH$share_yes))
data_muni_CH$log_share_CH <- log(data_muni_CH$share_yes_CH/(1 - data_muni_CH$share_yes_CH))

# Compute the difference in the logit transformation of the support between the national level and the municipality
data_muni_CH$diff_log <- data_muni_CH$log_share - data_muni_CH$log_share_CH

Estimate position of municipalities

Let’s first estimate the position of municipalities for a single legislature, for the exemple we take the last complete legislature - the 51^{st} legislature. To do so, we need to filter observations related to the ballots that took place during this legislature. Second, for multilevel modeling, it is good to rescale the variables. Third, we estimate the model.

# First, select observations
data_muni_CH_51 <- data_muni_CH[data_muni_CH$legislatur==51,]

# Second, rescale the variables
data_muni_CH_51_scale <- transform(data_muni_CH_51, 
                                   Discrimination = scale(Discrimination.50),
                                   diff_log_sc = scale(diff_log))

# Third, Tun the model
model <- lmer(diff_log ~ (Discrimination|NumberMun), data_muni_CH_51_scale)
#> Warning in checkConv(attr(opt, "derivs"), opt$par, ctrl = control$checkConv, :
#> Model failed to converge with max|grad| = 0.00210288 (tol = 0.002, component 1)

Now, the model object contains the results of the regression. To get the estimated position of municipalities’ ideological position, we need to extract the random slopes using the broom.mixed package. To plot the municipal position we create a data base with all the random slopes and confidence intervals. We then use the RSwissMaps package to map to position of municipalities.

# Extract the random effects
b <- broom.mixed::tidy(model, effects = "ran_vals", conf.int = TRUE)

# Creates data with random slopes, confidence intervales and place numeber
data_mun_results <- as.data.frame(list(rand = b[b$term=="Discrimination",]$estimate,
                                       low = b[b$term=="Discrimination",]$conf.low,
                                       high = b[b$term=="Discrimination",]$conf.high,
                                       bfs_nr = b[b$term=="Discrimination",]$level))

# Creat a mun template to plot 
mun_plot <- mun.template(2016)

# Change the merging variable number mun to fir the format of the template
data_mun_results$bfs_nr <- as.numeric(data_mun_results$bfs_nr)

# Merge the template with the extacted random slopes
mun_plot <-  merge(mun_plot, data_mun_results, by = "bfs_nr")

# Map the results 
mun.plot(bfs_id = mun_plot$bfs_nr, data = mun_plot$rand, year = 2016, boundaries_size = 0.1,
         legend_title = expression(u["1g"]), boundaries_color = "white")+
  scale_fill_gradient2(
    name = expression(u["1g"]),
    low = "red",
    mid = "light grey",
    high = "dark green",
    midpoint = 0,
    space = "Lab",
    na.value = "white",
    guide = "colourbar",
    aesthetics = "fill"
  )
Ideological position of Swiss municipalities during the 51 legislature

Ideological position of Swiss municipalities during the 51 legislature

Now that you have the ideological position you can use them as indicator in your own research.

It is also possible to position municipalities over different legislature. For this, we run the same type of model with all data and estimate the radom slop for the mixed municipality and legislature.

# Second, rescale the variables
data_muni_CH_scale <- transform(data_muni_CH, 
                                   Discrimination = scale(Discrimination.50),
                                   diff_log_sc = scale(diff_log))

# Third, Tun the model
model_time <- lmer(diff_log ~ (Discrimination|NumberMun:legislatur), data_muni_CH_scale)
#> Warning in checkConv(attr(opt, "derivs"), opt$par, ctrl = control$checkConv, :
#> Model failed to converge with max|grad| = 0.00605427 (tol = 0.002, component 1)

Using the broom.mixed package we can extract the random slopes and build a dataset with random slopes for municipalities over time. We can then plot the evolution of municipal position over time.

# Extract the random effects
b <- broom.mixed::tidy(model_time, effects = "ran_vals", conf.int = TRUE)

# Creates data with random slopes, confidence intervales and place numeber
data_mun_results_time <- as.data.frame(list(rand = b[b$term=="Discrimination",]$estimate,
                                            low = b[b$term=="Discrimination",]$conf.low,
                                            high = b[b$term=="Discrimination",]$conf.high,
                                            bfs_nr = substr(b[b$term=="Discrimination",]$level, 1, nchar(b[b$term=="Discrimination",]$level) - 3),
                                            legislatur = substr(b[b$term=="Discrimination",]$level, nchar(b[b$term=="Discrimination",]$level) -1, nchar(b[b$term=="Discrimination",]$level))))

# Creat a mun template to plot 
ggplot(data_mun_results_time)+
  geom_line(aes(x=legislatur, y = rand, group = bfs_nr), color = "darkgrey", linewidth = .1)+
  xlab("Legislatur")+
  ylab("Ideological position")+
  theme_minimal()

Now we have the ideological position of municipalities over-time. Please keep in mind that this position is related to the party competition space. The centralisation we observe over-time does not necessarily mean that municipalities are closer in the latest than the earliest legislatur, but that they are relative to the party competition dynamics. The over-time position should be understood as a relative difference. For more information, carefully read the paper that originated this methodological approach.

Further steps

Awesome, you now know how to compute the ideological position of Swiss municpality on the parties’ ideological space. You can use this in your own research. You can also use this method to compute position of municipalities on specific issues, using the comparative agenda’s project expert coding available with the RSwissPos package.

Be creative but also be aware that any estimation has limitations!