---
title: 'Using cobalt with Longitudinal Treatments'
author: "Noah Greifer"
date: "`r Sys.Date()`"
output: 
    html_vignette:
        df_print: kable
        toc: false

vignette: >
  %\VignetteIndexEntry{Using cobalt with Longitudinal Treatments}
  %\VignetteEngine{knitr::rmarkdown_notangle}
  %\VignetteEncoding{UTF-8}
bibliography: references.bib
link-citations: true
---

```{r, include = FALSE}
knitr::opts_chunk$set(message = FALSE,
                      fig.width = 5,
                      fig.height = 3.5,
                      fig.align = "center",
                      eval = rlang::is_installed("WeightIt"))
```

This is an introduction to the use of *cobalt* with longitudinal treatments. These occur when there are multiple treatment periods spaced over time, with the potential for time-dependent confounding and censoring to occur. A common way to estimate treatment effects in these scenarios is to use marginal structural models (MSMs), weighted by balancing weights. The goal of applying weights is to simulate a sequential randomization design, where the probability of being assigned to treatment at each time point is independent of each unit's prior covariate and treatment history. For introduction to MSMs in general, see @thoemmesPrimerInverseProbability2016, @vanderweeleCausalInferenceLongitudinal2016, @coleConstructingInverseProbability2008, or @robinsMarginalStructuralModels2000. The key issue addressed by this guide and *cobalt* in general is assessing balance before each treatment period to ensure the removal of confounding and bias due to censoring.

In preprocessing for MSMs, three types of variables are relevant: baseline covariates, treatments, and intermediate outcomes/time-varying covariates. The goal of balance assessment is to assess whether after preprocessing, the resulting sample is one in which each treatment is independent of baseline covariates, treatment history, and time-varying covariates. The tools in *cobalt* have been developed to satisfy these goals.

The next section describe how to use *cobalt*'s tools to assess balance with longitudinal treatments. First, we'll examine an example data set and identify some tools that can be used to generate weights for MSMs. Next we'll use `bal.tab()`, `bal.plot()`, and `love.plot()` to assess and present balance. Finally, we'll look at what changes when units are censored between treatment periods.

## Setup

We're going to use the `msmdata` data set in the *WeightIt* package.

```{r}
library("cobalt")
library("WeightIt")
data("msmdata", package = "WeightIt")
head(msmdata)
```

We have the variables `Y_B`, the outcome; `X1_0` and `X2_0`, the baseline covariates; `X1_1` and `X2_1`, time-varying covariates measured after treatment period 1; `X1_1` and `X2_1`, covariates measured after treatment period 2; and `A_1`, `A_2`, and `A_3`, the treatments at each of the three treatment periods.

The goal of balance assessment in this scenario is to ensure the following:

1)  `A_1` is independent from `X1_0` and `X2_0`
2)  `A_2` is independent from `X1_0`, `X2_0`, `A_1`, `X1_1`, and `X2_1`
3)  `A_3` is independent from `X1_0`, `X2_0`, `A_1`, `X1_1`, `X2_1`, `A_2`, `X1_2`, and `X2_2`

Note these conditions are different from and weaker than those described by @jacksonDiagnosticsConfoundingTimevarying2016. See his *confoundr* package for implementing the diagnostics he describes.

## `bal.tab()`

To examine balance on the original data, we can specify the treatment-covariate relationship we want to assess by using either the formula or data frame interfaces to `bal.tab()`. The formula interface requires a list of formulas, one for each treatment, and a data set containing the relevant variables. The data set must be in the "wide" setup, where each time point receives its own columns and each unit has exactly one row of data. The formula interface is similar to the *WeightIt* input seen below. The data frame interface requires a list of treatment values for each time point and a data frame or list of covariates for each time point. We'll use the formula interface here.

```{r}
bal.tab(list(A_1 ~ X1_0 + X2_0,
             A_2 ~ X1_1 + X2_1 +
               A_1 + X1_0 + X2_0,
             A_3 ~ X1_2 + X2_2 +
               A_2 + X1_1 + X2_1 +
               A_1 + X1_0 + X2_0),
        data = msmdata)
```

Here we see a summary of balance across all time points. This displays each variable, how many times it appears in balance tables, its type, and the greatest imbalance for that variable across all time points. Below this is a summary of sample sizes across time points. To request balance on individual time points, we can use the `which.time` argument, which can be set to one or more numbers or `.all` or `.none` (the default). Below we'll request balance on all time points by setting `which.time = .all`. Doing so hides the balance summary across time points, but this can be requested again by setting `msm.summary = TRUE`.

```{r}
bal.tab(list(A_1 ~ X1_0 + X2_0,
             A_2 ~ X1_1 + X2_1 +
               A_1 + X1_0 + X2_0,
             A_3 ~ X1_2 + X2_2 +
               A_2 + X1_1 + X2_1 +
               A_1 + X1_0 + X2_0),
        data = msmdata,
        which.time = .all)
```

Here we see balance by time point. At each time point, a `bal.tab` object is produced for that time point. These function just like regular `bal.tab` objects.

This output will appear no matter what the treatment types are (i.e., binary, continuous, multi-category), but for multi-category treatments or when the treatment types vary or for multiply imputed data, no balance summary will be computed or displayed.

To estimate the weights, we'll use `WeightIt::weightitMSM()` to fit a series of logistic regressions that generate the weights. See the *WeightIt* documentation for more information on how to use *WeightIt* with longitudinal treatments.

```{r}
Wmsm <- weightitMSM(list(A_1 ~ X1_0 + X2_0,
                         A_2 ~ X1_1 + X2_1 +
                           A_1 + X1_0 + X2_0,
                         A_3 ~ X1_2 + X2_2 +
                           A_2 + X1_1 + X2_1 +
                           A_1 + X1_0 + X2_0),
                    data = msmdata)
```

We can use `bal.tab()` with the `weightitMSM` object generated above. Setting `un = TRUE` would produce balance statistics before adjustment, like we did before. We'll set `which.time = .all` and `msm.summary = TRUE` to see balance for each time point and across time points.

```{r}
bal.tab(Wmsm, un = TRUE, which.time = .all, msm.summary = TRUE)
```

Note that to add covariates, we must use `addl.list` (which can be abbreviated as `addl`), which functions like `addl` in point treatments. The input to `addl.list` must be a list of covariates for each time point, or a single data data frame of variables to be assessed at all time points. The same goes for adding distance variables, which must be done with `distance.list` (which can be abbreviated as `distance`).

Next we'll use `bal.plot()` to more finely examine covariate balance.

## `bal.plot()`

We can compare distributions of covariates across treatment groups for each time point using `bal.plot()`, just as we could with point treatments.

```{r, fig.height=4}
bal.plot(Wmsm, var.name = "X1_0", which = "both",
         type = "histogram")
```

Balance for variables that only appear in certain time points will only be displayed at those time points:

```{r, fig.height=4}
bal.plot(Wmsm, var.name = "X2_1", which = "both")
```

As with `bal.tab()`, `which.time` can be specified to limit output to chosen time points.

Finally, we'll examine using `love.plot()` with longitudinal treatments to display balance for presentation.

## `love.plot()`

`love.plot()` works with longitudinal treatments just as it does with point treatments, except that the user can choose whether to display separate plots for each time point or one plot with the summary across time points. As with `bal.tab()`, the user can set `which.time` to display only certain time points, including setting it to `.all` to display all time points (note that not all variables will be present in all time points). When set to `.none` (the default), the summary across time points is displayed. The `agg.fun` argument is set to `"max"` by default.

```{r}
love.plot(Wmsm, binary = "std")
```

```{r}
love.plot(Wmsm, binary = "std", which.time = .all)
```

## Censoring

Units often stop being observed partway through a study, which leaves later treatments and the outcome unrecorded for them. If dropout depends on treatment or covariate history, the units who remain are not representative of the sample that started, and weighting them to resemble it is the standard remedy [@robinsAnalysisSemiparametricRegression1995; @hernanMarginalStructuralModels2000]. The censoring model that produces those weights sits in the sequence alongside the treatment models, and its balance is assessed at the point where the censoring occurs.

A censoring indicator is marked with `.cens()` and follows the survival convention, in which `1` means the unit was censored and `0` means it is still under observation. It goes in the list of models at the position where it happens. Below we add an indicator of dropout after treatment period 2, so that `A_3` is unobserved for the units it censors. Units with higher `X1_2` and those treated at period 2 are more likely to drop out.

```{r}
set.seed(100)
msmdata$C_2 <- rbinom(nrow(msmdata), 1,
                      prob = plogis(-4 + .35 * msmdata$X1_2 +
                                        .8 * msmdata$A_2))

#A_3 is not observed for the units censored after period 2
is.na(msmdata$A_3[msmdata$C_2 == 1]) <- TRUE
```

The list of formulas now has four entries for three treatment periods. The censoring model gets the same history as the treatment model that follows it.

```{r}
bal.tab(list(A_1 ~ X1_0 + X2_0,
             A_2 ~ X1_1 + X2_1 +
               A_1 + X1_0 + X2_0,
             .cens(C_2) ~ X1_2 + X2_2 +
               A_2 + X1_1 + X2_1 +
               A_1 + X1_0 + X2_0,
             A_3 ~ X1_2 + X2_2 +
               A_2 + X1_1 + X2_1 +
               A_1 + X1_0 + X2_0),
        data = msmdata)
```

There is one table per entry, of whichever kind that entry's model is. The first, second, and fourth are ordinary treatment balance tables; the third compares the units still under observation to the full sample at risk at that point, as described in the "Using *cobalt* with censoring" section of `vignette("cobalt")`. Its sample size table has a single column and reports how many units were at risk, how many remained, and how many were censored.

Two things about the third and fourth tables are worth pointing out. First, the fourth table is computed among the 6158 units still under observation, not all 7500: a unit censored at period 2 has no treatment at period 3 and nothing left to balance. Which units those are is worked out from the censoring indicators themselves, so it makes no difference whether the data leaves `A_3` missing for them, as here, or records a value anyway. Second, no balance summary across time points is displayed, and neither is the collected table of sample sizes that normally accompanies it; each time point's own table reports its sample sizes anyway. A censoring table and a treatment table describe different comparisons, so there is nothing meaningful to aggregate across them, just as there is nothing to aggregate across a mix of continuous and binary treatments. A list in which every entry is a censoring indicator is not a mixture and is summarized as usual.

`weightitMSM()` takes the same list and fits the censoring model along with the treatment models, returning a single set of weights that is their product.

```{r}
Wmsm.cens <- weightitMSM(list(A_1 ~ X1_0 + X2_0,
                              A_2 ~ X1_1 + X2_1 +
                                A_1 + X1_0 + X2_0,
                              .cens(C_2) ~ X1_2 + X2_2 +
                                A_2 + X1_1 + X2_1 +
                                A_1 + X1_0 + X2_0,
                              A_3 ~ X1_2 + X2_2 +
                                A_2 + X1_1 + X2_1 +
                                A_1 + X1_0 + X2_0),
                         data = msmdata)

bal.tab(Wmsm.cens, un = TRUE, which.time = 3)
```

Because the weights are the product across all four models, balance at any one time point is assessed with weights that include the others, censoring among them.

`bal.plot()` and `love.plot()` work here too. At a censoring time point, `bal.plot()` shows the weighted uncensored sample against the unweighted full sample rather than two treatment groups.

```{r, fig.height=4}
bal.plot(Wmsm.cens, var.name = "X1_2", which.time = 3,
         which = "both")
```

```{r}
love.plot(Wmsm.cens, binary = "std", which.time = .all)
```

## Other Packages

Here we used *WeightIt* to generate our MSM weights, but *cobalt* is compatible with other packages for longitudinal treatments as well. `CBMSM` objects from the *CBPS* package and `iptw` objects from the *twang* package can be used in place of the `weightitMSM` object in the above examples. In addition, users who have generated balancing weights outside any of these package can specify an argument to `weights` in `bal.tab()` with the formula or data frame methods to assess balance using those weights, or they can use the default method of `bal.tab()` to supply an object containing any of the objects required for balance assessment.

Note that *CBPS* estimates and assesses balance on MSM weights differently from *twang* and *cobalt*. Its focus is on ensuring balance across all treatment history permutations, whereas *cobalt* focuses on evaluating the similarity to sequential randomization. For this reason, it may appear that `CBMSM` objects have different balance qualities as measured by the two packages.

## References
