v2.3.1.904: time-varying weights, fix_weights, namespace cleanup, expanded tests#257
Conversation
…amespace pollution, fix dreamerr/get() crash - Fix faster_mode mismatch with time-varying sampling weights on balanced panel - Add fix_weights parameter: NULL (default), "varying", "base_period", "first_period" - Replace import(stats), import(utils), import(BMisc) with selective importFrom - Fix aggte() crash from dreamerr intercepting get() inside data.table - Add runtime message for time-varying weights detection - Skip inference tests gracefully when did v2.1.2 unavailable from CRAN - Add GitHub Action to auto-bump version on PR merge - 236 PASS, 0 FAIL, 8 SKIP
…sistency tests The half-split aggregation of the RC influence function in compute.att_gt.R incorrectly paired pre/post contributions across different units, producing wrong standard errors. Replace with rowsum() by unit ID for correct, order-independent aggregation. This also fixes a pre-existing SE discrepancy between faster_mode=TRUE and faster_mode=FALSE when fix_weights="varying" (~2x SE inflation in slow mode). Add 72 new tests verifying both ATT and SE match between slow/fast modes across all fix_weights options, est_methods, base_periods, control groups, data types (panel, RC, unbalanced), and covariate specifications. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… HTTPS mirror - test.yaml: replace hand-rolled install with r-lib/actions/setup-r-dependencies (fixes "remotes is required" error from deprecated devtools::install_deps) - bump-version.yaml: add concurrency group to prevent race conditions on concurrent PR merges - test-inference.R: use HTTPS CRAN mirror, verify install with requireNamespace Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Upgrade from r-lib/actions v1 (deprecated) to v2 to match the rest of the CI workflows. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This reverts commit 6abc765.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lete namespace cleanup, suppress test warnings
- Block fix_weights="varying" with custom est_method (breaks calling convention)
- Add missing @importFrom: stats::ecdf, stats::glm, stats::predict
- Register .w, D, N, post, weights as globalVariables (R CMD check clean)
- Replace fragile exists("use_rc_for_weights") with direct dp2 check
- Revert GH Actions workflow to reliable pull_request:closed trigger
- Fix glance.MP/AGGTEobj for faster_mode field names
- Fix get_wide_data .checkTypos, pre_process_did2 by=get() -> by=c()
- Suppress expected warnings in all test files (0 WARN, 1 SKIP, 610 PASS)
- Add 6 new test files: aggte, edge-cases, error-handling, faster-mode, ggdid, glance
…d of dropping Previously, faster_mode=TRUE would drop (g,t) cells that failed estimation when base_period="varying", while slower mode kept them as NA. This caused faster_mode=TRUE to hard-stop with "No valid (g,t) cells found" when all cells failed (e.g., singular RC covariate designs with fix_weights="varying"), while slower mode returned an MP with all-NA ATTs.
…e docs - Fix test my_rc_est: capture length(y) before subsetting to avoid returning a short influence function (sum(D==0) vs n_obs), which caused silent recycling warnings masked by blanket suppressWarnings() - Replace suppressWarnings with withCallingHandlers that promotes recycling warnings to errors while muffling expected Wald pre-test warns - Guard fix_weights="varying" + custom est_method rejection to panel=TRUE only, so the RC path works correctly with custom estimators - Restore geom_errorbarh() in splot() for horizontal group-time plots - Update est_method docs (roxygen + Rd) to describe both panel and RC custom-estimator signatures and correct return field name (att.inf.func) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e docs - Fix unbalanced panel crash with fix_weights="base_period"/"first_period" in slow mode: use current disdat IDs instead of stale rightids for influence function aggregation (fixes length mismatch error) - Fix est_method docs: signature was Y1,Y0,treat but code calls y1,y0,D
…e docs - Fix est_method docs: panel signature was f(Y1,Y0,treat,...) but code calls f(y1,y0,D,covariates,i.weights,inffunc,...). Updated to match. - Add test for unbalanced panel + fix_weights with units missing from reference period (exercises the row-dropping + IF aggregation path)
There was a problem hiding this comment.
Pull request overview
This PR updates the did package to improve correctness and API clarity around time-varying sampling weights, while also reducing namespace pollution and substantially expanding automated test coverage (including fast/slow mode parity).
Changes:
- Fixes fast/slow mode ATT parity when
weightsnamevaries over time and introducesfix_weightsto control weight resolution behavior. - Cleans up package imports (
importFrom()), fixes aget()/column-name collision crash, and improvesglance()output consistency infaster_mode. - Adds multiple new test files covering edge cases, error handling, plotting helpers, aggregations, and fast/slow consistency.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/testthat/test-user_bug_fixes.R | Wraps known-warning calls to stabilize expectations. |
| tests/testthat/test-jel_replication.R | Suppresses expected warnings in replication tests; adds parity checks. |
| tests/testthat/test-inference.R | Adds install-availability gating for old did version and skips when unavailable. |
| tests/testthat/test-glance.R | New tests for glance.MP()/glance.AGGTEobj() across faster_mode. |
| tests/testthat/test-ggdid.R | New tests for ggdid() outputs and error paths. |
| tests/testthat/test-faster-mode-consistency.R | New systematic faster_mode TRUE/FALSE consistency grid tests. |
| tests/testthat/test-error-handling.R | New tests for validation errors, warnings, and messages. |
| tests/testthat/test-edge-cases.R | New boundary/edge case tests for att_gt()/aggte(). |
| tests/testthat/test-att_gt.R | Adds fix_weights and “column literally named gname/tname/idname” regression coverage. |
| tests/testthat/test-aggte-comprehensive.R | New comprehensive aggregation behavior + IF-length tests. |
| R/utility_functions.R | Makes get_wide_data()/check_balance() more robust to name collisions. |
| R/tidy.R | Fixes glance() fields under faster_mode and aligns group/time counts. |
| R/pre_process_did2.R | Adds time-varying weights message; threads fix_weights; builds weights tensor. |
| R/pre_process_did.R | Threads fix_weights; adds time-varying weights message in slow path. |
| R/imports.R | Replaces blanket imports with targeted importFrom(); extends globalVariables(). |
| R/DIDparams2.R | Stores weights_tensor + fix_weights in DIDparams for faster mode. |
| R/DIDparams.R | Adds fix_weights to DIDparams for slower mode. |
| R/compute.att_gt2.R | Implements fix_weights behavior in faster mode, including RC fallback for "varying". |
| R/compute.att_gt.R | Implements fix_weights behavior in slower mode and adjusts IF aggregation. |
| R/compute.aggte.R | Replaces get() with set() to avoid column-name collision crash. |
| R/att_gt.R | Documents and validates fix_weights; updates custom est_method signature docs. |
| NEWS.md | Adds release notes for v2.3.1.904 changes. |
| NAMESPACE | Switches to selective imports and adds needed importFrom() entries. |
| man/pre_process_did2.Rd | Documents fix_weights and updated custom-estimator signatures. |
| man/pre_process_did.Rd | Documents fix_weights and updated custom-estimator signatures. |
| man/DIDparams.Rd | Documents fix_weights and weight-handling behavior. |
| man/conditional_did_pretest.Rd | Updates weightsname documentation to reflect time-varying behavior. |
| man/att_gt.Rd | Documents fix_weights and updated custom-estimator signatures. |
| DESCRIPTION | Bumps version to 2.3.1.904. |
| .github/workflows/test.yaml | Modernizes R CI actions and dependency installation. |
| .github/workflows/test-coverage.yaml | Updates coverage workflow to v2 actions + improved covr invocation. |
| .github/workflows/bump-version.yaml | Adds workflow to auto-increment dev version on PR merge. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| temp_lib <- tempfile() | ||
| dir.create(temp_lib) | ||
| remotes::install_version("did", version = "2.1.2", lib = temp_lib, repos = "http://cran.us.r-project.org") | ||
| old_did_available <- tryCatch({ | ||
| remotes::install_version("did", version = "2.1.2", lib = temp_lib, repos = "https://cloud.r-project.org", quiet = TRUE) | ||
| isTRUE(requireNamespace("did", lib.loc = temp_lib, quietly = TRUE)) | ||
| }, error = function(e) FALSE) | ||
|
|
||
| if (!old_did_available) { | ||
| # Clean up and skip all tests in this file | ||
| unlink(temp_lib, recursive = TRUE) | ||
| } |
There was a problem hiding this comment.
remotes::install_version() is executed at file load time (outside any test_that) and without a CRAN/offline guard. This will still attempt a network install during R CMD check even though the tests later skip_if(!old_did_available, ...), which can fail in restricted environments. Consider moving the install into the relevant test_that blocks and guarding it with skip_on_cran() / skip_if_offline() (or equivalent), so no network calls occur when checks disallow internet.
| old_did_available <- tryCatch({ | ||
| remotes::install_version("did", version = "2.1.2", lib = temp_lib, repos = "https://cloud.r-project.org", quiet = TRUE) | ||
| isTRUE(requireNamespace("did", lib.loc = temp_lib, quietly = TRUE)) | ||
| }, error = function(e) FALSE) | ||
|
|
||
| if (!old_did_available) { | ||
| # Clean up and skip all tests in this file | ||
| unlink(temp_lib, recursive = TRUE) | ||
| } |
There was a problem hiding this comment.
Cleanup of temp_lib is only done immediately when the install fails. When the install succeeds, the temp library directory will still contain installed packages and should be cleaned up reliably (e.g., via on.exit()/withr::defer()) using unlink(temp_lib, recursive = TRUE) to avoid leaving behind directories/artifacts in CI.
| old_did_available <- tryCatch({ | |
| remotes::install_version("did", version = "2.1.2", lib = temp_lib, repos = "https://cloud.r-project.org", quiet = TRUE) | |
| isTRUE(requireNamespace("did", lib.loc = temp_lib, quietly = TRUE)) | |
| }, error = function(e) FALSE) | |
| if (!old_did_available) { | |
| # Clean up and skip all tests in this file | |
| unlink(temp_lib, recursive = TRUE) | |
| } | |
| on.exit(unlink(temp_lib, recursive = TRUE), add = TRUE) | |
| old_did_available <- tryCatch({ | |
| remotes::install_version("did", version = "2.1.2", lib = temp_lib, repos = "https://cloud.r-project.org", quiet = TRUE) | |
| isTRUE(requireNamespace("did", lib.loc = temp_lib, quietly = TRUE)) | |
| }, error = function(e) FALSE) |
| post_rc <- as.numeric(disdat_long[[tname]] == tlist[t + tfac]) | ||
| w_rc <- disdat_long$.w | ||
| covariates_rc <- model.matrix(xformla, data = disdat_long) | ||
| n1_rc <- sum(G_rc + disdat_long$.C) # careful: n1 for RC is different |
There was a problem hiding this comment.
n1_rc is computed but never used. This looks like leftover scaffolding from the RC-weight path and adds noise to a performance-sensitive inner loop; please remove it (or use it if it was intended for scaling/checks).
| n1_rc <- sum(G_rc + disdat_long$.C) # careful: n1 for RC is different |
|
|
||
| # Determine the maximum number of time periods for any unit | ||
| max_time_periods <- data[, uniqueN(get(time_col))] | ||
| max_time_periods <- length(unique(data[[time_col]])) |
There was a problem hiding this comment.
check_balance() now computes max_time_periods via length(unique(data[[time_col]])). Since this function is likely used on large data.tables, consider using data.table::uniqueN(data[[time_col]]) (no get() needed) to keep this O(n) and avoid allocating the full unique vector.
| max_time_periods <- length(unique(data[[time_col]])) | |
| max_time_periods <- data.table::uniqueN(data[[time_col]]) |
…eanup - Remove unused n1_rc variable in fix_weights="varying" RC path - Use data.table::uniqueN() instead of length(unique()) in check_balance - Fix test-inference.R: withr::defer for temp_lib cleanup, NOT_CRAN guard around network install, remove stale unlink at EOF
|
@marcelortizv review |
geom_errorbarh() was deprecated in ggplot2 4.0.0. The replacement produces identical plots with no deprecation warning.
…rorbarh - Use binomial(link="logit") instead of bare logit symbol in trimmer() - Version-gate geom_errorbar(orientation="y") to ggplot2 >= 3.3.0, falling back to geom_errorbarh() on older installations
- Move overlap/rank checks inside fix_weights="varying" branch to run on RC data (covariates_rc, G_rc) instead of wide panel data - Add droplevels() on disdat_long before model.matrix() to prevent phantom zero-column dummies from unused factor levels - Keep panel guards in non-varying path (moved inside tryCatch) - Restore fix_weights="varying" + custom est_method block for all panel data (remove allow_unbalanced_panel bypass that left balanced panels exposed to wrong-signature dispatch) - Remove misleading "set panel=FALSE" workaround from error message - Workflow: scope idempotency check to base branch only, not --all refs
The "varying" option should only change weights, not the covariate conditioning set. Both code paths now use pre-period covariates for all observations in the stacked RC data, matching the panel estimator's covariate handling. Post-treatment covariates are never used. Fast path: rbind(cov_pre, cov_pre) instead of rbind(cov_pre, cov_post) Slow path: match() lookup to map each observation to its unit's pre-period covariates regardless of row ordering in disdat_long
With base_period="universal", pret can be later than the current time period for placebo cells. The varying path was always using covariates from pret, which meant conditioning on future covariates for placebo cells. Now uses min(pret, t) to match the panel estimator's convention: always the earlier of the two comparison periods.
- Add skip_on_cran() to each test_that in test-inference.R (more idiomatic than relying on the top-level NOT_CRAN guard alone) - Remove misleading "Added broom to Suggests" NEWS entry (broom was added in 2.3.1.903, not 2.3.1.904) - Add test exercising splot() via ggdid(agg_grp) to cover the ggplot2 version-gated errorbar layer and guard against deprecation warnings
What this PR does
This PR (v2.3.1.904) fixes a bug where
faster_mode = TRUEandFALSEproduced different ATT estimates with time-varying sampling weights, adds the newfix_weightsparameter for explicit user control over weight resolution, cleans up namespace pollution that was maskingdplyr::filter/dplyr::lag, and expands the test suite from ~330 to 687 tests with 0 warnings.Bug fixes
Time-varying weights produced different ATTs in fast vs slow mode. The fast path (
compute.att_gt2.R) always pulled weights fromweights_vector(first period). Now both paths use a per-periodweights_tensorso each 2×2 comparison uses the correct period's weight. (R/compute.att_gt2.R, R/pre_process_did2.R, R/DIDparams2.R)aggte()crashed when the user's group column was literally namedgname.dreamerr>= 1.5.0 interceptsdata.table'sget()inside[.data.table. Replaceddt[get(gname) == Inf, (gname) := 0]withset(). (R/compute.aggte.R)faster_mode = TRUEdropped failed (g,t) cells; slow mode kept them as NA. Withbase_period = "varying", fast mode returned NULL for failed cells, filtered them out, and hard-stopped with "No valid (g,t) cells found." Now both modes keep failed cells asatt = NA. (R/compute.att_gt2.R, lines 530–534)glance.MP()returned NULL forngroup/ntimewithfaster_mode = TRUE. DIDparams2 usestreated_groups_count/time_periods_count, notnG/nT. Added mode-aware branching. Also fixedglance.AGGTEobj()which was usingnrow(cohort_counts)(overcounts by including never-treated group) — now usestreated_groups_count. (R/tidy.R)Unbalanced panel +
fix_weights = "base_period"/"first_period"crashed in slow mode. The influence function aggregation at line 599 usedrightidscaptured before the fix_weights path dropped rows with missing reference-period weights. Replaced withcurrent_idsfrom the post-dropdisdat. (R/compute.att_gt.R, line 599)est_methoddocs described wrong custom estimator signature. Docs saidf(Y1, Y0, treat, covariates)but code actually callsf(y1, y0, D, covariates, i.weights, inffunc, ...)for panel andf(y, post, D, covariates, i.weights, inffunc, ...)for RC. Updated docs to match. (R/att_gt.R, lines 89–134)get_wide_data()could crash withdata.table.checkTyposwhen user columns match local variable names (e.g., a column namedtname). Pre-extractdata[[tname]]to avoid. (R/utility_functions.R)Namespace pollution. Blanket
import(stats),import(utils),import(BMisc)re-exported hundreds of symbols includingstats::filterandstats::lag, maskingdplyr. Replaced with selectiveimportFrom(). Also added missing imports forstats::ecdf,stats::glm,stats::predictand registered.w,D,N,post,weightsasglobalVariables. R CMD check now has 0 code-related NOTEs. (R/imports.R, NAMESPACE)New features
fix_weightsparameter inatt_gt()— controls how time-varying sampling weights are resolved in each 2×2 DiD comparison:NULL(default): earlier-period weight for balanced panel; per-observation for RC/unbalanced"varying": per-observation weights via RC estimators (blocked for customest_methodon panel)"base_period": fix at g-1 for all cells in a group (blocked forpanel = FALSE)"first_period": fix at first period for all cells (blocked forpanel = FALSE)Implemented in both slow path (R/compute.att_gt.R) and fast path (R/compute.att_gt2.R) with matching influence function aggregation. (R/att_gt.R, R/DIDparams.R, R/DIDparams2.R, R/pre_process_did.R, R/pre_process_did2.R)
nobs()S3 methods forMPandAGGTEobj. (R/tidy.R)tidy()additions:statistic(t-stat) andp.value(pointwise, two-sided) columns for bothMPandAGGTEobj. (R/tidy.R)Time-varying weight detection message when weights vary across time in balanced panel data. (R/pre_process_did.R, R/pre_process_did2.R)
CI / maintenance
bump-version.yamlGitHub Action: auto-bumps dev version in DESCRIPTION on PR merge to mastertest.yaml: replaced manual apt-get/cache withr-lib/actions/setup-r-dependencies@v2test-coverage.yaml: v1 → v2 actionstest-inference.Rcleanup: HTTPS mirror,NOT_CRANguard around network install,withr::deferfor temp directory cleanupTest suite
6 new test files added:
test-aggte-comprehensive.R— all aggregation types, na.rm, min_e/max_e/balance_etest-edge-cases.R— single group, two periods, no never-treated, unbalancedtest-error-handling.R— validation errors, warnings, messagestest-faster-mode-consistency.R— 36-combination grid: est_method × panel_type × control_group × base_periodtest-ggdid.R— plot generation for all aggregation typestest-glance.R— glance.MP and glance.AGGTEobj for both faster_mode settingsExisting tests modified:
suppressWarnings()on expected cband/Wald warnings to eliminate noise. Result: 0 FAIL, 0 WARN, 1 SKIP (known DRDID bug), 687 PASS.Test plan
devtools::test()— 687 PASS, 0 FAIL, 0 WARNdevtools::check()— 0 errors, 0 warnings, 3 NOTEs (all environmental: covr not installed, installed size, NTP)est_method×base_period×control_groupfix_weightsoptions tested on balanced, unbalanced, and RC panelsfix_weightswith units missing from reference period: no crash, fast/slow matchest_method+"varying"blocked;"base_period"/"first_period"blocked for RC