Skip to content

Clustered & analytical cluster-robust inference (multi-period) + BMisc deprecation fix - #35

Merged
marcelortizv merged 9 commits into
mainfrom
feature-clustered-analytic-se
Jun 13, 2026
Merged

Clustered & analytical cluster-robust inference (multi-period) + BMisc deprecation fix#35
marcelortizv merged 9 commits into
mainfrom
feature-clustered-analytic-se

Conversation

@marcelortizv

Copy link
Copy Markdown
Owner

Summary

Ports three clustered / cluster-robust inference features from bcallaway11/did (PRs #261/#262) into the multi-period path, plus a BMisc deprecation cleanup. Bumps the dev version to 0.2.3.

Point estimates are unchanged everywhere, and with cluster = NULL standard errors are byte-identical to before (the new branches are gated on a non-null, row-aligned cluster vector).

What's included

Feature 1 — analytic cluster-robust SEs without the bootstrap (multi-period).
ddd(..., cluster = <var>, boot = FALSE) now returns analytical cluster-robust SEs (cluster-sum CRVE on the influence function) in att_gt.R, instead of silently requiring the bootstrap. The ddd object gains cluster_vector (per-unit clusters aligned to inf_func_mat rows) and cluster_var. Not-yet-treated / GMM cells take their cluster-robust SE from the same cluster-sum V (the per-cell gmm_se override is skipped under clustering). Includes removal of the multi-period run_preprocess_multPeriods() guard that forced boot = TRUE under clustering (which otherwise made this path dead code). Two-period paths (att_dr.R, att_dr_rc.R) are intentionally unchanged and still require boot = TRUE for clustered inference.

Feature 2 — Remark 10 cluster-sum multiplier bootstrap.
mboot() now applies one multiplier per cluster to the influence function aggregated to cluster sums (not means), with SE scaling bSigma * sqrt(n_clusters) / n. Equal-sized clusters are unaffected; clustered bootstrap SEs change for unbalanced clusters and repeated cross-sections (flagged in NEWS).

Feature 3 — clustered SEs in agg_ddd() aggregations.
compute_se_agg() gains a cluster-sum branch; compute_aggregation() resolves the aligned cluster vector and warns + falls back to i.i.d. when clustering is requested on a variable ddd() did not cluster on (or on an unclustered object). agg_ddd() gains a cluster argument.

Deprecation cleanup.
Replaces the remaining deprecated BMisc::getListElement with BMisc::get_list_element (follow-up to #34). Combined with the already-present rhs_vars/make_balanced_panel, the package is now free of all BMisc deprecation warnings — clearing the CRAN examples WARN for the next submission.

Tests & validation

  • New tests cover the porting guide's validation checklist: analytic ≈ bootstrap, analytic == manual cluster-sum CRVE, equal-sized-cluster bootstrap unchanged, cluster = NULL byte-identical, NYT/GMM cells, warn/fallback, and row-permutation alignment.
  • Adversarial guards added and mutation-tested: an unbalanced-cluster bootstrap test that kills a full Feature-2 revert (which survives the equal-sized test), and an alignment-correctness test that pins the cluster vector against an independent ground truth and kills a wrong-but-stable mapping (which survives the old invariance-only check).
  • R CMD check: 0 errors / 0 warnings / 0 notes. Test suite: 175 passing, 0 fail, 0 skip.

Versioning / docs

  • Version bumped 0.2.2 -> 0.2.3; clustered-inference NEWS grouped under # triplediff 0.2.3 (BMisc deprecation bullets remain under 0.2.2).
  • README.md devel-version badge updated to 0.2.3 (the pkgdown site renders the committed README; navbar version tracks DESCRIPTION). The live site refreshes when this merges to main.

marcelortizv and others added 9 commits June 11, 2026 22:06
`BMisc::getListElement` is deprecated in BMisc (>= 1.4.9). Replaced the
remaining calls with `BMisc::get_list_element` in `process_attgt()` and the
aggregation routines. Follow-up to #34; eliminates deprecation warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Calling `ddd(cluster = <var>, boot = FALSE)` in the multiple-period path now
returns analytic cluster-robust standard errors (cluster-sum CRVE on the
influence function) in `att_gt()` instead of forcing the bootstrap. The `ddd`
object now carries `cluster_vector` and `cluster_var` so downstream
aggregation can reuse the aligned cluster assignment. Removed the
force-bootstrap guard in `run_preprocess_multPeriods()` that previously made
the analytic path unreachable. Two-period paths are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mark 10)

`mboot()` now applies one multiplier per cluster to the influence function
aggregated to cluster *sums* (rather than cluster *means*), with SE scaling
`bSigma * sqrt(n_clusters) / n`, following Callaway & Sant'Anna (2021,
Remark 10). Equal-sized clusters are unaffected; clustered bootstrap standard
errors change for unbalanced clusters and repeated cross-sections.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`compute_se_agg()` gains a cluster-sum branch that computes analytic
cluster-robust standard errors on the aggregated influence function (matching
the cluster-sum bootstrap). `compute_aggregation()` resolves the aligned
per-unit cluster vector stored on the `ddd` object and, on a cluster-variable
mismatch (or an object built without clustering), warns and falls back to
i.i.d. standard errors for both the analytic and bootstrap paths instead of
silently mis-reporting. `agg_ddd()` gains a `cluster` argument. Regenerated
man/agg_ddd.Rd and man/compute_se_agg.Rd.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds tests covering the new clustered-inference paths:
- test-cluster-analytic.R: analytic cluster SEs equal cluster-sum bootstrap,
  manual cluster-sum CRVE agreement, equal-size cluster invariance,
  byte-identical results when no cluster is supplied, and NYT/GMM cells.
- test-mboot-cluster.R: cluster-sum multiplier bootstrap behavior.
- test-aggte-clustervars-override.R: warn/fallback on cluster-var mismatch and
  row-permutation alignment of the cluster vector.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… alignment

Add two test files that close mutation-testing gaps in the existing
clustered-inference suite:

- test-cluster-unbalanced-bootstrap.R: exercises mboot() on UNBALANCED
  clusters (10 tiny + 10 large), where cluster sums != cluster means.
  Pins the SE to the cluster-SUM definition (Remark 10) and asserts it
  diverges materially from the old cluster-MEAN form. The existing
  equal-sized test cannot catch a full Feature 2 revert (means + old
  scaling are algebraically identical when clusters are equal-sized);
  this file kills that surviving mutant. Also checks analytic ~ bootstrap
  end-to-end on unbalanced data.

- test-cluster-alignment-correctness.R: pins att_gt()'s cluster_vector and
  the reported SE against an INDEPENDENT ground-truth partition (the known
  id->cl map joined onto first_period_dta$id), rather than the object's own
  cluster_vector. Catches a wrong-but-stable cluster mapping that the prior
  shuffle-INVARIANCE check cannot distinguish. Runs in R CMD check.

Verified by mutation testing: reverting cluster sums->means, reverting the
SE scaling, and applying a stable cluster-vector permutation each fail these
tests while passing on the current implementation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…emoved

The clustered-inference tests still described run_preprocess_multPeriods()'s
force-boot guard as a present "known bug" with checks "SKIPPED". That override
was removed, so the analytic cluster path is reached directly and those checks
run as regression guards. Update the comments to match; no assertion changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The analytic cluster-robust SE, agg_ddd() cluster argument, and Remark 10
bootstrap behavior change are a distinct feature batch from the 0.2.2 BMisc
deprecation cleanup, so they move under a new 0.2.3 NEWS section and the
package version is bumped to 0.2.3. (0.2.2 was unreleased; CRAN has 0.2.0.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pkgdown site renders the committed README.md (it does not re-knit
README.Rmd), so the devel-version badge has to be updated directly to track
the 0.2.3 version bump. README.Rmd's badge is generated by
badger::badge_devel() and will produce the same value once it is re-knit
against the 0.2.3 DESCRIPTION.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@marcelortizv
marcelortizv requested a review from pedrohcgs June 12, 2026 06:07
@marcelortizv
marcelortizv merged commit 67340a6 into main Jun 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant