How to Estimate Models with Measurement Error for our COVID-19 Indices

[This article was first published on R on Robert Kubinec, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

This tutorial gives an overview of the COVID-19 policy indexes just released by the CoronaNet project of which I am a part and the Oxford Government Response Tracker. See the associated paper here. This post shows you how to download the estimated indices and also how to incorporate measurement error into analyses using the eivtools and brms R packages.

As shown in our working paper, these six indices (social distancing policies, business restrictions, school restrictions, health resources, health monitoring and mask policies) provide statistical estimates that take into account coding and data limitations. Each index has an associated value between 0 and 100 along with an uncertainty interval that provides the most likely values for the true level of policy activity for that index type. We also provide for each estimate the posterior standard deviation of the estimate, which is an analogue to the standard error in frequentist statistics. These standard deviations are helpful for incorporating measurement error in analyses using existing packages without much further work. It is the aim of this blog post to show how to do that.

If you want to see the full code underlying this post as an Rmarkdown file, just hop over to the site’s Github repo. The Rmarkdown file is in the content/post folder and the Google mobility data is in the content/post/data folder.

Load and Merge Data

First, the outcome variable we will look at is the Google cell phone mobility data reported in retail establishments and restaurants. This is a key metric in COVID-19 spread because these kinds of areas can be a hotspot for indoors transmission of the virus.

mobility <- readRDS("data/gmob.rds") %>% 
    filter(is.na(sub_region_1), is.na(sub_region_2), is.na(metro_area)) %>% 
    select(country,date_policy="date",
           retail="retail_and_recreation_percent_change_from_baseline")

I next load the CoronaNet-OxCGRT indices from this direct download link as a CSV:

indices <- read_csv("https://drive.google.com/uc?export=download&id=1dMCTVPrf-tJyhv_uxr0yAQO-Elx0QOCG")

I also want to point out that we have the full list of indicators (i.e, individual policies as variables) available via a direct download link as well:

indicators <- read_csv("https://drive.google.com/uc?id=1lorcowHNnF0Vl6pxBjMdjTC4yPhHBLJI&export=download")

I’ll display the data to get a sense of what it looks like. First, these are the raw indicators. There are a lot of them–more than 160 variables:

indicators %>% 
  filter(date_policy>ymd("2020-04-01"),
         date_policy% 
  select(country,date_policy,social_distance,mask_everywhere,primary_school) %>% 
  knitr::kable() %>% 
  kable_styling()
country date_policy social_distance mask_everywhere primary_school
Afghanistan 2020-04-02 0 0.1624358 4.516191
Afghanistan 2020-04-03 0 0.1624358 4.516191
Afghanistan 2020-04-04 0 0.1624358 4.565445
Afghanistan 2020-04-05 0 0.1624358 4.565445
Afghanistan 2020-04-06 0 0.1624358 4.565445

The data is organized in a country-day-policy record format with the different policy indicators in columns. The fractional values indicate a proportion of the population that have the following policy in place (i.e., at the provincial or municipal level). The high value for primary_school has to do with the fact that this variable naturally has 3 levels, from totally open to totally closed, and when provincial and municipal policies are added it can exceed 3. For more information about the underlying indicators, you can see our codebook.

We can now take a glance at the estimated indices that collapse these indicators into single constructs:

indices %>% 
  filter(date_policy==ymd("2020-04-01"),
         country=="Afghanistan") %>% 
  knitr::kable() %>% 
  kable_styling()
country modtype date_policy med_est high_est low_est sd_est
Afghanistan Business Restrictions 2020-04-01 62.53219 67.09647 57.94249 2.8344479
Afghanistan Health Monitoring 2020-04-01 41.40538 48.99068 34.40337 4.4375788
Afghanistan Health Resources 2020-04-01 46.97723 52.57886 41.45534 3.4747765
Afghanistan Mask Policies 2020-04-01 40.92886 48.72028 32.89501 4.8434060
Afghanistan School Restrictions 2020-04-01 77.33891 78.97166 75.70822 0.9911008
Afghanistan Social Distancing 2020-04-01 61.88964 68.51834 55.19300 3.9687823

This dataset is organized somewhat differently. We have one row per index value per index type. The modtype column records which index is in each row. The columns include estimates of the given index for that day. med_est is the median posterior estimate; i.e., the most likely estimate. The upper and lower bounds of the 5% - 95% posterior distribution (the uncertainty interval) are the low_est - high_est columns. Finally, the posterior standard deviation (standard error) is the sd_est column.

If you just want “the” value of the index, then med_est is the best bet. However, it’s best not to ignore the other columns. A simple way to take into account measurement error is to fit three separate models, one with med_est, another with low_est, and another with high_est. If there are important differences across these models than you would know that measurement error in the indices could be affecting your results.

However, there are better ways to incorporate your uncertainty into models, which I will show next. We will use the retail and recreation outcome from the Google mobility as the response we want to understand, and each of our indices as predictors. We will use the sd_est column to incorporate Normally-distributed measurement error in our models; i.e., we will assume that measurement error is centered around med_est with a standard deviation of sd_est. It’s not an exact incorporation of uncertainty as the posterior isn’t a perfect bell curve shape, but it’s pretty good as the posterior is usually at least unimodal if not symmetric.

First I use the eivtools package to do a what is called an errors-in-variables model. These have been around for some time in economics and other fields, and are relatively easy to estimate. To do so, first I convert our index into two datasets with the posterior median estimates and standard deviations and one column for each index. I can then join these together to make one dataset with both estimates per index:

index_med <- select(indices,country,date_policy,med_est,modtype) %>% 
  spread(key="modtype",value="med_est")

index_sds <- select(indices,country,date_policy,sd_est,modtype) %>% 
  spread(key="modtype",value="sd_est")

# need to adjust names so that we can bind by column

names(index_med)[3:8] <- paste0(names(index_med)[3:8],"_med") %>% 
  str_replace(" ","_")
names(index_sds)[3:8] <- paste0(names(index_sds)[3:8],"_sd") %>% 
  str_replace(" ","_")

# merge back together

index_combined <- left_join(index_med,index_sds,by=c("country","date_policy"))

Finally we will merge our predictors into the Google mobility data:

# we only have index values through Jan 16th, 2021

mobility <- left_join(mobility,index_combined,by=c("country","date_policy")) %>% 
  filter(!is.na(Social_Distancing_sd))

To make the modeling a bit easier, we will only use a subset of the countries in the full data.

mobility <- filter(mobility, country %in% c("United States",
                                            "Brazil","China",
                                            "United Kingdom",
                                            "France",
                                            "South Africa","Nigeria",
                                            "Indonesia",
                                            "Australia","Thailand"))

Conventional Errors-in-Variables

Now we can use the eivreg as a function for estimating a linear regression model for our outcome, retail. We need to simplify our uncertainty measures because conventional measurement/errors-in-variables models can only handle one standard deviation/standard error per variable. As such, we will pass in the average posterior standard deviation for each index as our estimate of measurement error Sigma:

# first create Sigma, the covariance matrix
# only has a diagonal value as we don't have covariance estimates across indices
# we have to square them first as Sigma is a covariance matrix and we have standard deviations (variance = SD^2)

Sigma <- diag(c(mean(mobility$`Business_Restrictions_sd`^2,na.rm=T),
                mean(mobility$`Health_Monitoring_sd`^2,na.rm=T),
                mean(mobility$`Health_Resources_sd`^2,na.rm=T),
                mean(mobility$`Mask_Policies_sd`^2,na.rm=T),
                mean(mobility$`School_Restrictions_sd`^2,na.rm=T),
                mean(mobility$`Social_Distancing_sd`^2,na.rm=T)))

# add dimnames to identify each variables

dimnames(Sigma) <- list(c("Business_Restrictions_med",
                          "Health_Monitoring_med",
                          "Health_Resources_med",
                          "Mask_Policies_med",
                          "School_Restrictions_med",
                          "Social_Distancing_med"),
                        c("Business_Restrictions_med",
                          "Health_Monitoring_med",
                          "Health_Resources_med",
                          "Mask_Policies_med",
                          "School_Restrictions_med",
                          "Social_Distancing_med"))

retail_eiv <- eivreg(retail~Business_Restrictions_med +
                            Health_Monitoring_med +
                            Health_Resources_med +
                            Mask_Policies_med +
                            School_Restrictions_med +
                            Social_Distancing_med,
                     data=mobility,Sigma_error=Sigma)

We can then see what our estimated associations are given that we incorporated average standard errors for each index:

summary(retail_eiv)
## 
## Call:
## eivreg(formula = retail ~ Business_Restrictions_med + Health_Monitoring_med + 
##     Health_Resources_med + Mask_Policies_med + School_Restrictions_med + 
##     Social_Distancing_med, data = mobility, Sigma_error = Sigma)
## 
## Error Covariance Matrix
##                           Business_Restrictions_med Health_Monitoring_med
## Business_Restrictions_med                     3.658                 0.000
## Health_Monitoring_med                         0.000                 9.721
## Health_Resources_med                          0.000                 0.000
## Mask_Policies_med                             0.000                 0.000
## School_Restrictions_med                       0.000                 0.000
## Social_Distancing_med                         0.000                 0.000
##                           Health_Resources_med Mask_Policies_med
## Business_Restrictions_med                0.000              0.00
## Health_Monitoring_med                    0.000              0.00
## Health_Resources_med                     6.375              0.00
## Mask_Policies_med                        0.000             15.79
## School_Restrictions_med                  0.000              0.00
## Social_Distancing_med                    0.000              0.00
##                           School_Restrictions_med Social_Distancing_med
## Business_Restrictions_med                   0.000                 0.000
## Health_Monitoring_med                       0.000                 0.000
## Health_Resources_med                        0.000                 0.000
## Mask_Policies_med                           0.000                 0.000
## School_Restrictions_med                     1.755                 0.000
## Social_Distancing_med                       0.000                 8.896
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -53.157 -11.276   2.005  12.105  57.204 
## 
## Coefficients:
##                           Estimate Std. Error t value Pr(>|t|)    
## (Intercept)               12.45477    1.74730   7.128 1.30e-12 ***
## Business_Restrictions_med  0.27001    0.04336   6.228 5.48e-10 ***
## Health_Monitoring_med      0.02532    0.02871   0.882   0.3779    
## Health_Resources_med       0.32352    0.06208   5.212 2.01e-07 ***
## Mask_Policies_med         -0.04313    0.02231  -1.933   0.0533 .  
## School_Restrictions_med    0.17267    0.01967   8.779  < 2e-16 ***
## Social_Distancing_med     -1.42736    0.04879 -29.254  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## Number of observations used: 2680 
## Latent residual standard deviation: 16.14 
## Latent R-squared: 0.4038, (df-adjusted: 0.4022)
## 
## EIV-Adjusted vs Unadjusted Coefficients:
##                           Adjusted Unadjusted
## (Intercept)               12.45477   10.67932
## Business_Restrictions_med  0.27001    0.26442
## Health_Monitoring_med      0.02532    0.05415
## Health_Resources_med       0.32352    0.21373
## Mask_Policies_med         -0.04313   -0.05209
## School_Restrictions_med    0.17267    0.14216
## Social_Distancing_med     -1.42736   -1.25473

We see that the social distancing index has the strongest association with reduced retail mobility, followed by the mask policy index. The other indices are associated with increased retail mobility. It is important to note that even with the inclusion of measurement error, the effects are still very precisely estimated due to the amount of data we have.

Bayesian Measurement Models

Next we will do the same exercise, except we will use Bayesian estimation with the R package brms. The big advantage here is that we do not need to simplify to average posterior standard deviations for our estimate of uncertainty; rather, we can include the standard deviation/standard error for each variable separately. It’s also frankly easier to use this package as we don’t need to take the extra step of making a Sigma matrix.

Instead, we can use the handy me() function as part of the brms formula syntax to identify each variable with measurement error:

# read from disk as this can take a while to run

if(run_model) {
  
  # turn off correlations among latent variables to match what we gave 
  # eivreg
  
  brms_formula <- bf(retail ~ 
                  me(Business_Restrictions_med,Business_Restrictions_sd) +
                  me(Health_Monitoring_med,Health_Monitoring_sd) +
                  me(Health_Resources_med,Health_Resources_sd) +
                  me(Mask_Policies_med,Mask_Policies_sd) +
                  me(School_Restrictions_med,School_Restrictions_sd) +
                  me(Social_Distancing_med,Social_Distancing_sd),
                  center=TRUE) +
                  set_mecor(FALSE)
  
  brms_est <- brm(brms_formula,
                data=mobility,
                silent=0,
                chains=1,save_pars = save_pars(),
                iter=500,
                warmup=250,
                backend='rstan')
  
  saveRDS(brms_est,"data/brms_est.rds")
  
} else {
  
  brms_est <- readRDS("data/brms_est.rds")
  
}

We can then look at the estimated associations:

summary(brms_est)
##  Family: gaussian 
##   Links: mu = identity; sigma = identity 
## Formula: retail ~ me(Business_Restrictions_med, Business_Restrictions_sd) + me(Health_Monitoring_med, Health_Monitoring_sd) + me(Health_Resources_med, Health_Resources_sd) + me(Mask_Policies_med, Mask_Policies_sd) + me(School_Restrictions_med, School_Restrictions_sd) + me(Social_Distancing_med, Social_Distancing_sd) 
##    Data: mobility (Number of observations: 2680) 
## Samples: 1 chains, each with iter = 500; warmup = 250; thin = 1;
##          total post-warmup samples = 250
## 
## Population-Level Effects: 
##                                                     Estimate Est.Error l-95% CI
## Intercept                                              13.89      1.95    10.20
## meBusiness_Restrictions_medBusiness_Restrictions_sd     0.28      0.04     0.21
## meHealth_Monitoring_medHealth_Monitoring_sd             0.06      0.02     0.02
## meHealth_Resources_medHealth_Resources_sd               0.20      0.05     0.11
## meMask_Policies_medMask_Policies_sd                    -0.06      0.02    -0.09
## meSchool_Restrictions_medSchool_Restrictions_sd         0.14      0.02     0.10
## meSocial_Distancing_medSocial_Distancing_sd            -1.31      0.04    -1.38
##                                                     u-95% CI Rhat Bulk_ESS
## Intercept                                              17.55 1.00      218
## meBusiness_Restrictions_medBusiness_Restrictions_sd     0.35 1.00      136
## meHealth_Monitoring_medHealth_Monitoring_sd             0.11 1.00      119
## meHealth_Resources_medHealth_Resources_sd               0.28 1.00      119
## meMask_Policies_medMask_Policies_sd                    -0.03 1.01      249
## meSchool_Restrictions_medSchool_Restrictions_sd         0.18 1.06      288
## meSocial_Distancing_medSocial_Distancing_sd            -1.23 1.00      213
##                                                     Tail_ESS
## Intercept                                                187
## meBusiness_Restrictions_medBusiness_Restrictions_sd      128
## meHealth_Monitoring_medHealth_Monitoring_sd              193
## meHealth_Resources_medHealth_Resources_sd                196
## meMask_Policies_medMask_Policies_sd                      206
## meSchool_Restrictions_medSchool_Restrictions_sd          147
## meSocial_Distancing_medSocial_Distancing_sd              162
## 
## Family Specific Parameters: 
##       Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
## sigma    16.22      0.24    15.79    16.68 1.00      573      197
## 
## Samples were drawn using sampling(NUTS). For each parameter, Bulk_ESS
## and Tail_ESS are effective sample size measures, and Rhat is the potential
## scale reduction factor on split chains (at convergence, Rhat = 1).

The associations are quite similar in magnitude and sign to those we estimated with eivreg. We estimated that an increase of one unit in the social distancing index is associated with a -1.427 decrease in retail mobility, while with brms we estimated a -1.31 decrease in retail mobility.

As we can see, for this simple model the two are equivalent, and eivreg is much faster–it took almost no time to estimate the model, while brms took 20 minutes. However, brms is incorporating much more measurement uncertainty, and brms also allows for a much wider of array of modeling options, such as GLMs (probit/logit/ordered logit), multilevel/hierarchical models, and multiple imputation. The eivreg is limited to continuous (Gaussian) outcomes and does not allow for interactions with variables with measurement uncertainty. In any case, both of these packages will incorporate measurement uncertainty in their estimates.

Comparison with No Measurement Error

Finally, we can examine what measurement error does to our coefficients’ uncertainty by comparing the results to the standard OLS lm regression that ignores uncertainty:

lm_est <- lm(retail~Business_Restrictions_med +
                            Health_Monitoring_med +
                            Health_Resources_med +
                            Mask_Policies_med +
                            School_Restrictions_med +
                            Social_Distancing_med,
                     data=mobility)

summary(lm_est)
## 
## Call:
## lm(formula = retail ~ Business_Restrictions_med + Health_Monitoring_med + 
##     Health_Resources_med + Mask_Policies_med + School_Restrictions_med + 
##     Social_Distancing_med, data = mobility)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -53.785 -10.103   2.617  11.961  55.443 
## 
## Coefficients:
##                           Estimate Std. Error t value Pr(>|t|)    
## (Intercept)               10.67932    1.97234   5.415 6.69e-08 ***
## Business_Restrictions_med  0.26442    0.03575   7.397 1.85e-13 ***
## Health_Monitoring_med      0.05415    0.02351   2.304  0.02132 *  
## Health_Resources_med       0.21373    0.04215   5.071 4.23e-07 ***
## Mask_Policies_med         -0.05209    0.01797  -2.899  0.00377 ** 
## School_Restrictions_med    0.14216    0.01881   7.557 5.64e-14 ***
## Social_Distancing_med     -1.25473    0.03588 -34.971  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 16.66 on 2673 degrees of freedom
## Multiple R-squared:  0.3654, Adjusted R-squared:  0.364 
## F-statistic: 256.6 on 6 and 2673 DF,  p-value: < 2.2e-16

We can see that the coefficients are similar though not identical to brms and eivreg, and the standard errors are noticeably smaller. The eivreg estimated that there was 19.44% more uncertainty than the naive lm estimator, and brms 8.33% more uncertainty. As such, this kind of uncertainty is important to take into account, and it can get much more critical when there is less data available or when are examining non-linear effects such as interactions.

To leave a comment for the author, please follow the link and comment on their blog: R on Robert Kubinec.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)