flexMatch helps construct matched event windows when
units experience treatment at different times. It makes sample
eligibility and matching visible before estimation. A clean window and
good covariate balance do not establish causal identification: parallel
trends, anticipation, repeated treatment and the target estimand still
need substantive justification.
The bundled data are entirely synthetic. The complete panel contains 10,000 units and 65,000 observations, including missing histories and repeated events. We use 100 units so every example is quick.
data("flexmatch_example")
panel <- flexmatch_example[flexmatch_example$unit_id <= 100, ]
head(panel)
#> unit_id year industry region size leverage event outcome
#> <int> <int> <char> <char> <num> <num> <int> <num>
#> 1: 1 2013 Manufacturing West 88.8 0.237 0 33.28
#> 2: 1 2014 Manufacturing West 90.9 0.247 0 34.38
#> 3: 1 2015 Manufacturing West 93.0 0.257 0 35.48
#> 4: 1 2016 Manufacturing West 95.1 0.267 0 36.58
#> 5: 1 2017 Manufacturing West 97.2 0.277 0 37.68
#> 6: 1 2018 Manufacturing West 99.3 0.287 0 38.78result <- flexmatch(panel, "unit_id", "year", "event",
k_pre = 1, k_post = 1, k_freeze = 1,
match_vars = c("size", "leverage"), method = "nearest",
assess_quality = TRUE, plot_events = TRUE, verbose = FALSE)
result$diagnostics
#> $candidate_counts
#> $candidate_counts$treated
#> [1] 33
#>
#> $candidate_counts$control
#> [1] 227
#>
#>
#> $matching
#> $matching$treated_candidates
#> [1] 33
#>
#> $matching$treated_matched
#> [1] 33
#>
#> $matching$treated_unmatched
#> [1] 0
#>
#> $matching$unmatched_treated_ids
#> integer(0)
#>
#> $matching$dropped_rows_missing_covariates
#> [1] 0
head(result$final_expanded_data)
#> stack_id cohort_id unit_id year rel_time treat post weights cohort_year
#> <char> <char> <int> <int> <int> <num> <int> <num> <int>
#> 1: 2_2017_1 2017_1 2 2016 -1 1 0 1 2017
#> 2: 2_2017_1 2017_1 2 2017 0 1 1 1 2017
#> 3: 2_2017_1 2017_1 2 2018 1 1 1 1 2017
#> 4: 100_2017_1 2017_1 100 2016 -1 0 0 1 2017
#> 5: 100_2017_1 2017_1 100 2017 0 0 1 1 2017
#> 6: 100_2017_1 2017_1 100 2018 1 0 1 1 2017
#> industry region size leverage event outcome
#> <char> <char> <num> <num> <int> <num>
#> 1: Retail North 86.2 0.235 0 34.34
#> 2: Retail North 88.3 0.245 1 37.43
#> 3: Retail North 90.4 0.255 0 38.93
#> 4: Energy Central 111.2 0.254 0 37.56
#> 5: Energy Central 113.3 0.264 0 38.65
#> 6: Energy Central 115.4 0.274 0 39.75The treatment indicator marks event starts. k_pre and
k_post define the window; k_freeze extends the
cooldown after an event. Matching uses covariates at
match_by = -1 by default, with exact matching on the event
period. Period distances count the ordered periods present in the input.
A completely missing calendar period must be handled in the input before
using these distances.
print(result$event_plot)
#> Warning: Raster pixels are placed at uneven horizontal intervals and will be shifted
#> ℹ Consider using `geom_tile()` instead.
#> Raster pixels are placed at uneven horizontal intervals and will be shifted
#> ℹ Consider using `geom_tile()` instead.result$quality_stats$balance_table
#> Variable Mean_T Mean_C SMD VR Quality_SMD Quality_VR
#> <char> <num> <num> <num> <num> <char> <char>
#> 1: size 138.330303 144.6757576 -0.1925478 1.257435 Acceptable Distorted
#> 2: leverage 0.277697 0.2846061 -0.1603314 1.270539 Acceptable Distorted
print(result$quality_stats$numeric_balance_panel)Balance thresholds are descriptive aids. They do not prove parallel trends or eliminate unobserved confounding.
Use control_type = "nevert" for never-treated controls
or "nyt" for eventual adopters before their first event.
"all" and "nt" currently select the same
clean-window pool. treated_type = "all" allows repeated
events that satisfy the window rules. This assumes earlier events have
no remaining relevant effect after the chosen cooldown; that assumption
should be justified.
include_all_years = TRUE retains observed histories and
bins periods outside the window. Those extra observations are not
guaranteed to satisfy the clean-window rules and should not be treated
as automatically uncontaminated controls for estimation.
if (requireNamespace("fixest", quietly = TRUE)) {
fit <- fit_event_study(result$final_expanded_data, "outcome", "year")
coef(fit)
}
#> fixest::rel_time::0:treat fixest::rel_time::1:treat
#> 2.000000 2.400909The wrapper uses matching weights, a unit-within-set fixed effect, a matched-set-by-calendar-period fixed effect, and clustering by the original unit. Pooled stacked estimates still require an appropriate weighting and treatment-effect interpretation; this wrapper does not implement every modern difference-in-differences estimator.
Use help(package = "flexMatch"),
?flexmatch, and ?flexmatch_example for the
full R help index. The lower-level functions expose each stage for
inspection and customization.