The Brazilian COVID-19 vaccination campaign: a modelling analysis of sociodemographic factors on uptake

Objective Dose shortages delayed access to COVID-19 vaccination. We aim to characterise inequality in two-dose vaccination by sociodemographic group across Brazil. Design This is a cross-sectional study. Setting We used data retrieved from the Brazilian Ministry of Health databases published between 17 January 2021 and 6 September 2021. Methods We assessed geographical inequalities in full vaccination coverage and dose by age, sex, race and socioeconomic status. We developed a Campaign Optimality Index to characterise inequality in vaccination access due to premature vaccination towards younger populations before older and vulnerable populations were fully vaccinated. Generalised linear regression was used to investigate the risk of death and hospitalisation by age group, socioeconomic status and vaccination coverage. Results Vaccination coverage is higher in the wealthier South and Southeast. Men, people of colour and low-income groups were more likely to be only partially vaccinated due to missing or delaying a second dose. Vaccination started prematurely for age groups under 50 years which may have hindered uptake in older age groups. Vaccination coverage was associated with a lower risk of death, especially in older age groups (ORs 9.7 to 29.0, 95% CI 9. 4 to 29.9). Risk of hospitalisation was greater in areas with higher vaccination rates due to higher access to care and reporting. Conclusions Vaccination inequality persists between states, age and demographic groups despite increasing uptake. The association between hospitalisation rates and vaccination is attributed to preferential delivery to areas of greater transmission and access to healthcare.


Packages
library(dplyr) library(readr) library(lubridate) library(car) library(xtable) • The readr package provides a function, read_delim that is faster than the built-in read.csvfunction and provides a progress bar.• The car package provides the vif function which we use to check for a concerning about of covariation between the covariates of our model.• The xtable package is needed to make a LaTeX table summarising the final model fit.

$vaccination_coverage NULL
If you want to check that the files you have match the ones used, here are some checksums (which can be obtained with sha256sum * from within data/.9116d21e84b8ab4d4e8b2437644248618dc39affc7f6101d8ac7c631950c2e8e agesex_coverage_muni_new.csv 7a5a6d07ab0beadd1c015d087802192aa02a227fad72ab33d8e0299600db29b0 census2010_muni_covariates.csve7db11cdf762b65b51df22ab4a71bf6f57f20d503ec568f34788c62bd5e0073f dates_50.rds2ee48a45bb88d3b78fa9c5571918d5bce23aa135fc3234ff1e1cc3836cfb4f32 sivep_20092021.csv4c7bfe45057871a354ef00ab7a905b1daafff844dbd0d89045f8ed8c5f06c67a sivep_2020.csv

Satellite cities and excluded municipality codes
The following aggregated_satellites function is taken from the preprocessing script.
We exclude the municipality with code 431453 as it has a transient existence and 999999 since it is a malformed value.Since the codes are variously integers or characters we will filter these out on a case by case basis.

4
BMJ Publishing Group Limited (BMJ) disclaims all liability and responsibility arising from any reliance Supplemental material placed on this supplemental material which has been supplied by the author(s) BMJ Open doi: 10.1136/bmjopen-2023-076354 :e076354.14 2024; BMJ Open , et al.

Li SL
i Use `spec()`to retrieve the full column specification for this data.i Specify the column types or set `show_col_types = FALSE`to quiet this message.

Vaccination campaign data
Because in the preprocessing we used integers to represent age ranges, and in this script we use strings describing the range, it is helpful to have a function to change from the integers to the strings age_group_int_to_string <-function(age_groups) { case_when(age_groups == 19 ~"ageunder20", age_groups == 20 ~"age20to29", age_groups == 30 ~"age30to39", age_groups == 40 ~"age40to49", age_groups == 50 ~"age50to59", age_groups == 60 ~"age60to69", age_groups == 70 ~"age70to79", age_groups == 80 ~"age80_plus") } We can read the data of when the vaccination campaign reached half coverage (for an age group in a specific municipality) with the following and rename the columns to match the rest of the code.N.b. since we already handled the questionable municipality codes in the preprocessing script we do not need to worry about this again.

Old preprocessing steps
Here is an older version of the code to do this processing step, but the values are coming out slightly different which is a bit concerning.

5
BMJ Publishing Group Limited (BMJ) disclaims all liability and responsibility arising from any reliance Supplemental material placed on this supplemental material which has been supplied by the author(s) We need to coerce this to a data frame towards the end because otherwise it complains that the grouping column mun_vac is no longer present, even though it is, we have just renamed it.

Vaccination coverage data
Because the vaccination coverage was calculated from estimates of population size it is possible that it takes values greater than 1.0.When this happens we need to clip the values down to 1.0.

Old preprocessing steps
Here is an older version of the code to do this processing step: 6 BMJ Publishing Group Limited (BMJ) disclaims all liability and responsibility arising from any reliance Supplemental material placed on this supplemental material which has been supplied by the author(s)

SIVEP-Gripe dataset Preprocessing function
There are a couple of transformations that we want to apply to both the 2020 and 2021 SIVEP-Gripe datasets, so we should abstract them into a function to do this.In the filter we have the following conditions: • CLASSI_FIN %in% c(4, 5, 9, NA) to select the cases that are probably COVID-19.
Note that an earlier version of the code excluded 9 even though they only make up a very small number of cases.• SEM_PRI >= 10 to only keep instances where the symptom onset is after epi-week 10.

Preparing the data
The only difference in the processing of these data is that for the 2021 dataset we limit ourselves to cases with a symptom onset before 7 September 2021.This preprocessing step takes about a minute on my laptop.

Combined dataframe
We want to be able to filter the cases based on the state of the vaccination campaign so we need to join these data frames so we have the information linked properly.
First we start by joining the data frame that contains a Boolean reflecting if at least 50% of the age group in that municipality had been vaccinated.tmp <-left_join( sivep_df, vac_dates_df, by = c("patient_address_muni_code", "age_group") ) |> mutate(vac_campaign = ifelse(date_symptoms < vac_start_date, "pre", "post")) stopifnot(!any(is.na(tmp))) The next step is to join in the vaccination coverage as a proportion of the age group in the municipality on the date of symptom onset.9 BMJ Publishing Group Limited (BMJ) disclaims all liability and responsibility arising from any reliance Supplemental material placed on this supplemental material which has been supplied by the author(s) BMJ Open doi: 10.1136/bmjopen-2023-076354 :e076354.14 2024; BMJ Open , et al.Li SL vac_cov_renamed_df <-rename(vac_cov_df, date_symptoms = date) tmp2 <-left_join( tmp, vac_cov_renamed_df, by = c("age_group", "patient_address_muni_code", "date_symptoms") ) rm(vac_cov_renamed_df) pre_vac_mask <-tmp2$date_symptoms <= ymd("2021-01-16") tmp2[pre_vac_mask, ]$proportion_vaccinated <-0.0Because there are not going to be any coverages from before the start of vaccination and there will be cases from this time, those records will get an NA for that variable so we need to fill those in with zero because that is the true value in that case.There are only 134 so it is a extremely small amount of corrupted data.
print(nrow( filter(vac_cov_df, date < as.Date("2021-01-17")))) [1] 134 Finally we put all of the data into a single data frame so we can give it to the glm function later.
52b1bb1ec523b67f093d5e8bd9cb5821221729921e0cc104526f2d015f2b6325 out/final-mortality-dataset.csv 10 BMJ Publishing Group Limited (BMJ) disclaims all liability and responsibility arising from any reliance Supplemental material placed on this supplemental material which has been supplied by the author(s)