```
library(tidyverse)
library(RSQLite)
library(fixest)
library(broom)
```

# Difference in Differences

You are reading **Tidy Finance with R**. You can find the equivalent chapter for the sibling **Tidy Finance with Python** here.

In this chapter, we illustrate the concept of *difference in differences* (DD) estimators by evaluating the effects of climate change regulation on the pricing of bonds across firms. DD estimators are typically used to recover the treatment effects of natural or quasi-natural experiments that trigger sharp changes in the environment of a specific group. Instead of looking at differences in just one group (e.g., the effect in the treated group), DD investigates the treatment effects by looking at the difference between differences in two groups. Such experiments are usually exploited to address endogeneity concerns (e.g., Roberts and Whited 2013). The identifying assumption is that the outcome variable would change equally in both groups without the treatment. This assumption is also often referred to as the assumption of parallel trends. Moreover, we would ideally also want a random assignment to the treatment and control groups. Due to lobbying or other activities, this randomness is often violated in (financial) economics.

In the context of our setting, we investigate the impact of the Paris Agreement (PA), signed on December 12, 2015, on the bond yields of polluting firms. We first estimate the treatment effect of the agreement using panel regression techniques that we discuss in Fixed Effects and Clustered Standard Errors. We then present two methods to illustrate the treatment effect over time graphically. Although we demonstrate that the treatment effect of the agreement is anticipated by bond market participants well in advance, the techniques we present below can also be applied to many other settings.

The approach we use here replicates the results of Seltzer, Starks, and Zhu (2022) partly. Specifically, we borrow their industry definitions for grouping firms into green and brown types. Overall, the literature on ESG effects in corporate bond markets is already large but continues to grow (for recent examples, see, e.g., Halling, Yu, and Zechner (2021), Handler, Jankowitsch, and Pasler (2022), Huynh and Xia (2021), among many others).

The current chapter relies on this set of R packages.

## Data Preparation

We use TRACE and Mergent FISD as data sources from our `SQLite`

-database introduced in Accessing and Managing Financial Data and TRACE and FISD.

```
<- dbConnect(
tidy_finance SQLite(),
"data/tidy_finance_r.sqlite",
extended_types = TRUE
)
<- tbl(tidy_finance, "fisd") |>
fisd select(complete_cusip, maturity, offering_amt, sic_code) |>
collect() |>
drop_na()
<- tbl(tidy_finance, "trace_enhanced") |>
trace_enhanced select(cusip_id, trd_exctn_dt, rptd_pr, entrd_vol_qt, yld_pt)|>
collect() |>
drop_na()
```

We start our analysis by preparing the sample of bonds. We only consider bonds with a time to maturity of more than one year to the signing of the PA, so that we have sufficient data to analyze the yield behavior after the treatment date. This restriction also excludes all bonds issued after the agreement. We also consider only the first two digits of the SIC industry code to identify the polluting industries (in line with Seltzer, Starks, and Zhu 2022).

```
<- ymd("2015-12-12")
treatment_date
<- c(
polluting_industries 49, 13, 45, 29, 28, 33, 40, 20,
26, 42, 10, 53, 32, 99, 37
)
<- fisd |>
bonds filter(offering_amt > 0) |>
mutate(
time_to_maturity = as.numeric(maturity - treatment_date) / 365,
sic_code = as.integer(substr(sic_code, 1, 2)),
log_offering_amt = log(offering_amt)
|>
) filter(time_to_maturity >= 1) |>
select(
cusip_id = complete_cusip,
time_to_maturity, log_offering_amt, sic_code|>
) mutate(polluter = sic_code %in% polluting_industries)
```

Next, we aggregate the individual transactions as reported in TRACE to a monthly panel of bond yields. We consider bond yields for a bond’s last trading day in a month. Therefore, we first aggregate bond data to daily frequency and apply common restrictions from the literature (see, e.g., Bessembinder et al. 2008). We weigh each transaction by volume to reflect a trade’s relative importance and avoid emphasizing small trades. Moreover, we only consider transactions with reported prices `rptd_pr`

larger than 25 (to exclude bonds that are close to default) and only bond-day observations with more than five trades on a corresponding day (to exclude prices based on too few, potentially non-representative transactions).

```
<- trace_enhanced |>
trace_aggregated filter(rptd_pr > 25) |>
group_by(cusip_id, trd_exctn_dt) |>
summarize(
avg_yield = weighted.mean(yld_pt, entrd_vol_qt * rptd_pr),
trades = n(),
.groups = "drop"
|>
) drop_na(avg_yield) |>
filter(trades >= 5) |>
mutate(month = floor_date(trd_exctn_dt, "months")) |>
group_by(cusip_id, month) |>
slice_max(trd_exctn_dt) |>
ungroup() |>
select(cusip_id, month, avg_yield)
```

By combining the bond-specific information from Mergent FISD for our bond sample with the aggregated TRACE data, we arrive at the main sample for our analysis.

```
<- bonds |>
bonds_panel inner_join(trace_aggregated, join_by(cusip_id), multiple = "all") |>
drop_na()
```

Before we can run the first regression, we need to define the `treated`

indicator, which is the product of the `post_period`

(i.e., all months after the signing of the PA) and the `polluter`

indicator defined above.

```
<- bonds_panel |>
bonds_panel mutate(post_period = month >= floor_date(treatment_date, "months")) |>
mutate(treated = polluter & post_period)
```

As usual, we tabulate summary statistics of the variables that enter the regression to check the validity of our variable definitions.

```
|>
bonds_panel pivot_longer(
cols = c(avg_yield, time_to_maturity, log_offering_amt),
names_to = "measure"
|>
) group_by(measure) |>
summarize(
mean = mean(value),
sd = sd(value),
min = min(value),
q05 = quantile(value, 0.05),
q50 = quantile(value, 0.50),
q95 = quantile(value, 0.95),
max = max(value),
n = n(),
.groups = "drop"
)
```

```
# A tibble: 3 × 9
measure mean sd min q05 q50 q95 max n
<chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int>
1 avg_yield 4.08 4.21 0.0595 1.27 3.38 8.10 128. 127530
2 log_offering_amt 13.3 0.823 4.64 12.2 13.2 14.5 16.5 127530
3 time_to_maturity 8.55 8.41 1.01 1.50 5.81 27.4 101. 127530
```

## Panel Regressions

The PA is a legally binding international treaty on climate change. It was adopted by 196 Parties at COP 21 in Paris on 12 December 2015 and entered into force on 4 November 2016. The PA obliges developed countries to support efforts to build clean, climate-resilient futures. One may thus hypothesize that adopting climate-related policies may affect financial markets. To measure the magnitude of this effect, we first run an OLS regression without fixed effects where we include the `treated`

, `post_period`

, and `polluter`

dummies, as well as the bond-specific characteristics `log_offering_amt`

and `time_to_maturity`

. This simple model assumes that there are essentially two periods (before and after the PA) and two groups (polluters and non-polluters). Nonetheless, it should indicate whether polluters have higher yields following the PA compared to non-polluters.

The second model follows the typical DD regression approach by including individual (`cusip_id`

) and time (`month`

) fixed effects. In this model, we do not include any other variables from the simple model because the fixed effects subsume them, and we observe the coefficient of our main variable of interest: `treated`

.

```
<- feols(
model_without_fe fml = avg_yield ~ treated + post_period + polluter +
+ time_to_maturity,
log_offering_amt vcov = "iid",
data = bonds_panel
)
<- feols(
model_with_fe fml = avg_yield ~ treated | cusip_id + month,
vcov = "iid",
data = bonds_panel
)
etable(
model_without_fe, model_with_fe, coefstat = "tstat", digits = 3, digits.stats = 3
)
```

```
model_without_fe model_with_fe
Dependent Var.: avg_yield avg_yield
Constant 10.7*** (57.0)
treatedTRUE 0.462*** (9.31) 0.983*** (29.5)
post_periodTRUE -0.174*** (-5.92)
polluterTRUE 0.481*** (15.3)
log_offering_amt -0.551*** (-39.0)
time_to_maturity 0.058*** (41.6)
Fixed-Effects: ----------------- ---------------
cusip_id No Yes
month No Yes
________________ _________________ _______________
VCOV type IID IID
Observations 127,530 127,530
R2 0.032 0.647
Within R2 -- 0.007
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
```

Both models indicate that polluters have significantly higher yields after the PA than non-polluting firms. Note that the magnitude of the `treated`

coefficient varies considerably across models.

## Visualizing Parallel Trends

Even though the regressions above indicate that there is an impact of the PA on bond yields of polluters, the tables do not tell us anything about the dynamics of the treatment effect. In particular, the models provide no indication about whether the crucial *parallel trends* assumption is valid. This assumption requires that in the absence of treatment, the difference between the two groups is constant over time. Although there is no well-defined statistical test for this assumption, visual inspection typically provides a good indication.

To provide such visual evidence, we revisit the simple OLS model and replace the `treated`

and `post_period`

indicators with month dummies for each group. This approach estimates the average yield change of both groups for each period and provides corresponding confidence intervals. Plotting the coefficient estimates for both groups around the treatment date shows us the dynamics of our panel data.

```
<- feols(
model_without_fe_time fml = avg_yield ~ polluter + month:polluter +
+ log_offering_amt,
time_to_maturity vcov = "iid",
data = bonds_panel |>
mutate(month = factor(month))
)
<- tidy(model_without_fe_time) |>
model_without_fe_coefs filter(str_detect(term, "month")) |>
mutate(
month = ymd(substr(term, nchar(term) - 9, nchar(term))),
treatment = str_detect(term, "TRUE"),
ci_up = estimate + qnorm(0.975) * std.error,
ci_low = estimate + qnorm(0.025) * std.error
)
|>
model_without_fe_coefs ggplot(aes(
month, color = treatment,
linetype = treatment,
shape = treatment
+
)) geom_vline(aes(xintercept = floor_date(treatment_date, "month")),
linetype = "dashed"
+
) geom_hline(aes(yintercept = 0),
linetype = "dashed"
+
) geom_errorbar(aes(ymin = ci_low, ymax = ci_up),
alpha = 0.5
+
) guides(linetype = "none") +
geom_point(aes(y = estimate)) +
labs(
x = NULL,
y = "Yield",
shape = "Polluter?",
color = "Polluter?",
title = "Polluters respond stronger to Paris Agreement than green firms"
)
```

Figure 1 shows that throughout most of 2014, the yields of the two groups changed in unison. However, starting at the end of 2014, the yields start to diverge, reaching the highest difference around the signing of the PA. Afterward, the yields for both groups fall again, and the polluters arrive at the same level as at the beginning of 2014. The non-polluters, on the other hand, even experience significantly lower yields than polluters after the signing of the agreement.

Instead of plotting both groups using the simple model approach, we can also use the fixed-effects model and focus on the polluter’s yield response to the signing relative to the non-polluters. To perform this estimation, we need to replace the `treated`

indicator with separate time dummies for the polluters, each marking a one-month period relative to the treatment date. We then regress the monthly yields on the set of time dummies and `cusip_id`

and `month`

fixed effects.

```
<- bonds_panel |>
bonds_panel_alt mutate(
diff_to_treatment = interval(
floor_date(treatment_date, "month"), month
%/% months(1)
)
)
<- bonds_panel_alt |>
variables distinct(diff_to_treatment, month) |>
arrange(month) |>
mutate(variable_name = as.character(NA))
<- "avg_yield ~ "
formula
for (j in 1:nrow(variables)) {
if (variables$diff_to_treatment[j] != 0) {
<- names(bonds_panel_alt)
old_names <- bonds_panel_alt |>
bonds_panel_alt mutate(new_var = diff_to_treatment == variables$diff_to_treatment[j] &
polluter)<- ifelse(variables$diff_to_treatment[j] < 0,
new_var_name str_c("lag", abs(variables$diff_to_treatment[j])),
str_c("lead", variables$diff_to_treatment[j])
)$variable_name[j] <- new_var_name
variablesnames(bonds_panel_alt) <- c(old_names, new_var_name)
<- str_c(
formula
formula,ifelse(j == 1,
new_var_name,str_c("+", new_var_name)
)
)
}
}<- str_c(formula, "| cusip_id + month")
formula
<- feols(
model_with_fe_time fml = as.formula(formula),
vcov = "iid",
data = bonds_panel_alt
)
<- tidy(model_with_fe_time) |>
model_with_fe_time_coefs mutate(
term = str_remove(term, "TRUE"),
ci_up = estimate + qnorm(0.975) * std.error,
ci_low = estimate + qnorm(0.025) * std.error
|>
) left_join(
variables,join_by(term == variable_name)
|>
) bind_rows(tibble(
term = "lag0",
estimate = 0,
ci_up = 0,
ci_low = 0,
month = floor_date(treatment_date, "month")
))
|>
model_with_fe_time_coefs ggplot(aes(x = month, y = estimate)) +
geom_vline(aes(xintercept = floor_date(treatment_date, "month")),
linetype = "dashed"
+
) geom_hline(aes(yintercept = 0),
linetype = "dashed"
+
) geom_errorbar(aes(ymin = ci_low, ymax = ci_up),
alpha = 0.5
+
) geom_point(aes(y = estimate)) +
labs(
x = NULL,
y = "Yield",
title = "Polluters' yield patterns around Paris Agreement signing"
)
```

The resulting graph shown in Figure 2 confirms the main conclusion of the previous image: polluters’ yield patterns show a considerable anticipation effect starting toward the end of 2014. Yields only marginally increase after the signing of the agreement. However, as opposed to the simple model, we do not see a complete reversal back to the pre-agreement level. Yields of polluters stay at a significantly higher level even one year after the signing.

Notice that during the year after the PA was signed, the 45th President of the United States was elected on November 8, 2016. During his campaign there were some indications of intentions to withdraw the US from the PA, which ultimately happened on November 4, 2020. Hence, reversal effects are potentially driven by these actions.

## Exercises

- The 46th President of the US rejoined the Paris Agreement in February 2021. Repeat the difference in differences analysis for the day of his election victory. Note that you will also have to download new TRACE data. How did polluters’ yields react to this action?
- Based on the exercise on ratings in TRACE and FISD, include ratings as a control variable in the analysis above. Do the results change?

## References

*Review of Financial Studies*22 (10): 4219–58. https://doi.org/10.1093/rfs/hhn105.

*Working Paper*. https://dx.doi.org/10.2139/ssrn.3681666.

*Working Paper*. https://dx.doi.org/10.2139/ssrn.4099566.

*Journal of Financial and Quantitative Analysis*56 (6): 1985–2009. https://doi.org/10.1017/S0022109020000757.

*Handbook of the Economics of Finance*, 2:493–572. Elsevier. https://EconPapers.repec.org/RePEc:eee:finchp:2-a-493-572.

*Working Paper*. https://www.nber.org/papers/w29994.