You are reading the work-in-progress first edition of *Causal Inference in R*. This chapter has its foundations written but is still undergoing changes.

# 9 Using the propensity score

The propensity score is a *balancing* tool – we use it to help us make our exposure groups *exchangeable*. There are many ways to incorporate the propensity score into an analysis. Commonly used techniques include stratification (estimating the causal effect within propensity score stratum), matching, weighting, and direct covariate adjustment. In this section, we will focus on *matching* and *weighting*; other techniques will be discussed once we introduce the *outcome model*. Recall at this point in the book we are still in the *design* phase. We have not yet incorporated the outcome into our analysis at all.

## 9.1 Matching

Ultimately, we want the exposed and unexposed observations to be *exchangeable* with respect to the confounders we have proposed in our DAG (so we can use the observed effect for one to estimate the counterfactual for the other). One way to do this is to ensure that each observation in our analysis sample has at least one observation of the opposite exposure that has *match*ing values for each of these confounders. If we had a small number of binary confounders, for example, we might be able to construct an *exact match* for observations (and only include those for whom such a match exists), but as the number and continuity of confounders increases, exact matching becomes less feasible. This is where the propensity score, a summary measure of all of the confounders, comes in to play.

Let’s setup the data as we did in Chapter 8.

```
library(broom)
library(touringplans)
seven_dwarfs_9 <- seven_dwarfs_train_2018 |> filter(wait_hour == 9)
```

We can re-fit the propensity score using the MatchIt package, as below. Notice here the `matchit`

function fit a logistic regression model for our propensity score, as we had in Chapter 8. There were 60 days in 2018 where the Magic Kingdom had extra magic morning hours. For each of these 60 exposed days, `matchit`

found a comparable unexposed day, by implementing a nearest-neighbor match using the constructed propensity score. Examining the output, we also see that the target estimand is an “ATT” (do not worry about this yet, we will discuss this and several other estimands in Chapter 11).

```
library(MatchIt)
m <- matchit(
park_extra_magic_morning ~ park_ticket_season + park_close + park_temperature_high,
data = seven_dwarfs_9
)
m
```

```
A matchit object
- method: 1:1 nearest neighbor matching without replacement
- distance: Propensity score
- estimated with logistic regression
- number of obs.: 354 (original), 120 (matched)
- target estimand: ATT
- covariates: park_ticket_season, park_close, park_temperature_high
```

We can use the `get_matches`

function to create a data frame with the original variables that only consists of those who were matched. Notice here our sample size has been reduced from the original 354 days to 120.

```
matched_data <- get_matches(m)
glimpse(matched_data)
```

```
Rows: 120
Columns: 18
$ id <chr> "5", "340", "12", "1…
$ subclass <fct> 1, 1, 2, 2, 3, 3, 4,…
$ weights <dbl> 1, 1, 1, 1, 1, 1, 1,…
$ park_date <date> 2018-01-05, 2018-12…
$ wait_hour <int> 9, 9, 9, 9, 9, 9, 9,…
$ attraction_name <chr> "Seven Dwarfs Mine T…
$ wait_minutes_actual_avg <dbl> 33.0, 8.0, 114.0, 32…
$ wait_minutes_posted_avg <dbl> 70.56, 80.62, 79.29,…
$ attraction_park <chr> "Magic Kingdom", "Ma…
$ attraction_land <chr> "Fantasyland", "Fant…
$ park_open <time> 09:00:00, 09:00:00,…
$ park_close <time> 24:00:00, 23:00:00,…
$ park_extra_magic_morning <dbl> 1, 0, 1, 0, 1, 0, 1,…
$ park_extra_magic_evening <dbl> 0, 0, 0, 0, 0, 0, 0,…
$ park_ticket_season <chr> "regular", "regular"…
$ park_temperature_average <dbl> 43.56, 57.61, 70.91,…
$ park_temperature_high <dbl> 54.29, 65.44, 78.26,…
$ distance <dbl> 0.18410, 0.18381, 0.…
```

## 9.2 Weighting

One way to think about matching is as a crude “weight” where everyone who was matched gets a weight of 1 and everyone who was not matched gets a weight of 0 in the final sample. Another option is to allow this weight to be smooth, applying a weight to allow, on average, the covariates of interest to be balanced in the weighted population. To do this, we will construct a weight using the propensity score. There are many different weights that can be applied, depending on your target estimand of interest (see Chapter 11 for details). For this section, we will focus on the “Average Treatment Effect” weights, commonly referred to as an “inverse probability weight”. The weight is constructed as follows, where each observation is weighted by the *inverse* of the probability of receiving the exposure they received.

\[w_{ATE} = \frac{X}{p} + \frac{(1 - X)}{1 - p}\]

For example, if observation 1 had a very high likelihood of being exposed given their pre-exposure covariates (\(p = 0.9\)), but they in fact were *not* exposed, their weight would be 10 (\(w_1 = 1 / (1 - 0.9)\)). Likewise, if observation 2 had a very high likelihood of being exposed given their pre-exposure covariates (\(p = 0.9\)), and they *were* exposed, their weight would be 1.1 (\(w_2 = 1 / 0.9\)). Intuitively, we give *more* weight to observations who, based on their measured confounders, appear to have useful information for constructing a counterfactual – we would have predicted that they were exposed and but by chance they were not, or vice-versa. The propensity package is useful for implementing propensity score weighting.

```
library(propensity)
seven_dwarfs_9_with_ps <-
glm(
park_extra_magic_morning ~ park_ticket_season + park_close + park_temperature_high,
data = seven_dwarfs_9,
family = binomial()
) |>
augment(type.predict = "response", data = seven_dwarfs_9)
seven_dwarfs_9_with_wt <- seven_dwarfs_9_with_ps |>
mutate(w_ate = wt_ate(.fitted, park_extra_magic_morning))
```

Table 9.1 shows the weights in the first column.

```
seven_dwarfs_9_with_wt |>
select(
w_ate,
.fitted,
park_date,
park_extra_magic_morning,
park_ticket_season,
park_close,
park_temperature_high
) |>
head() |>
knitr::kable()
```

w_ate | .fitted | park_date | park_extra_magic_morning | park_ticket_season | park_close | park_temperature_high |
---|---|---|---|---|---|---|

1.433 | 0.3019 | 2018-01-01 | 0 | peak | 23:00:00 | 58.63 |

1.392 | 0.2815 | 2018-01-02 | 0 | peak | 24:00:00 | 53.65 |

1.409 | 0.2900 | 2018-01-03 | 0 | peak | 24:00:00 | 51.11 |

1.232 | 0.1881 | 2018-01-04 | 0 | regular | 24:00:00 | 52.66 |

5.432 | 0.1841 | 2018-01-05 | 1 | regular | 24:00:00 | 54.29 |

1.262 | 0.2074 | 2018-01-06 | 0 | regular | 23:00:00 | 56.25 |

`seven_dwarfs_9_with_wt`

dataset, including their propensity scores in the `.fitted`

column and weight in the `w_ate`

column.