| Title: | BACI / Paired-DiD / Synthetic-Control Estimators for Carbon-MRV Audits |
|---|---|
| Description: | A reproducibility-by-construction toolkit for Before-After-Control-Impact (BACI) audits of carbon-MRV registers. Provides a staggered-treatment simulator with a known oracle ATT, a paired difference-in-differences estimator with proponent-cluster-robust standard errors, a time-varying synthetic-control comparator with simplex-constrained donor weights (Wang and Carreira-Perpinan, 2013), a proponent-level cluster bootstrap, a Goodman-Bacon (2021) staggered-DiD decomposition wrapper, and 'targets'-skeleton helpers for end-to-end pipeline reproducibility. Designed against the Macintosh et al. (2024) Australian Human-Induced Regeneration setting but applicable to any paired BACI register. |
| Authors: | Max Moldovan [aut, cre] (ORCID: <https://orcid.org/0000-0001-9680-8474>) |
| Maintainer: | Max Moldovan <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-10-16 07:26:44 UTC |
| Source: | https://github.com/max578/bacipair |
Estimates the average treatment effect on the treated (ATT) on a paired BACI panel via a two-way fixed-effects regression of the outcome on (a) a treated-arm-by-post-treatment interaction, plus (b) pair fixed effects via within-pair-and-arm demeaning, and (c) year fixed effects.
baci_paired_did( panel, outcome = "woody_cover", treated_label = "credited", comparison_label = "comparison", cluster_col = "proponent_id" )baci_paired_did( panel, outcome = "woody_cover", treated_label = "credited", comparison_label = "comparison", cluster_col = "proponent_id" )
panel |
A |
outcome |
Character. Outcome column name (default |
treated_label, comparison_label
|
Character scalars. Levels of
the |
cluster_col |
Character. Cluster identifier (default
|
Standard errors are cluster-robust at the proponent level (default) or
at the pair level if cluster_col = "pair_id". The estimator is the
canonical CR1 sandwich form used in econometric panel work
(Cameron & Miller, 2015), and is computed without external
dependencies.
Operationally, this is the naive paired-DiD comparator the translation memo (RDT-8) uses as the foil for the synthetic-control estimator: when elevation / rainfall climatology covary with treatment timing, paired-DiD has residual bias that the SC estimator nets out.
List with components:
att_hatNumeric. Estimated ATT (paired-DiD coefficient).
se_clusterNumeric. Cluster-robust (CR1) standard error.
t_statNumeric. Robust t-statistic.
ci_lo,ci_hi
Numeric. 95% normal-approximation CI.
n_obs,n_clusters,n_pairs,n_years
Integers. Sample counts.
coefficientsNumeric named vector of the estimated
coefficients on the demeaned design: the ATT interaction
(did_i_dm) plus one coefficient per non-base year dummy
(yr_<year>_dm). The pair-and-arm intercept and the
treated-arm and post-treatment main effects are absorbed by the
within-pair-and-arm demeaning at fit time and are not part of
the returned vector.
callCaptured match.call().
sim <- simulate_staggered_panel(seed = 20260520L, n_pairs = 30L, n_proponents = 8L) did <- baci_paired_did(sim$panel) did$att_hat did$se_clustersim <- simulate_staggered_panel(seed = 20260520L, n_pairs = 30L, n_proponents = 8L) did <- baci_paired_did(sim$panel) did$att_hat did$se_cluster
targets-skeleton plan for a bacipair auditReturns a list of targets::tar_target calls covering the canonical
pipeline:
bacipair_targets_plan( simulate_args = list(seed = 20260520L, n_pairs = 200L), synth_args = list(), did_args = list(), boot_args = list(n_boot = 200L, seed = 20260521L), log_path = "logs/oracle-recovery.json" )bacipair_targets_plan( simulate_args = list(seed = 20260520L, n_pairs = 200L), synth_args = list(), did_args = list(), boot_args = list(n_boot = 200L, seed = 20260521L), log_path = "logs/oracle-recovery.json" )
simulate_args |
A named list passed to
|
synth_args |
A named list passed to |
did_args |
A named list passed to |
boot_args |
A named list passed to
|
log_path |
Character. Path for the JSON oracle-recovery log
(default |
simulate the staggered-treatment panel (or load a real BACI panel),
fit the synthetic-control estimator,
fit the paired-DiD estimator,
proponent-cluster bootstrap of the SC point estimate,
Goodman-Bacon decomposition diagnostic,
write a JSON oracle-recovery log.
Skeleton means: this is a list ready to drop into targets::tar_plan()
(or targets::tar_pipeline()). The function does not execute the
pipeline. To run it, paste the returned list into a _targets.R
script and call targets::tar_make().
The targets package is in Suggests, so this function raises an
informative error if targets is not installed.
A named list of targets::tar_target() calls, one per
pipeline node. Names are panel, synth_control_fit,
paired_did_fit, cluster_boot, goodman_bacon,
oracle_recovery_log.
if (requireNamespace("targets", quietly = TRUE)) { plan <- bacipair_targets_plan() names(plan) }if (requireNamespace("targets", quietly = TRUE)) { plan <- bacipair_targets_plan() names(plan) }
Decomposes a two-way fixed-effects DiD estimator into a weighted
average of all 2x2 DiD estimators implied by the treatment timing,
as in Goodman-Bacon (2021). Wraps bacondecomp::bacon(); that
package is declared in Suggests, so the wrapper raises an
informative error if it is not installed.
goodman_bacon_decomp( panel, outcome = "woody_cover", id = "pair_id", time = "year", treated_label = "credited", treatment_indicator = "post_treatment" )goodman_bacon_decomp( panel, outcome = "woody_cover", id = "pair_id", time = "year", treated_label = "credited", treatment_indicator = "post_treatment" )
panel |
A |
outcome |
Character. Outcome column name (default
|
id |
Character. Unit identifier column (default |
time |
Character. Time column (default |
treated_label |
Character. Level of |
treatment_indicator |
Character. Name of the binary treatment
indicator column (default |
This decomposition is the diagnostic the translation memo (RDT-8)
relies on to flag "forbidden comparisons" - 2x2 cells in which an
already-treated cohort acts as the comparator for a not-yet-treated
cohort and so contaminates the TWFE coefficient with negative
weights. A clean decomposition (large weight on
Treated vs Untreated; small / non-negative weights on
Earlier vs Later Treated) is a precondition for trusting the
single-coefficient TWFE.
Operates on the long-format treated-arm panel: only rows whose
arm column matches treated_label are passed to
bacondecomp::bacon(), so each observation is a (pair_id, year)
pair with a single 0/1 treatment indicator.
A list with components:
decompositionA data.frame returned by
bacondecomp::bacon(); one row per 2x2 cell, with treated,
untreated, estimate, weight, and type.
twfe_estimateNumeric. The implied weighted-average TWFE estimate.
weight_summaryA data.frame summarising weights and
weighted estimates by type.
if (requireNamespace("bacondecomp", quietly = TRUE)) { sim <- simulate_staggered_panel(seed = 20260520L, n_pairs = 30L, n_proponents = 8L) gb <- goodman_bacon_decomp(sim$panel) gb$weight_summary }if (requireNamespace("bacondecomp", quietly = TRUE)) { sim <- simulate_staggered_panel(seed = 20260520L, n_pairs = 30L, n_proponents = 8L) gb <- goodman_bacon_decomp(sim$panel) gb$weight_summary }
x a bacipair abstention?Producer-side predicate; complements the orchestra-wide,
producer-agnostic is_orchestra_decline() in
ORCHESTRA_dev/integration/refusal_contract.R, which recognises this
class (and every other member's) by the _refusal/_abstention naming
convention without depending on bacipair's namespace.
is_bacipair_abstention(x)is_bacipair_abstention(x)
x |
Any object, typically a caught condition. |
A length-1 logical.
sim <- simulate_staggered_panel(seed = 20260520L, n_pairs = 4L, n_proponents = 2L) cond <- tryCatch( synth_control_baci(sim$panel), bacipair_abstention = function(e) e ) is_bacipair_abstention(cond)sim <- simulate_staggered_panel(seed = 20260520L, n_pairs = 4L, n_proponents = 2L) cond <- tryCatch( synth_control_baci(sim$panel), bacipair_abstention = function(e) e ) is_bacipair_abstention(cond)
Resamples whole proponent clusters (with replacement) from a BACI panel and refits a chosen estimator on each draw. Returns the bootstrap distribution of the panel-average ATT, along with a 95% percentile interval and the bootstrap standard error.
proponent_cluster_bootstrap( panel, estimator = c("synth_control", "paired_did"), n_boot = 200L, seed = 20260521L, cluster_col = "proponent_id", ... )proponent_cluster_bootstrap( panel, estimator = c("synth_control", "paired_did"), n_boot = 200L, seed = 20260521L, cluster_col = "proponent_id", ... )
panel |
A |
estimator |
Character, one of |
n_boot |
Integer (>= 50). Number of bootstrap draws (default 200). |
seed |
Integer. RNG seed (default |
cluster_col |
Character. Cluster column name (default
|
... |
Extra arguments forwarded to the chosen estimator. For
|
Cluster-level resampling is the right inferential unit when one proponent runs many adjacent projects (Macintosh et al., 2024) - resampling pairs would understate dependence.
List with components:
drawsNumeric vector of length n_boot of point
estimates per draw.
seNumeric. Bootstrap standard deviation across draws.
ci_lo,ci_hi
Numeric. 2.5th / 97.5th percentile.
n_boot,estimator,seed
Bookkeeping.
sim <- simulate_staggered_panel(seed = 20260520L, n_pairs = 30L, n_proponents = 8L) boot <- proponent_cluster_bootstrap(sim$panel, estimator = "paired_did", n_boot = 50L) boot$ci_lo boot$ci_hisim <- simulate_staggered_panel(seed = 20260520L, n_pairs = 30L, n_proponents = 8L) boot <- proponent_cluster_bootstrap(sim$panel, estimator = "paired_did", n_boot = 50L) boot$ci_lo boot$ci_hi
Generates a synthetic Before-After-Control-Impact (BACI) panel with
staggered treatment timing, two arms ("credited" and "comparison"),
per-pair static covariates (baseline cover, elevation, rainfall
climatology), per-year shared rainfall anomaly, and proponent
assignments drawn from a Zipf distribution. The data-generating process
is structured so that the panel-average ATT is recoverable from the
comparison arm; paired-DiD with the matched 1:1 comparator carries
residual bias when elevation / rainfall climatology covary with
treatment timing; and the donor-pool synthetic-control estimator should
recover the oracle within the kill-gate tolerance documented in
synth_control_baci().
simulate_staggered_panel( seed = 20260520L, n_pairs = 200L, t_min = 2010L, t_max = 2025L, treatment_year_min = 2013L, treatment_year_max = 2022L, additionality_share = 0.18, additionality_mean_pp = 4.5, additionality_sd_pp = 1.5, additionality_dynamics = c(0.2, 0.55, 0.85, 1, 1, 1, 1, 1, 1, 1), rainfall_sd_pp = 1.4, site_baseline_mean_pp = 22, site_baseline_sd_pp = 4, site_pair_sd_pp = 1, noise_sd_pp = 0.8, secular_trend_pp_per_year = 0.4, elevation_mean_m = 350, elevation_sd_m = 200, elevation_trend_coupling = 0.0015, rainfall_climatology_mean_pp = 0, rainfall_climatology_sd_pp = 0.6, elevation_treatment_year_coupling = -2.5, n_proponents = 50L, proponent_zipf_s = 1.2 )simulate_staggered_panel( seed = 20260520L, n_pairs = 200L, t_min = 2010L, t_max = 2025L, treatment_year_min = 2013L, treatment_year_max = 2022L, additionality_share = 0.18, additionality_mean_pp = 4.5, additionality_sd_pp = 1.5, additionality_dynamics = c(0.2, 0.55, 0.85, 1, 1, 1, 1, 1, 1, 1), rainfall_sd_pp = 1.4, site_baseline_mean_pp = 22, site_baseline_sd_pp = 4, site_pair_sd_pp = 1, noise_sd_pp = 0.8, secular_trend_pp_per_year = 0.4, elevation_mean_m = 350, elevation_sd_m = 200, elevation_trend_coupling = 0.0015, rainfall_climatology_mean_pp = 0, rainfall_climatology_sd_pp = 0.6, elevation_treatment_year_coupling = -2.5, n_proponents = 50L, proponent_zipf_s = 1.2 )
seed |
Integer scalar. The simulation RNG seed (default
|
n_pairs |
Integer scalar (>= 2). Number of treated/comparison pairs (default 200). |
t_min, t_max
|
Integer scalars. Inclusive panel year range (defaults 2010 / 2025). |
treatment_year_min, treatment_year_max
|
Integer scalars. Range
of staggered treatment years (defaults 2013 / 2022). Must lie inside
|
additionality_share |
Numeric in |
additionality_mean_pp, additionality_sd_pp
|
Numeric. Mean / SD of
the (untruncated) Normal asymptotic-additionality distribution among
pairs with |
additionality_dynamics |
Numeric vector of non-negative weights
indexed by years-since-treatment (1, 2, ...). The realised
individual treatment effect in year |
rainfall_sd_pp, noise_sd_pp
|
Numeric. Rainfall-anomaly and idiosyncratic noise SDs in percentage points of woody cover. |
site_baseline_mean_pp, site_baseline_sd_pp
|
Numeric. Mean and SD of the site-baseline draw in percentage points of woody cover. |
site_pair_sd_pp |
Numeric. Within-pair shift SD in percentage points of woody cover. |
secular_trend_pp_per_year |
Numeric. Common secular trend in percentage points of woody cover per year (default 0.4). |
elevation_mean_m, elevation_sd_m
|
Numeric. Per-pair elevation distribution (defaults 350 / 200 m). |
elevation_trend_coupling |
Numeric. Coupling between elevation
and the secular trend (default |
rainfall_climatology_mean_pp, rainfall_climatology_sd_pp
|
Numeric. Per-pair rainfall-climatology offset distribution. |
elevation_treatment_year_coupling |
Numeric. Strength of the selection-on-observables coupling between elevation and treatment year (default -2.5; higher elevation means earlier registration). |
n_proponents |
Integer (1 <= n_proponents <= n_pairs). Number of distinct proponents to assign (default 50). |
proponent_zipf_s |
Numeric > 0. Zipf shape parameter for proponent cluster sizes (default 1.2). |
All values, distributions, and seeds are explicit. There is no hidden
RNG. Inputs are validated by stopifnot() guards.
A list with elements:
panelA data.table with columns including pair_id,
proponent_id, arm, year, treatment_year,
post_treatment, treated_arm, post_idx, baseline_cover,
elevation_m, rainfall_climatology, rainfall_sensitivity,
site_pair_shift, rainfall_anomaly,
true_additionality_max, has_real_additionality, ite,
noise, woody_cover.
pair_tableA data.table of per-pair covariates.
rainfallA data.table of per-year shared rainfall
anomaly.
oracle_panel_attNumeric scalar. The oracle panel-average ATT (mean ITE over treated post-treatment observations).
oracle_unit_attA data.table of per-unit oracle ATT
(pair_id, unit_att).
parametersNamed list of every parameter used to generate
the panel, plus generated_at ISO-8601 timestamp.
sim <- simulate_staggered_panel(seed = 20260520L, n_pairs = 50L) sim$oracle_panel_att head(sim$panel)sim <- simulate_staggered_panel(seed = 20260520L, n_pairs = 50L) sim$oracle_panel_att head(sim$panel)
For each treated unit, constructs a per-unit synthetic-control comparator by minimising the squared distance between the treated pre-treatment trajectory + static predictors and a simplex-constrained weighted average of every other pair's comparison-arm trajectory + predictors. Returns per-(pair, year) ATT estimates and the panel-average ATT.
synth_control_baci( panel, predictor_cols = c("baseline_cover", "elevation_m", "rainfall_climatology"), predictor_weight = 20, min_pre_years = 3L )synth_control_baci( panel, predictor_cols = c("baseline_cover", "elevation_m", "rainfall_climatology"), predictor_weight = 20, min_pre_years = 3L )
panel |
A |
predictor_cols |
Character vector of predictor column names
(default |
predictor_weight |
Numeric >= 0. Relative weight on the predictor block (default 20). |
min_pre_years |
Integer >= 2. Minimum pre-treatment years required per treated unit (default 3). |
Pure base R; deterministic; no extra dependencies. The treated unit's own matched 1:1 comparator is excluded from its donor pool to avoid leakage; the pool is every other pair's comparison arm.
List with components:
fitsList of per-unit fits.
per_unit_attdata.table of per-unit estimated ATT,
plus the oracle ATT (oracle_unit) when the input panel
carried an ite column.
panel_att_hatNumeric. Panel-average estimated ATT.
pre_fit_rmseNumeric. Root-mean-square pre-fit residual across treated units.
sim <- simulate_staggered_panel(seed = 20260520L, n_pairs = 30L, n_proponents = 8L) fit <- synth_control_baci(sim$panel) fit$panel_att_hatsim <- simulate_staggered_panel(seed = 20260520L, n_pairs = 30L, n_proponents = 8L) fit <- synth_control_baci(sim$panel) fit$panel_att_hat
Captures the comparison between the simulated oracle ATT and the
point estimates from the synthetic-control and paired-DiD estimators
(plus the cluster-bootstrap CI on the SC estimator), in the schema
used by deliverables/RDT-20/logs/oracle-recovery.json.
write_oracle_recovery_log( panel, synth_control_fit, paired_did_fit = NULL, cluster_boot = NULL, path )write_oracle_recovery_log( panel, synth_control_fit, paired_did_fit = NULL, cluster_boot = NULL, path )
panel |
A simulator return list (with |
synth_control_fit |
A return list from |
paired_did_fit |
A return list from |
cluster_boot |
A return list from
|
path |
Character. Output JSON path. |
Requires jsonlite; declared in Suggests.
Invisibly returns path. Side effect: writes the log.
if (requireNamespace("jsonlite", quietly = TRUE)) { sim <- simulate_staggered_panel(seed = 20260520L, n_pairs = 30L, n_proponents = 8L) sc <- synth_control_baci(sim$panel) did <- baci_paired_did(sim$panel) log_path <- tempfile(fileext = ".json") write_oracle_recovery_log(sim, sc, did, path = log_path) jsonlite::fromJSON(log_path)$sc_minus_oracle }if (requireNamespace("jsonlite", quietly = TRUE)) { sim <- simulate_staggered_panel(seed = 20260520L, n_pairs = 30L, n_proponents = 8L) sc <- synth_control_baci(sim$panel) did <- baci_paired_did(sim$panel) log_path <- tempfile(fileext = ".json") write_oracle_recovery_log(sim, sc, did, path = log_path) jsonlite::fromJSON(log_path)$sc_minus_oracle }