| Title: | Flexible Matching for Stacked Difference-in-Differences |
| Version: | 0.1.0 |
| Description: | Tools for building stacked difference-in-differences samples in staggered-adoption settings. The package labels valid treated and control candidates, performs cross-sectional matching, expands matched units into event-time stacks, and returns diagnostics for balance assessment and visualization. |
| License: | MIT + file LICENSE |
| Depends: | R (≥ 4.1.0) |
| Encoding: | UTF-8 |
| LazyData: | true |
| RoxygenNote: | 7.3.3 |
| Imports: | data.table, ggplot2, MatchIt, scales |
| Suggests: | fixest, knitr, rmarkdown, testthat (≥ 3.0.0) |
| Config/testthat/edition: | 3 |
| URL: | https://github.com/avkazakovgit/flexMatch |
| BugReports: | https://github.com/avkazakovgit/flexMatch/issues |
| VignetteBuilder: | knitr |
| NeedsCompilation: | no |
| Packaged: | 2026-10-11 18:17:41 UTC; avkazakov |
| Author: | Aleksandr Kazakov [aut, cre, cph] |
| Maintainer: | Aleksandr Kazakov <avkazakov@proton.me> |
| Repository: | CRAN |
| Date/Publication: | 2026-10-21 15:40:08 UTC |
Flexible Matching for Stacked Difference-in-Differences
Description
Tools for building stacked difference-in-differences samples in staggered-adoption settings. The package labels valid treated and control candidates, performs cross-sectional matching, expands matched units into event-time stacks, and returns diagnostics for balance assessment and visualization.
Details
Start with flexmatch() and flexmatch_example. The pipeline screens
event windows, matches covariates using 'MatchIt', expands matched sets,
and produces balance and timing diagnostics. See
vignette("introduction", package = "flexMatch") for a worked example.
Matching and timing screens do not establish parallel trends, rule out
anticipation, or identify a causal effect by themselves. Repeated treatments
require a substantive justification for the chosen cooldown period.
Author(s)
Maintainer: Aleksandr Kazakov avkazakov@proton.me [copyright holder]
See Also
Useful links:
Report bugs at https://github.com/avkazakovgit/flexMatch/issues
Assess Matching Quality
Description
Assess Matching Quality
Usage
assess_match_quality(
match_res,
stack_candidates,
dt,
match_vars,
id_col,
time_col
)
Arguments
match_res |
List returned by |
stack_candidates |
Candidate table returned by |
dt |
A |
match_vars |
Character vector of covariates used for matching.
These variables are measured at the period defined by |
id_col |
Character. The name of the unique identifier column. This identifies the panel unit, such as a firm, county, school, or person. |
time_col |
Character. The name of the time identifier column. This can be a year, quarter, date, or any sortable period variable. |
Value
A list with balance_table and optional numeric_balance_panel and categorical_balance_panel ggplot objects. Threshold labels in the table are descriptive heuristics, not statistical tests of exchangeability.
Examples
data("flexmatch_example")
d <- flexmatch_example[flexmatch_example$unit_id <= 100, ]
c <- label_candidates(d, "unit_id", "year", "event", 1, 1, 1)
m <- match_candidates(d, c, "unit_id", "year", "size", method = "nearest")
q <- assess_match_quality(m, c, d, "size", "unit_id", "year")
q$balance_table
print(q$numeric_balance_panel)
Expand Matched Cross-Section to Full Stacks
Description
Expand Matched Cross-Section to Full Stacks
Usage
expand_matched_stacks(
dt,
matched_data,
id_col,
time_col,
k_pre,
k_post,
match_by = -1,
include_all_years = FALSE
)
Arguments
dt |
A |
matched_data |
Matched cross section returned by |
id_col |
Character. The name of the unique identifier column. This identifies the panel unit, such as a firm, county, school, or person. |
time_col |
Character. The name of the time identifier column. This can be a year, quarter, date, or any sortable period variable. |
k_pre |
Integer. Required number of pre-treatment periods. These periods are retained in the standard event window and are also used when checking that treated and control candidates have enough clean history. |
k_post |
Integer. Required number of post-treatment periods. These periods are retained in the standard event window and are also used when checking whether recent or upcoming events contaminate a control window. |
match_by |
Integer. Relative period used for matching covariates. Default |
include_all_years |
Logical. If |
Value
A data.table with one row per matched unit, matched set, and retained period. It includes stack_id, cohort_id, rel_time, treat, post, and weights. The original unit column name is stored as an attribute for clustering.
Examples
data("flexmatch_example")
d <- flexmatch_example[flexmatch_example$unit_id <= 100, ]
c <- label_candidates(d, "unit_id", "year", "event", 1, 1, 1)
m <- match_candidates(d, c, "unit_id", "year", "size", method = "nearest")
s <- expand_matched_stacks(d, m$matched_cross_section, "unit_id", "year", 1, 1)
table(s$rel_time)
Fit an Event-Study Model with fixest
Description
Convenience wrapper around fixest::feols() for the stacked dataset
returned by flexmatch().
Usage
fit_event_study(final_data, outcome, time_col, ref = -1, cluster = NULL, ...)
Arguments
final_data |
Analysis-ready stacked data from |
outcome |
Character scalar naming the outcome column. |
time_col |
Character scalar naming the calendar-time fixed effect column. |
ref |
Reference period for |
cluster |
Character vector of clustering variables. By default, cluster
by the original unit identifier stored by |
... |
Additional arguments passed to |
Details
This is an illustrative regression wrapper. Identification and the interpretation of pooled event coefficients require appropriate parallel-trends, anticipation, treatment-effect, and weighting assumptions. Repeated units are clustered together by default. Pass estimator options through ...; do not supply a second weights argument.
Value
A fixest model with matching weights, unit-within-set fixed effects, matched-set-by-period fixed effects, and clustered standard errors.
Examples
if (requireNamespace("fixest", quietly = TRUE)) {
data("flexmatch_example")
d <- flexmatch_example[flexmatch_example$unit_id <= 100, ]
r <- flexmatch(d, "unit_id", "year", "event", 1, 1, 1, "size",
method = "nearest", assess_quality = FALSE, plot_events = FALSE,
verbose = FALSE)
fit <- fit_event_study(r$final_expanded_data, "outcome", "year")
coef(fit)
}
Flexmatch: Automated Stacked Difference-in-Differences with Flexible Matching Logic
Description
flexmatch orchestrates a complete Stacked Difference-in-Differences (DiD) pipeline.
The package is designed for staggered-adoption panel settings where users want to:
identify valid treated and control observations around treatment events,
match treated and control units on covariates measured at a chosen relative time,
expand the matched sample into event-time stacks for estimation, and
inspect diagnostics before fitting an event-study model.
flexmatch() is the main high-level wrapper. It builds clean treatment stacks,
matches treated units to valid controls, expands the matched sample into event-time
panels, and returns diagnostics for review. The package also ships with
flexmatch_example, a synthetic unbalanced panel dataset that lets users try different
matching and expansion options immediately.
Usage
flexmatch(
dt,
id_col,
time_col,
event_col,
k_pre,
k_post,
k_freeze,
match_vars,
match_exact = NULL,
match_by = -1,
include_all_years = FALSE,
treated_type = "first",
control_type = "all",
assess_quality = TRUE,
plot_events = TRUE,
verbose = TRUE,
...
)
Arguments
dt |
A |
id_col |
Character. The name of the unique identifier column. This identifies the panel unit, such as a firm, county, school, or person. |
time_col |
Character. The name of the time identifier column. This can be a year, quarter, date, or any sortable period variable. |
event_col |
Character. The name of the 0/1 dummy indicating the start of treatment. The variable should equal 1 only in the period when a treatment event begins. |
k_pre |
Integer. Required number of pre-treatment periods. These periods are retained in the standard event window and are also used when checking that treated and control candidates have enough clean history. |
k_post |
Integer. Required number of post-treatment periods. These periods are retained in the standard event window and are also used when checking whether recent or upcoming events contaminate a control window. |
k_freeze |
Integer. Additional cool-down periods used to keep treated units out of the control pool. Increasing this value makes the control pool stricter because recently treated units stay "dirty" for longer after the event window ends. |
match_vars |
Character vector of covariates used for matching.
These variables are measured at the period defined by |
match_exact |
Optional character vector of exact-match covariates. Use this when matched units must agree exactly on selected dimensions such as industry or region. |
match_by |
Integer. Relative period used for matching covariates. Default |
include_all_years |
Logical. If |
treated_type |
Character controlling which treatment events are allowed to define stacks.
|
control_type |
Character controlling which observations can act as controls.
|
assess_quality |
Logical. If |
plot_events |
Logical. If |
verbose |
Logical. If |
... |
Additional arguments passed to |
Value
A list containing the stacked analysis data, matched cross section,
candidate pool, diagnostics, optional plots, and the original matchit object.
Important components are:
-
final_expanded_data: the analysis-ready stacked panel. -
matched_cross_section: matched treated/control observations at the matching period. -
stack_candidates: all observations that passed the temporal eligibility rules. -
diagnostics: counts of candidates, matches, unmatched treated units, and rows dropped for missing covariates. -
quality_stats: balance outputs ifassess_quality = TRUE. -
event_plot: the event-dynamics plot ifplot_events = TRUE.
Examples
data("flexmatch_example")
demo_panel <- flexmatch_example[flexmatch_example$unit_id <= 100, ]
results <- flexmatch(demo_panel, "unit_id", "year", "event",
k_pre = 1, k_post = 1, k_freeze = 1, match_vars = "size",
method = "nearest", assess_quality = TRUE, plot_events = TRUE,
verbose = FALSE)
head(results$final_expanded_data)
results$quality_stats$balance_table
print(results$event_plot)
Example Panel Dataset for flexmatch()
Description
A large unbalanced unit-year panel dataset shipped with the package to demonstrate treatment timing, matching, and stack expansion options.
Usage
flexmatch_example
Format
A data frame with 65000 rows and 8 variables:
- unit_id
Integer unit identifier.
- year
Calendar year.
- industry
Industry category used for exact matching examples.
- region
Geographic region label.
- size
Continuous covariate for matching.
- leverage
Continuous covariate for matching.
- event
Binary indicator equal to 1 in periods when treatment starts.
- outcome
Example outcome variable with treatment-driven dynamics.
Details
The dataset was designed to let users experiment with:
-
treated_type = "first"versus"all", -
control_type = "all","nyt","nt", and"nevert", exact matching with
match_exact,standard event windows versus
include_all_years = TRUE, anddownstream estimation with
fit_event_study().
The dataset has 10,000 units observed between 2013 and 2022, but units enter
and exit the panel at different dates, so histories range from 3 to 10
years. Units are assigned deterministically to treatment patterns including
never treated, once treated, late treated, sparse repeated treatment, and
frequent treatment. This makes the dataset useful for illustrating the practical
differences between treated_type, control_type,
match_exact, and include_all_years.
Examples
data("flexmatch_example")
head(flexmatch_example)
with(flexmatch_example, table(event))
Label Treatment and Control Stack Candidates
Description
Identifies valid stack candidates while respecting treatment timing, clean windows, and exact matching period lookup.
Usage
label_candidates(
dt,
id_col,
time_col,
event_col,
k_pre,
k_post,
k_freeze,
match_by = -1,
treated_type = c("first", "all"),
control_type = c("all", "nyt", "nt", "nevert")
)
Arguments
dt |
A |
id_col |
Character. The name of the unique identifier column. This identifies the panel unit, such as a firm, county, school, or person. |
time_col |
Character. The name of the time identifier column. This can be a year, quarter, date, or any sortable period variable. |
event_col |
Character. The name of the 0/1 dummy indicating the start of treatment. The variable should equal 1 only in the period when a treatment event begins. |
k_pre |
Integer. Required number of pre-treatment periods. These periods are retained in the standard event window and are also used when checking that treated and control candidates have enough clean history. |
k_post |
Integer. Required number of post-treatment periods. These periods are retained in the standard event window and are also used when checking whether recent or upcoming events contaminate a control window. |
k_freeze |
Integer. Additional cool-down periods used to keep treated units out of the control pool. Increasing this value makes the control pool stricter because recently treated units stay "dirty" for longer after the event window ends. |
match_by |
Integer. Relative period used for matching covariates. Default |
treated_type |
Character controlling which treatment events are allowed to define stacks.
|
control_type |
Character controlling which observations can act as controls.
|
Details
Period distances use the ordered periods present anywhere in the panel. Missing calendar periods across the entire input cannot be inferred. Controls must have a clean full window; treated units must have clean pre-event observations and no additional event inside the post-event window. All unit/time pairs must be unique.
Value
A data.table with eligible unit/cohort rows, a treatment flag, and the period at which matching covariates are observed.
Examples
data("flexmatch_example")
d <- flexmatch_example[flexmatch_example$unit_id <= 100, ]
candidates <- label_candidates(d, "unit_id", "year", "event", 1, 1, 1)
table(candidates$treat)
Cross-Sectional Matching for Stacked DiD
Description
Performs exact cohort matching plus flexible 1-to-K matching via MatchIt.
Usage
match_candidates(
dt,
stack_candidates,
id_col,
time_col,
match_vars,
match_exact = NULL,
...
)
Arguments
dt |
A |
stack_candidates |
Candidate table returned by |
id_col |
Character. The name of the unique identifier column. This identifies the panel unit, such as a firm, county, school, or person. |
time_col |
Character. The name of the time identifier column. This can be a year, quarter, date, or any sortable period variable. |
match_vars |
Character vector of covariates used for matching.
These variables are measured at the period defined by |
match_exact |
Optional character vector of exact-match covariates. Use this when matched units must agree exactly on selected dimensions such as industry or region. |
... |
Additional arguments passed to |
Details
Matching is exact on the event cohort period, in addition to any exact covariates supplied. Matching with replacement can reuse a unit in different matched sets. Rows with missing matching covariates are removed. The methods 'cem' and 'subclass' are rejected because their pooled calls ignore the required exact-cohort argument. For method = 'exact', the cohort and exact covariates are included directly in the matching formula.
Value
A list with matched_cross_section, the original MatchIt match_object, and diagnostics for missing matching covariates.
Examples
data("flexmatch_example")
d <- flexmatch_example[flexmatch_example$unit_id <= 100, ]
c <- label_candidates(d, "unit_id", "year", "event", 1, 1, 1)
m <- match_candidates(d, c, "unit_id", "year", "size", method = "nearest")
head(m$matched_cross_section)
Plot Event Dynamics (Lasagna Plot with Overlay)
Description
Plot Event Dynamics (Lasagna Plot with Overlay)
Usage
plot_event_dynamics(
dt,
dt_matched = NULL,
id_col,
time_col,
event_col,
n_bins = 4L,
sort_by_intensity = FALSE,
show_data = NULL
)
Arguments
dt |
A |
dt_matched |
Optional matched cross section used to mark selected units. |
id_col |
Character. The name of the unique identifier column. This identifies the panel unit, such as a firm, county, school, or person. |
time_col |
Character. The name of the time identifier column. This can be a year, quarter, date, or any sortable period variable. |
event_col |
Character. The name of the 0/1 dummy indicating the start of treatment. The variable should equal 1 only in the period when a treatment event begins. |
n_bins |
Positive integer number of treatment-intensity categories. |
sort_by_intensity |
Logical; order units by treatment intensity. |
show_data |
Optional character scalar naming a variable in |
Value
A list containing a ggplot object in plot and the unit order_table.
Examples
data("flexmatch_example")
d <- flexmatch_example[flexmatch_example$unit_id <= 50, ]
p <- plot_event_dynamics(d, id_col = "unit_id", time_col = "year",
event_col = "event", show_data = "outcome")
print(p$plot)