flexMatch: see how matching choices build the final stacks

Aleksandr Kazakov

Turn a panel into inspectable matched event windows. flexMatch screens treatment timing, matches units within the same event period, then expands each match into a stack. The explorer below shows how 18 specifications change the actual retained observations, matched partners and control reuse.

What the package produces

A matched set contains a treated event and its matched controls. A stacked observation is an original unit-period row copied into one of these sets. The same underlying unit can appear in several sets, so copies are not independent observations.

1 · ScreenCheck observed histories, treatment starts, buffers and the matching period. Keep eligible unit × event-period candidates.
2 · MatchUse MatchIt with exact event-period alignment and optional extra exact restrictions. Keep the actual supported matches.
3 · ExpandAttach the selected units’ panel rows. Preserve set IDs, unit-within-set IDs, relative time and matching weights.

This is a sample-construction tool for matched event studies. It does not establish parallel trends, select a causal estimand, or guarantee that previously treated controls are valid. Use the treatment process to choose the design before tuning matching options.

Reproducible example data

flexmatch_example is entirely synthetic: 10,000 units and 65,000 observations over 2013–2022, with unbalanced histories and repeated treatment starts. The explorer uses the first 300 units to keep examples quick. The synthetic outcome contains persistent and repeated treatment effects; the broad-control examples below demonstrate construction mechanics, not successful causal recovery.

data("flexmatch_example")
panel <- as.data.frame(flexmatch_example[flexmatch_example$unit_id <= 300, ])
head(panel[, c("unit_id", "year", "event", "size", "leverage", "industry", "region")])
#>   unit_id year event size leverage      industry region
#> 1       1 2013     0 88.8    0.237 Manufacturing   West
#> 2       1 2014     0 90.9    0.247 Manufacturing   West
#> 3       1 2015     0 93.0    0.257 Manufacturing   West
#> 4       1 2016     0 95.1    0.267 Manufacturing   West
#> 5       1 2017     0 97.2    0.277 Manufacturing   West
#> 6       1 2018     0 99.3    0.287 Manufacturing   West

The event column must mark starts with 1 and other observations with 0. For permanent adoption, convert an absorbing treated-state indicator into starts first. Provide one row per original unit and observed period. Time distances count globally ordered labels in the input; gaps absent for every unit are not reconstructed as elapsed calendar periods.

Explore argument choices and final stacks

The live R widget below uses a fixed reproducible random panel with staggered histories, internal holes and three matching covariates. Change arguments independently: each combination runs flexmatch(), not a saved preset. Covariates, nearest-neighbour matching and the control-pool rule stay fixed.

This live widget runs the flexMatch R package on the website server. Open explorer directly.

Understand the timing screen

The retained window and the exclusion buffer serve different roles. At an event-period candidate c, expansion retains c - k_pre through c + k_post. Controls must pass the dirty-period screen at every retained period, not only at c.

On a regular observed-period index, any control start in the inclusive interval [c − k_pre − k_post − k_freeze, c + 2 × k_post] disqualifies that control candidate. This is more conservative than excluding starts only inside the retained window. It follows from the current screen’s past cooldown and future buffer; these arguments are not estimated anticipation or persistence parameters.

Treated candidates need the complete pre/post window, clean pre-history and no further start in the post-window. treated_type = "first" keeps the first observed start; it does not certify that no treatment happened before the dataset began. A non-positive match_by requires an observed covariate period and can lie outside the retained window. Only the chosen matching-period covariates are required by matching; outcome completeness needs a separate check.

Choose a treatment design first

Setting What it actually selects Interpretation to justify
control_type = "nevert" Units with no observed start anywhere in the supplied panel, also passing window/history checks. Comparability of those controls; treatment before or after observed coverage remains unknown.
control_type = "nyt" Eventually treated units before their first observed start, passing the same buffers. Never-treated units are excluded. No anticipation over the relevant horizon and an appropriate adoption comparison.
control_type = "all" or "nt" The same clean-window candidate pool; previously treated units may return after the buffer. Prior effects must have ceased if these units are to serve as untreated controls.
treated_type = "all" Every observed start passing the screen. Repeated-event interpretation, carryover and dependence across events.
include_all_years = TRUE All observed histories for the selected matches, with outer event-time bins. Extra histories are outside the eligibility screen and can contain other starts.

For permanent adoption, start with treated_type = "first" and substantively suitable "nevert" or "nyt" controls. Increasing k_freeze does not make a permanently treated unit untreated again. For temporary or repeated shocks, a broad pool can be useful if the assumed washout period has a defensible basis.

Balance, support and the retention tradeoff

The figure below uses MatchIt’s standardized mean differences for the two matching covariates. Each specification’s unmatched comparison starts from its own candidate pool; a tight caliper also changes the matched treated subset. The 0.1 line is a descriptive guide, not a causal-validity threshold.

Exact matching on industry and region, a tighter caliper, or a wider window can discard treated events. Report who was excluded, the remaining covariate overlap, achieved ratios and the resulting target population. Nearest matching is order- and specification-dependent; the same number of rows need not mean the same matched sample. Matching quality does not address unobserved confounding by itself.

Use the pipeline in R

result <- flexmatch(panel, "unit_id", "year", "event",
  k_pre = 1, k_post = 1, k_freeze = 0,
  match_vars = c("size", "leverage"), match_by = -1,
  treated_type = "first", control_type = "nevert",
  method = "nearest", ratio = 1, replace = FALSE,
  verbose = FALSE)
result$diagnostics$event_matching
#> $treated_candidates
#> [1] 98
#> 
#> $treated_matched
#> [1] 98
#> 
#> $treated_unmatched
#> [1] 0
#> 
#> $unmatched_events
#> [1] unit_id     cohort_year
#> <0 rows> (or 0-length row.names)
result$diagnostics$control_usage
#> $matched_slots
#> [1] 98
#> 
#> $unique_original_units
#> [1] 62
#> 
#> $original_units_in_multiple_sets
#> [1] 29
result$diagnostics$expansion
#> $matched_sets
#> [1] 98
#> 
#> $units_within_sets
#> [1] 196
#> 
#> $original_units
#> [1] 160
#> 
#> $stacked_rows
#> [1] 588
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 139.2429 139.0336735  0.006595657 1.023072   Excellent   Balanced
#> 2: leverage   0.2755   0.2821224 -0.163767443 1.394050  Acceptable  Distorted
plot_matched_stacks(result$final_expanded_data, max_stacks = 4,
  event_col = "event")$plot

In result$diagnostics, matching preserves original-unit counts, event_matching distinguishes repeated treated events and lists unmatched unit × event-period pairs, control_usage measures reuse, and expansion reports the final sets and rows.

For stage-by-stage access, use label_candidates(), match_candidates() and expand_matched_stacks(). plot_event_dynamics() summarizes timing across original units; plot_matched_stacks() displays the actual final matched sets.

fit_event_study() optionally uses fixest, matching weights, unit-within-set fixed effects and matched-set-by-calendar-period fixed effects. It clusters by the original unit by default. That wrapper still requires a justified comparison group, treatment-effect aggregation and inference design; it does not implement every modern difference-in-differences estimator. Repeated rows across sets are not new independent units, and missing outcomes can change its estimation sample further.

R help and reproducibility

Open help(package = "flexMatch"), ?flexmatch, ?plot_matched_stacks, and ?flexmatch_example in R. This tutorial is also available as vignette("introduction", package = "flexMatch") after installation.

Built with flexMatch 0.1.1, MatchIt 4.8.1, ggplot2 4.0.3 and R Under development (unstable) (2026-10-10 r90659). All scenarios use the same synthetic 300-unit input. Matching defaults and available methods follow the installed MatchIt version.