3  Fect Plot Options

In this chapter, we explore various visualization options avialable in the fect package using data from Grumbach and Sahn (2020). This chapter is authored by Jinwen Wu and Yiqing Xu.


plot.fect is an S3 method that offers various options for customizing data and results visualization. Below is a brief summary of the most commonly used options.

While these customization options are demonstrated using the default gap plot, they can be applied universally, with only a few exceptions.

3.1 Load Data

As explained in the previous chapter, Grumbach and Sahn (2020) examines the mobilizing effect of minority candidates on coethnic support in U.S. congressional elections. The treatment variable indicates the presence of an Asian candidate, and the outcome variable represents the proportion of general election contributions from Asian donors.

First, we load the required packages. The dataset, gs2020, is included with the fect package and can be loaded using data(fect). We also specify the treatment, outcome, controls (covariates), unit and time indices, and level of clustering.

# load libraries and data
library(ggplot2)
library(panelView)
library(fect)
data(fect)

# Define variables
y <- "general_sharetotal_A_all"  # Outcome: Share of Asian contributions
d <- "cand_A_all"                # Treatment: Presence of an Asian candidate
controls <- c("cand_H_all", "cand_B_all") # Controls: Presence of Hispanic and Blank candidates
unit <- "district_final"          # Unit variable: District
time <- "cycle"                   # Time variable: Election cycle
clust <- "district_final"         # Clustering variable: District

3.2 Gap Plot

To create the gap plot, also known as the event study plot, we first apply fect, the fixed effects counterfactual estimator. For details, see Chapter 2.

out <- fect(Y = y, D = d, X = controls, index = c(unit, time), 
               data = gs2020, method = "fe", force = "two-way", 
               se = TRUE, parallel = TRUE, nboots = 100)
#> Some units are totally removed after drop missing values of the outcome or covariates.
#> Parallel computing ...
#> Bootstrapping for uncertainties ...
#> 100 runs

After running the model, we can plot the dynamic treatment effects over (relative) time, including confidence intervals if se = TRUE is specified in the estimation. Note that type = "gap" is the default option, so we omit it here.

plot(out)

3.2.1 Starting Period

By default, the first post-treatment period is set to 0, and the last pre-treatment period is set to -1. However, some researchers prefer to designate the former as 1 and the latter as 0. To achieve this, set start0 = TRUE.

plot(out, start0 = TRUE,  # Shift time so treatment begins at 0
     main = "Custom Starting Period")

3.2.2 Axis and Legend

Below, we customize the x- and y-axis ranges, labels, tick breaks, and legend. The x-axis labels are rotated for clarity. Moreover, by setting xlim = c(-10, 1), the x-axis is restricted to time periods -8 to 1, with the treatment shifted to begin at period 0 instead of period 1.

plot(out, 
     xlim = c(-10, 1),  # only show time periods -8 to 1
     ylim = c(-0.15, 0.30),  # set y-range
     xlab = "Custom Time Axis", # x-axis label
     ylab = "Estimated ATT", # y-axis label
     xangle = 90, # rotate x-axis labels by 90°
     xbreaks = seq(-10, 1, by = 2), 
     # Label x-axis from -12 to 1 with a break of 2
     main = "Axis and Legend Customization")
#> Scale for x is already present.
#> Adding another scale for x, which will replace the existing scale.

3.2.3 Confidence Intervals

Below, we plot the treatment effect with 90% confidence intervals instead of 95%.

plot(out, plot.ci = "0.9", 
     main = "90% confidence intervals")

3.2.4 Text and Theme

This plot adjusts text sizes with a series of cex options.

plot(out, 
     ylim = c(-0.15, 0.3), # set yrange
     theme.bw = FALSE,      # Change the color theme
     cex.main = 1.25,     # Scale for the main title
     cex.axis = 1.2,      # Axis tick label size
     cex.lab = 1.2,       # Axis label size
     cex.legend = 1,    # Legend text size
     cex.text = 1.2,        # Annotation text size
     main = "Text and Theme Customization")

3.2.5 Line Customization

Here, we demonstrate how to change main and the horizontal reference lines.

plot(out, 
     line.width = c(1.5, 0.5),  
     lcolor = "skyblue",      # Color for horizontal zero line
     lwidth = 2,                # Width of the horizontal line
     main = "Line Customization")

3.3 Pretrend Tests

We can conduct several tests to shed light on (not directly test) the parallel trends (PT) assumption, including the equivalence test and the placebo test. For details, see Chapter 2 or Liu, Wang, and Xu (2024).

3.3.1 Equivalence Test

In the equivalence plot (type = "equiv"), the equivalence bound is defined by the two-one-sided test (TOST) threshold. For example, in the plot below, the bound is set by tost.threshold = 0.1, with lines at -0.1 and 0.1. This threshold should be set based on the magnitude of the ATT or the standard deviation of the outcome (or residualized outcome).

The bound option has four choices: "none", "min", "equiv", or "both". When set to "none", no bound is displayed.

plot(out, type = "equiv", bound = "equiv", tost.threshold = 0.1, 
     ylim = c(-0.15, 0.15))

The "min" displays the minimum range bound based on the maximum absolute pre‐treatment residual (e.g., if the largest pre-treatment estimate is 0.3, lines at -0.3 and 0.3).

plot(out, type = "equiv", bound = "min", ylim = c(-0.15, 0.15))

We can plot both the minimum range and the equivalence bound with bound = "both", which is also the default option.

plot(out, type = "equiv", tost.threshold = 0.1, ylim = c(-0.15, 0.15))

We use the stats argument to select which results to display, label them with stats.labs, and position the legend with stats.pos. Setting show.stats = FALSE hides the test results entirely.

plot(out, type = "equiv",
     ylim = c(-0.25, 0.25),
     stats = c("F.p", "equiv.p"),
     stats.labs = c("F Test P-value", "Equivalence P-value"),
     stats.pos = c(-8, 0.2),   # (x, y) position for the stats text
     show.stats = TRUE,     # Can be switched off to hide all test stats
     main = "Statistical Test Annotations")

3.3.2 Placebo Test

A placebo test evaluates whether the “fake” ATT is statistically distinguishable in a placebo period. It artificially assigns treatment during placebo periods and estimates the “placebo effect” in those periods.

Therefore, the model must be re-run. Below, we set placebo.period = c(-2, 0), specifying the pre-treatment periods used for the placebo test.

out_fe_placebo <- fect(Y = y, D = d, X = controls, data = gs2020, 
                       index = c(unit, time), force = "two-way",
                       method = "fe", CV = FALSE, parallel = TRUE,
                       se = TRUE, nboots = 1000, placeboTest = TRUE,
                       placebo.period = c(-2, 0))
#> Some units are totally removed after drop missing values of the outcome or covariates.
#> Parallel computing ...
#> Bootstrapping for uncertainties ...
#> 1000 runs

plot(out_fe_placebo)

The plot shows the estimated “effects” in the pre-treatment periods (placebo effects). The blue lines in the pre-treatment period suggest that we do not observe significant effects of the treatment in the pre-periods.

3.4 Carryover Effects

One type of plot rarely seen in the empirical literature is how the difference between treatment and control groups evolves after treatment ends. We call it the "exit" plot, where the x-axis represents time relative to treatment exit. In contrast, the "gap" plot focuses on treatment entry. The "exit" plot is essential for assessing potential carryover effects.

plot(out, type = "exit")

The test for carryover effects examines whether the treatment effect persists after treatment ends. It artificially labels several post-treatment periods as treated and estimates the “placebo effect” in those periods. By setting carryover.period = c(1, 3), we specify a placebo period that includes three post-treatment periods. If the treatment effect is purely contemporaneous (i.e., there are no carryover effects), the test will not reject the null hypothesis. In this application, the average carryover effect is close to zero and statistically indistinguishable from zero.

out_fe_carryover <- fect(Y = y, D = d, X = controls, data = gs2020,
                         index = c(unit, time), force = "two-way",
                         parallel = TRUE, se = TRUE, CV = FALSE,
                         nboots = 1000, carryoverTest = TRUE,
                         carryover.period = c(1, 3))
#> Some units are totally removed after drop missing values of the outcome or covariates.
#> Parallel computing ...
#> Bootstrapping for uncertainties ...
#> 1000 runs
plot(out_fe_carryover)

3.5 Effect Heterogenity

We provide two ways to visualize treatment effect heterogeneity: a "box" plot, which shows the distribution of individual treatment effects, and a "calendar" plot, which depicts the ATT conditional on calendar time. We plan to expand this functionality to allow for more pre-treatment covariates soon.

In the box plot, the box in each period represents the range of the middle 50% of the individual effects, while the whiskers show the 2.5%–95% quantiles and the horizontal line represents the median.

plot(out, type = "box", xlim = c(-12, 5))

In the calendar plot, the blue ribbon represents a loess fit of the conditional ATT, with 95% confidence intervals.

plot(out, type = "calendar")