Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- **`ChaisemartinDHaultfoeuille.by_path` + `placebo=True`** — per-path backward-horizon placebos `DID^{pl}_{path, l}` for `l = 1..L_max`. The same per-path SE convention used for the event-study (joiners/leavers IF precedent: switcher-side contributions zeroed for non-path groups; cohort structure and control pool unchanged; plug-in SE with path-specific divisor `N^{pl}_{l, path}`) is applied to backward horizons via the new `switcher_subset_mask` parameter on `_compute_per_group_if_placebo_horizon`. Surfaced on `results.path_placebo_event_study[path][-l]` (negative-int inner keys mirroring `placebo_event_study`); `summary()` renders the rows alongside per-path event-study horizons; `to_dataframe(level="by_path")` emits negative-horizon rows alongside the existing positive-horizon rows. **Bootstrap** (when `n_bootstrap > 0`) propagates per-`(path, lag)` percentile CI / p-value through the same `_bootstrap_one_target` dispatch as the per-path event-study, with the canonical NaN-on-invalid contract enforced on the new surface (PR #364 library-wide invariant). **SE inherits the cross-path cohort-sharing deviation from R** documented for `path_effects` (full-panel cohort-centered plug-in vs R's per-path re-run): tracks R within tolerance on single-path-cohort panels, diverges materially on cohort-mixed panels — the bootstrap SE is a Monte Carlo analog of the analytical SE and inherits the same deviation. R-parity confirmed at `tests/test_chaisemartin_dhaultfoeuille_parity.py::TestDCDHDynRParityByPathPlacebo` on the new `multi_path_reversible_by_path_placebo` scenario (point estimates exact match; SE within Phase-2 envelope rtol ≤ 5%); positive analytical + bootstrap invariants at `tests/test_chaisemartin_dhaultfoeuille.py::TestByPathPlacebo` (and the gated `::TestBootstrap` subclass). See `docs/methodology/REGISTRY.md` §ChaisemartinDHaultfoeuille `Note (Phase 3 by_path ...)` → "Per-path placebos" for the full contract.

## [3.3.0] - 2026-04-25

### Added
Expand Down
57 changes: 56 additions & 1 deletion benchmarks/R/generate_dcdh_dynr_test_values.R
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ scenarios$joiners_only_controls_trends_lin <- list(
# per-path results live at res$by_level_1, res$by_level_2, ... in rank
# order (1 = most frequent observed path). res$by_levels is a character
# vector of comma-joined path labels (e.g. "0,1,1,1") in the same order.
extract_dcdh_by_path <- function(res, n_effects) {
extract_dcdh_by_path <- function(res, n_effects, n_placebos = 0) {
by_levels <- res$by_levels
out <- list()
for (i in seq_along(by_levels)) {
Expand All @@ -589,6 +589,26 @@ extract_dcdh_by_path <- function(res, n_effects) {
n_obs = as.numeric(effects[h, "N"])
)
}
# Per-path placebos. When did_multiplegt_dyn is called with
# by_path=k AND placebo=N, each by_level_i has its own
# slot$results$Placebos table with N rows. Negative-keyed
# ("-1", "-2", ...) so the Python parity loop can iterate the
# full forward+backward horizon set with int(k) on the keys.
if (n_placebos > 0) {
placebos <- slot$results$Placebos
if (!is.null(placebos)) {
for (h in seq_len(min(n_placebos, nrow(placebos)))) {
horizons[[as.character(-h)]] <- list(
effect = as.numeric(placebos[h, "Estimate"]),
se = as.numeric(placebos[h, "SE"]),
ci_lo = as.numeric(placebos[h, "LB CI"]),
ci_hi = as.numeric(placebos[h, "UB CI"]),
n_switchers = as.numeric(placebos[h, "Switchers"]),
n_obs = as.numeric(placebos[h, "N"])
)
}
}
}
out[[i]] <- list(
path = by_levels[i],
frequency_rank = i,
Expand Down Expand Up @@ -644,6 +664,41 @@ scenarios$multi_path_reversible_by_path <- list(
results = extract_dcdh_by_path(res14, n_effects = 3)
)

# Scenario 15: multi_path_reversible + by_path=3 + placebo=2 (per-path
# backward placebo case). Same deterministic DGP and n_periods=10 as
# scenario 14 (the DGP's `f_g_to_path` is sized for max_switch=6, fixed
# at L_max=3 + n_periods=10). For placebo=2: F_g=2 cohort has backward
# index F_g-1-2=-1 out of range, so those 20 switchers contribute NaN
# at lag=2; F_g in [3..7] (60 switchers) produce a valid lag=2 estimate.
# R drops the F_g=2 cohort from Placebo_2 automatically; the parity
# test compares only over the rows that R produced.
cat(" Scenario 15: multi_path_reversible_by_path_placebo\n")
d15 <- gen_reversible(n_groups = N_GOLDEN, n_periods = 10,
pattern = "multi_path_reversible", seed = 115,
L_max = 3)
res15 <- did_multiplegt_dyn(
df = d15, outcome = "outcome", group = "group", time = "period",
treatment = "treatment", effects = 3, placebo = 2, by_path = 3,
ci_level = 95
)
scenarios$multi_path_reversible_by_path_placebo <- list(
data = export_data(d15),
# n_switcher_groups records the switcher cohort count fed into
# gen_reversible's `counts_per_F_g` allocator (80 = sum c(20, 20, 15,
# 10, 10, 5)); the realized panel has 120 unique groups after the
# default 20 never-treated + 20 always-treated control rows are
# appended (gen_reversible defaults at line 64). Recording both fields
# avoids the metadata-vs-data mismatch the reviewer flagged on
# PR #371 R2: anyone reusing this scenario's metadata sees both the
# switcher count (the load-bearing number for the DGP allocation) and
# the realized panel size.
params = list(pattern = "multi_path_reversible",
n_switcher_groups = N_GOLDEN, n_realized_groups = N_GOLDEN + 40L,
n_periods = 10, seed = 115, effects = 3, placebo = 2,
by_path = 3, ci_level = 95),
results = extract_dcdh_by_path(res15, n_effects = 3, n_placebos = 2)
)

# ---------------------------------------------------------------------------
# Write output
# ---------------------------------------------------------------------------
Expand Down
153 changes: 153 additions & 0 deletions benchmarks/data/dcdh_dynr_golden_values.json

Large diffs are not rendered by default.

Loading
Loading