upmfit Primer

Avery I. McIntosh

This vignette is long form documentation for the R package \(upmfit\). It provides some background on the package method, developed by McIntosh, et al.,1 and walks the reader through examples of the package functions.

Background

Tuberculosis (TB) household contact studies are a mainstay of TB research. The general framework of such studies is that individuals presenting with TB-like symptoms arrive at clinic, are diagnosd with TB, and are asked to participate in a household study where all household members are enrolled and have demographic and health variables recorded by a study team. The person first presenting as sick, the index case, often has disease characteristics recorded such as the duration of cough or HIV status. The remaining household members, the so-called household contacts, are usually given a Tuberculin Skin Test (TST) at baseline to test for latent TB infection, and then again later on after a period of follow-up, which could be 6 weeks to 6 month post study enrollment.

A particularly difficult problem with these studies is that investigators are unable to statistically account for the fact that in an area with a high prevalence of TB, infection of a household contact from a person outside their home is quite likely, even if they are living with a person who has infectious TB. This omission of risk from a “community” source can introduce substantial bias into estimates of housheold risk factors for TB transmission. A further issue is that beyond prevalence estimates, investigators do not have much of an indication as to the risk of TB acquisition from outside the home.

Household-community dynamic models have been proposed for other diseases such as flu, however the unique characteristics of TB (a long latency period and unobserved transmission) make the use of standard models for household-community infection ill-equiped to capture the dynamics of TB infection.

Method

The Unified Probability Model (UPM) addresses the limitations of household contact studies for TB by modeling risk of infection from the community as a noise parameter in a Bayesian mixed effects logistic regression model of individual- and household-based risk factors for TB infection (e.g. number of windows in the home, age and smoking status for each contact). The parameterization of the model makes it weakly non-identifiable, meaning the score contains discontinuities for some parameter values. This limitation is particularly acute in the presence of household clustering, which is fundamental in household contact studies. However, with some very minimal prior probility distribution specification on the household clustering effect, the model can generate posterior probability distributions for coefficients of household risk, which can be exponentiated to yield odds ratios, as well as a posterior distribution of the risk of household-acquired and community-acquired infection among household contacts of a primary index case, what in the function output for upmbuilder() and upmrun() is returned as pHHinfection and post.comm.risk, respectively, and in the model specification in McIntosh, et al. is denoted \(p^C\) and \(p^{HH}\).

It should be noted that, while the UPM and R package \(upmfit\) were designed specifically with tuberculosis household contact studies in mind, the method is applicable to any scenario where there are two competing risks for a single outcome with unobserved linkage between the sources of risk and the outcome. Examples where this method could be applicable are in profiling the risk of MRSA infection from a nosocomial versus other source, or in modeling risk of TB infection among populations utilizing homeless shelters. Non-clinical applications of the UPM could be an industrial process control setting where a manufactured item has two sources of degradation, one observed and one unobserved, or a chemical mixing procedure where catalysis can occur from the mixing of an exogenous agent or autocatalysis.

Examples

The \(upmfit\) package comes with synthetic dataset upmdata inbuilt for application of the package functions by the user. Once the package library is loaded, the user can display the first few rows of the dataset and some of the data characteristics:

head(upmdata)
##       x1     x2    x3    x4 x5 cluster y
## 1  5.293  0.272 2.093 4.354  0       1 1
## 2  1.398  0.975 1.911 4.354  0       1 0
## 3  0.449 -0.002 0.031 4.354  0       1 1
## 4  4.371  3.165 2.009 4.354  0       1 0
## 5 -0.181  1.963 3.830 4.354  1       1 1
## 6 -2.240  0.121 5.435 4.354  1       1 1
dim(upmdata) #dataset dimensions (rows by columns)
## [1] 800   7

Each observation (row) in upmdata was simulated to have a particular risk of having outcome \(y\), which was determined by a logit-transformed linear combination of predictors \(x1\) through \(x5\). The first four predictors are numeric, with the fourth being constant at the household or index case level. The final predictor \(x5\) is binary. The parameter values for the predictors are:

Each observation is clustered such that observations falling into the same cluster have some common Gaussian noise added to the linear component of risk. This noise component is also known as “random effects” or “hierarchical regression.” In the synthetic dataset there are 108 households, roughly the size of many tuberculosis household studies. Additionally, 16 percent of the data were simulated such that irrespective of the individual risk of outcome \(y\), those persons are assigned outcome \(y\) = 1. This random assignment of outcomes to a subset of the at-risk population simulates the so-called community-acquired infection.

Observe the following example application of function upmbuilder(). This function takes are argument: a dataset; if applicable, a vector indicating the columns in the dataset which are categorical variables, for restructuring of the model formulation; and if desirable, prior probability distribution parameters other than the default values (which are relatively non-informative on the odds-ratio scale). It outputs a list of three items: a JAGS model script, a design matrix (possibly updated due to restructing in the presence of categorical predictors); and a list of the modeled coefficients.

First observe the output of the design matrix. In this example, there are no categorical variables.

upmbuilder(upmdata)[[2]][1:5,]
##       x1     x2    x3    x4 x5 cluster y
## 1  5.293  0.272 2.093 4.354  0       1 1
## 2  1.398  0.975 1.911 4.354  0       1 0
## 3  0.449 -0.002 0.031 4.354  0       1 1
## 4  4.371  3.165 2.009 4.354  0       1 0
## 5 -0.181  1.963 3.830 4.354  1       1 1

The design matrix output is unchanged due to the absence of any categorical predictors. Now observe the reformulation of the design matrix in the presence of categorical predictors (the function suppressWarnings() is invoked below to supress the inbuilt warning in upmbuilder() which automatically gives a warning to users about reformulation of indicator variable prior probabilities in the presence of categorical variables).

test.data <- data.frame(y=rbinom(10,1,runif(10)),
                        x=rnorm(10),
                        w=rnorm(10),
                        z=c(rep("red",3),rep("green",3),rep("blue",4)),
                        cluster=c(1,1,2,2,5:10)
                        )
#a simulated dataset with categorical predictor 'z'
test.data
##    y          x          w     z cluster
## 1  0  0.9532668  0.2774668   red       1
## 2  0 -0.7744096  0.2448819   red       1
## 3  0  1.0604747 -1.1251807   red       2
## 4  1  0.8758088 -0.9981249 green       2
## 5  1 -0.5462657  2.8004981 green       5
## 6  1 -0.4122659  1.1273394 green       6
## 7  0 -0.0379532 -0.9825754  blue       7
## 8  1  0.2578133 -0.5506083  blue       8
## 9  1 -0.8936549  1.2780891  blue       9
## 10 0  0.8720952 -0.4469763  blue      10
suppressWarnings(upmbuilder(test.data, categorical.columns = 4)[[2]])
##             x          w zgreen zred cluster y
## 1   0.9532668  0.2774668      0    1       1 0
## 2  -0.7744096  0.2448819      0    1       1 0
## 3   1.0604747 -1.1251807      0    1       2 0
## 4   0.8758088 -0.9981249      1    0       2 1
## 5  -0.5462657  2.8004981      1    0       5 1
## 6  -0.4122659  1.1273394      1    0       6 1
## 7  -0.0379532 -0.9825754      0    0       7 0
## 8   0.2578133 -0.5506083      0    0       8 1
## 9  -0.8936549  1.2780891      0    0       9 1
## 10  0.8720952 -0.4469763      0    0      10 0

Note that column \(z\) in the original dataset has been reformulated as two new columns to reflect the indicator variable status of the predictors. Now the intercept variable references the \(z\) variable level “blue.”

Next, observe the model formulation for the inbuilt dataset upmdata (the cat() function is invoked here because otherwise the model formulation is in one uninterrupted chunk for reading into function upmfit()):

cat(upmbuilder(upmdata)[[1]])
function(){}
for(i in 1:n){
y[i]~dbin(theta[i]+Pc[i]-Pc[i]*theta[i], 1)
logit(theta[i])<-beta.0+beta.1*x1[i]+beta.2*x2[i]+beta.3*x3[i]+beta.4*x4[i]+beta.5*x5[i]+b.0[cluster[i]]
logit(Pc[i])<-alpha
}
for(j in 1:max(cluster)){
b.0[j]~dnorm(b.0.hat[j],tau.beta.0)
b.0.hat[j]<-mu.beta.0
}
mu.beta.0~dnorm(0,0.5)
tau.beta.0<-pow(sigma.b.0,-2)
sigma.b.0~dunif(0,10)
post.comm.risk<-exp(alpha)/(1+exp(alpha))
pHHinfection<-1-(post.comm.risk+(1-pTBinfection))
pTBinfection~dbeta(1.2+k,1.2+n-k)
beta.0~dnorm(0,0.1)
beta.1~dnorm(0,0.1)
beta.2~dnorm(0,0.1)
beta.3~dnorm(0,0.1)
beta.4~dnorm(0,0.1)
beta.5~dnorm(0,0.1)
alpha~dnorm(0,0.44)
}

If one were to run the above defined test.data through upmbuilder(), the model formulation and priors would automatically change to reflect the indicator variable parameterization. This point must be stressed: if the user inputs categorical variables and wishes to use prior probability distributions for model coefficients different than the default values, they must input the hyperparameters to reflect the indicator variable reformulation of the design matrix.

Also note that the section of code defining clusters (households) is omitted in the model formula if there is not a variable in the input design matrix with title “cluster.”

Finally, observe the coefficient output for the two above mentioned datasets.

suppressWarnings(upmbuilder(test.data, categorical.columns = 4))[[3]]
## [1] "beta.0" "beta.1" "beta.2" "beta.3" "beta.4"
upmbuilder(upmdata)[[3]]
## [1] "beta.0" "beta.1" "beta.2" "beta.3" "beta.4" "beta.5"

To implement the model-building function upmrun(), the user will use syntax similar to that used in upmbuilder(), with the exception that upmrun() contains additional function options for specifying the MCMC characteristic of the sampler: number of MCMC samples desired by the user, the burn-in period, thinning interval, and so on. In this vignette the iterations are limited to 100 for timely package building, but in practice at least the default setting of 50000 should be used to ensure MCMC convergence.

example.run <- upmrun(upmdata, n.iter=100)
## module glm loaded
## Compiling model graph
##    Resolving undeclared variables
##    Allocating nodes
## Graph information:
##    Observed stochastic nodes: 800
##    Unobserved stochastic nodes: 118
##    Total graph size: 12887
## 
## Initializing model
example.run
## Inference for Bugs model at "/var/folders/3h/5ywnq74520nfv8rly6p_dmlh0000gn/T//RtmpkzFwyH/model1dff21e931f4.txt", fit using jags,
##  3 chains, each with 100 iterations (first 50 discarded)
##  n.sims = 150 iterations saved
##                mu.vect sd.vect    2.5%     25%     50%     75%   97.5%
## beta.0          -2.258   1.342  -4.265  -3.371  -2.530  -1.183   0.107
## beta.1           0.222   0.146  -0.056   0.123   0.209   0.317   0.493
## beta.2          -4.316   1.012  -6.269  -4.904  -4.331  -3.558  -2.318
## beta.3          -0.181   0.250  -0.876  -0.253  -0.147  -0.045   0.326
## beta.4           0.469   0.265  -0.009   0.265   0.476   0.637   1.016
## beta.5          -0.433   0.800  -2.015  -0.865  -0.450  -0.054   1.359
## mu.beta.0       -0.784   0.918  -2.067  -1.519  -0.930  -0.173   1.046
## pHHinfection     0.148   0.029   0.094   0.128   0.147   0.164   0.203
## post.comm.risk   0.211   0.023   0.161   0.195   0.213   0.226   0.254
## sigma.b.0        3.212   0.828   1.704   2.444   3.314   3.796   4.698
## deviance       825.047  15.301 793.265 814.360 826.407 835.801 851.175
##                 Rhat n.eff
## beta.0         3.772     3
## beta.1         1.175    16
## beta.2         2.398     4
## beta.3         1.637     7
## beta.4         1.959     5
## beta.5         1.124    35
## mu.beta.0      1.990     5
## pHHinfection   1.311    10
## post.comm.risk 1.704     6
## sigma.b.0      2.991     4
## deviance       1.072    29
## 
## For each parameter, n.eff is a crude measure of effective sample size,
## and Rhat is the potential scale reduction factor (at convergence, Rhat=1).
## 
## DIC info (using the rule, pD = var(deviance)/2)
## pD = 110.2 and DIC = 935.3
## DIC is an estimate of expected predictive error (lower deviance is better).

The output is identical to a jags() invocation run in R using a user-defined long form model script. Note that using the default settings for all MCMC sampler controls is not always sufficient for the posterior parameter estimates, such as the posterior median, to converge. Diagnostic Rhat could be less than the rule-of-thumb value of 1.10 for the variables, and the effective sample size n.eff can be low. These convergence indicators show that for chain convergence such that the user is sampling from the true posterior distribution for each parameter, the user may need to specify more samples, more thinning, a larger burn-in period, or some combination of all three controls. Further analysis of convergence using trace plots and overlaid chain densities is always warranted when assessing convergence of Bayesian models.

To access the full posterior chains for each variable, load package \(mcmcplots\) and convert the defined JAGS object into posterior chains of samples:

#load package mcmcplots
library(mcmcplots)
## Loading required package: coda
## Warning: package 'coda' was built under R version 3.3.2
#define object as posterior chains
example.run_mcmc <- as.mcmc(example.run)

#print first few observations of each chain
example.run_mcmc[1:5,]
## [[1]]
##          beta.0     beta.1    beta.2     beta.3    beta.4    beta.5
## [1,] -0.9807289 0.09281821 -5.580786 -0.7299452 0.6207864 -1.080816
## [2,] -1.2650245 0.03936879 -5.525109 -0.7505877 0.6636299  1.209771
## [3,] -1.2140731 0.24755521 -5.587167 -0.9520320 0.6478741  1.393055
## [4,] -1.2557144 0.13510205 -5.667918 -0.8504242 0.7993285  1.494370
## [5,] -1.3539319 0.06791666 -5.592306 -0.8831148 0.4585562  1.274226
##      deviance  mu.beta.0 pHHinfection post.comm.risk sigma.b.0
## [1,] 818.1864 -1.4854618    0.1537017      0.2250023  3.621268
## [2,] 833.9170 -0.9050296    0.1418943      0.2307581  3.515708
## [3,] 831.9699 -1.5826095    0.1386300      0.2406710  3.942612
## [4,] 820.3968 -1.1360879    0.1606185      0.2183772  3.575530
## [5,] 814.2532 -0.8540137    0.1051514      0.2606706  3.570303
## 
## [[2]]
##         beta.0      beta.1    beta.2     beta.3    beta.4     beta.5
## [1,] -4.256739 -0.05419337 -5.257774  0.1092233 0.6347516 -0.5631470
## [2,] -4.289139  0.42076017 -5.650885 -0.1839725 0.6528995 -0.5250978
## [3,] -4.222557  0.41516282 -5.230273 -0.1713673 0.9006543 -0.8318569
## [4,] -3.837967  0.26935009 -5.290811 -0.2181727 0.6398469 -0.7116408
## [5,] -3.897061  0.26159389 -4.847695 -0.2472581 0.6960326 -0.6403933
##      deviance   mu.beta.0 pHHinfection post.comm.risk sigma.b.0
## [1,] 809.5547  0.04465329   0.14021802      0.2072171  3.571338
## [2,] 802.8869 -0.43551302   0.11050942      0.2319731  3.310286
## [3,] 812.2898 -0.53315554   0.12332849      0.2159276  3.592864
## [4,] 815.7514  0.38865116   0.13328916      0.2162410  3.477887
## [5,] 826.4629  0.22064270   0.07293174      0.2524666  3.195803
## 
## [[3]]
##          beta.0     beta.1    beta.2        beta.3      beta.4      beta.5
## [1,] -0.5100618 0.20659232 -4.073020  0.0127790309 -0.00759144 -0.73532092
## [2,] -0.4547172 0.13539856 -3.756220 -0.0040940053  0.22697941 -0.23478335
## [3,] -0.4078059 0.05115238 -3.456113 -0.0105406490  0.06160016 -0.31511917
## [4,] -0.2947321 0.06179592 -3.500758 -0.0003751443  0.15143673 -0.24653238
## [5,]  0.2847193 0.14087663 -3.497360 -0.1230872340  0.15328087 -0.03781239
##      deviance mu.beta.0 pHHinfection post.comm.risk sigma.b.0
## [1,] 815.0231 -1.758840    0.1224736      0.2098310  3.042628
## [2,] 810.4950 -2.047335    0.1529152      0.1997978  2.870742
## [3,] 821.0664 -1.839556    0.1479164      0.1919286  2.945661
## [4,] 808.6391 -2.054152    0.2103109      0.1613118  2.917380
## [5,] 802.9224 -1.825428    0.1939100      0.1601808  2.301566

Note that the prior probability distribution for variable \(alpha\) must be transformed to be interpretable on the unit interval. The default parameter values for \(alpha\) are a Gaussian distribution with mean 0, standard deviation 1/\(\sqrt{0.44}\) \(\approx\) 1.51. BUGS-type scripts do not recognize standard deviation, but instead use \(precision\), which is 1 divided by the square root of the standard deviation. Thus, to generate an informative prior distribution on \(alpha\) that translates into a prior on \(p^C\) having mean and median close to 0.10, the following function argument would be added to the model: prior.alpha = c(-2.2, 1/sqrt(3)). Observe the distribution of \(alpha\) and \(p^C\) for these values:

#summary of p^C:
alpha.values <- rnorm(5000,-2.2, sd=1/sqrt(3))
summary(exp(alpha.values)/(1+exp(alpha.values)))
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
## 0.01018 0.06976 0.10020 0.11200 0.14120 0.47610

Care must be taken in specifying a prior probability distribution for the model parameters. The UPM does well in simulation with relatively non-informative priors (the defaults for this package), however there are certainly times when the user would wish to specify an informative prior. A good example might be on a variable such as smoking status. The effect of smoking on odds of TB acquisition may not be exactly known, but smoking is certainly not protective against transmission. To ignore this obvious direction of effect would be a mistake. Thus, like all Bayesian models, care should be taken when choosing prior probability distributions for UPM parameters, and it may be useful to run the model with a range of informative priors to assess the sensitivity of posterior estimates to prior information.

Note that in the construction of the posterior risk of household-acquired infection term \(p^{HH}\), one must specify a prior probability distribution on the risk of TB infection in the study population. The default prior probability for this term is relatively non-informative: a Beta-distributed random variable with primary and secondary shape parameters both equal to 1.2. For the often substantial sample size used in household contact studies (often in the hundreds or thousands) the posterior probability distribution is robust to prior distribution specification. In this version, the prior specification for \(p^{TB}\) used to generate \(p^{HH}\) is fixed. Future iterations of this package will allow for user-specified prior shape parameters to this term.

To select competing parameter sets for the UPM, or for comparison of the UPM to a competing Bayesian model, we recommend the model fit tool known as the Deviance Information Criterion. This model selection tool is calcualated and output automatically by JAGS. It is defined as \begin{equation} \text{DIC} = \hat{D} +2p_D, \end{equation}

where \(\hat{D}\) is the model deviance (\(-2\) times the log-likelihood) calculated at the means of the individual parameter posterior distributions, and \(p_D\) is the effective number of parameters, defined as \(\bar{D}-\hat{D}\), where \(\bar{D}\) is the mean of posterior distribution of the deviance. A model with smaller DIC than a competing model is judged to estimate the data better, while accounting for overfitting. DIC is a Bayesian analogue to the well established Akaike Information Criterion (AIC), which can be defined as a log-likelihood penalized for parameter dimensionality, aiming to balance model fit with model complexity. The dimensionality penalization is not exactly translatable into a hierarchical Bayesian setting, where random effects are estimated directly as part of fitting the model. The notion of model complexity in this setting must take into account the prior probabilty specification and joint constraints on model parameters. Thus, model complexity is defined for DIC using \(p_D\), and not simply the number of modeled parameters, as is done for AIC. Note that JAGS and WINBUGS/OPENBUGS calculate deviance in the same manner, but the same is not true for \(p_D\), which is calculated differently for JAGS and WINBUGS/OPENBUGS, which may return different values due to the calculation method. Thus, comparative analysis of model fit should be done using the same software platform.


  1. Extensions to Bayesian Generalized Linear Mixed Effects Models for Household Tuberculosis Transmission, \(Statistics\ in\ Medicine\) (2017)