Elections and Quarantines

Postal Voting

The Electoral Commission’s recent plans for accommodating those in quarantine make sense from a democratic point of view, after all, every eligible voter should be given a chance to cast their vote.

The drive-through solution verges a tad on the absurd, but it’s worth bearing in mind Malta still never got around to implementing a postal voting system, a rarity in western democracies. Normally, this just means that the state has to subsidise flights for citizens living abroad who have to vote, but this time round it also means we can’t use the mail to vote in a pandemic like the US did. (Since we are on this subject, technology is a fairly bad solution to voting, as is covered comprehensively in this Tom Scott video.)

Demographics of Groups

More pertinently to this post however, the article set me wondering on how COVID might change the electoral landscape. My first instincts were that if there was another spike in quarantines like we had with the Omicron surge, the total votes would decrease, but the proportion would stay stable, since as far as we can tell COVID infects supporters of both parties equally.

But that’s assuming way too much homogeneity in both the demographics of party support and COVID infections. For instance, we know that in the Omicron surge, the bulk of the infections were in younger people. The level of PL/PN support won’t be identical across age cohorts, and if one cohort that’s more likely to vote one way is also more likely to be in quarantine, then you can end up with some pretty interesting effects.

To play around with this idea, we can set up an experiment to see how differing rates of voter turnout in each individual age group might swing things. The first stop was the most recent MT survey for the age cohort splits. Then I used the population tables on pages 5 and 6 here to sum up the number of people in each cohort.

Which means we end up with:

population <- tibble(group = c('16-35', '36-50', '51-65', '65+'),
PL = c(31.4, 40.2, 52.0, 52.1)/100,
PN = c(28.4, 26.3, 22.4, 36.6)/100,
None = c(13.6, 17.7, 7.8, 2.7)/100,
DontKnow = c(20.9, 13.2, 14.9, 5.7)/100,
pop_size = c(149226, 112153, 89646, 91517)) %>%
pivot_longer(2:5, names_to = "pop") %>%
mutate(people = round(pop_size * value))

population
## # A tibble: 16 x 5
##    group pop_size pop      value people
##    <chr>    <dbl> <chr>    <dbl>  <dbl>
##  1 16-35   149226 PL       0.314  46857
##  2 16-35   149226 PN       0.284  42380
##  3 16-35   149226 None     0.136  20295
##  4 16-35   149226 DontKnow 0.209  31188
##  5 36-50   112153 PL       0.402  45086
##  6 36-50   112153 PN       0.263  29496
##  7 36-50   112153 None     0.177  19851
##  8 36-50   112153 DontKnow 0.132  14804
##  9 51-65    89646 PL       0.52   46616
## 10 51-65    89646 PN       0.224  20081
## 11 51-65    89646 None     0.078   6992
## 12 51-65    89646 DontKnow 0.149  13357
## 13 65+      91517 PL       0.521  47680
## 14 65+      91517 PN       0.366  33495
## 15 65+      91517 None     0.027   2471
## 16 65+      91517 DontKnow 0.057   5216

The last step is to create a final filter table with updatable proportions, where we can artificially depress turnout in each cohort. This might be due to obligatory quarantine, reluctance to queue up on voting day or other reasons.

So for example, for a youth vote deflated by 3% and a senior vote deflated by 5%, the relevant proportion table would be 100% - 3% and 100% - 5% of that group respectively. Or in R terms:

young = 3
senior = 5

prop_table <-
crossing(group = c('16-35', '36-50', '51-65', '65+'),
pop = c('PL', 'PN', "Don't Know", 'No Vote')) %>%
mutate(
prop = c(1-(young/100),
1-(young/100),

1-(senior/100),
1-(senior/100),
prop = if_else(prop > 1, 1, prop))

prop_table
## # A tibble: 16 x 3
##    group pop         prop
##    <chr> <chr>      <dbl>
##  1 16-35 Don't Know  0.97
##  2 16-35 No Vote     0.97
##  3 16-35 PL          0.97
##  4 16-35 PN          0.97
##  5 36-50 Don't Know  1
##  6 36-50 No Vote     1
##  7 36-50 PL          1
##  8 36-50 PN          1
##  9 51-65 Don't Know  1
## 10 51-65 No Vote     1
## 11 51-65 PL          1
## 12 51-65 PN          1
## 13 65+   Don't Know  0.95
## 14 65+   No Vote     0.95
## 15 65+   PL          0.95
## 16 65+   PN          0.95

The cascade parameter toys with another idea besides age cohort. There is no reason to assume a particular group is more susceptible to being infected, but there can be the case of say a super-spreader at one political rally and not another which disproportionately puts a mass of people out.

The final piece to coming up with a hypothetical vote is just joining the prop_table to population and calculating the estimated votes as the product of people and this proportion.

vote <- population %>%
inner_join(prop_table) %>%
mutate(vote = round(people * prop))

vote
## # A tibble: 8 x 7
##   group pop_size pop   value people  prop  vote
##   <chr>    <dbl> <chr> <dbl>  <dbl> <dbl> <dbl>
## 1 16-35   149226 PL    0.314  46857  0.97 45451
## 2 16-35   149226 PN    0.284  42380  0.97 41109
## 3 36-50   112153 PL    0.402  45086  1    45086
## 4 36-50   112153 PN    0.263  29496  1    29496
## 5 51-65    89646 PL    0.52   46616  1    46616
## 6 51-65    89646 PN    0.224  20081  1    20081
## 7 65+      91517 PL    0.521  47680  0.95 45296
## 8 65+      91517 PN    0.366  33495  0.95 31820

Shiny App

I wrapped up the code in a shiny app so it would be easier to explore.

Sensible Parameters

Using some back of the napkin estimations, Malta had roughly 50,000 people in quarantine at the peak of Omicron, so a 10% value across the board would simulate this worst case. As for the cascade parameter, imagining a super-spreader event is around 300 people, and an average household is 3.5 people + 3 additional contacts for each of those (300 * 3.5 = 1,050 * 3 = 3,150), +/- 0.7% would be a pretty reasonable parameter.

Conclusion

The impacts of even the most out there tweaks are relatively meaningless both because of the size of PL’s lead and because the most polarized by age cohort of voters in the 51-65 range are also the smallest.

tibble(group = c('16-35', '36-50', '51-65', '65+'),
PL = c(31.4, 40.2, 52.0, 52.1)/100,
PN = c(28.4, 26.3, 22.4, 36.6)/100,
None = c(13.6, 17.7, 7.8, 2.7)/100,
DontKnow = c(20.9, 13.2, 14.9, 5.7)/100,
pop_size = c(149226, 112153, 89646, 91517)) %>%
mutate(pl_support = PL/(PL+PN))
## # A tibble: 4 x 7
##   group    PL    PN  None DontKnow pop_size pl_support
##   <chr> <dbl> <dbl> <dbl>    <dbl>    <dbl>      <dbl>
## 1 16-35 0.314 0.284 0.136    0.209   149226      0.525
## 2 36-50 0.402 0.263 0.177    0.132   112153      0.605
## 3 51-65 0.52  0.224 0.078    0.149    89646      0.699
## 4 65+   0.521 0.366 0.027    0.057    91517      0.587

Limitations

Two things not accounted for are the difference in eligible voters from the population and election day turnout. There are about 360,000 eligible voters currently, but if we sum the over 16 population in our table we get around 426,000:

population %>%
tally(people)
## # A tibble: 1 x 1
##        n
##    <dbl>
## 1 425865

The reason is probably people who don’t have a vote for legal or health reasons and foreign nationals, applying a broad factor across the board would probably be incorrect. (Differences in migration are more likely to be prevalent in younger groups, and losing your vote because of a felony becomes more probable the older you are, if only because you have had more time to be criminal).

Election day turnout is typically in the low 90s, again, applying a broad factor across all age groups is probably wrong, so we’ll ignore both of these. They probably won’t make that much of a difference since we’re doing percentages rather than raw votes.

And lastly, we’re also basing a heck of a lot on a single MT poll, and inferences for subgroups remain a particularly tricky issue as I discussed previously here.