Skip to content
Closed
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
50 changes: 50 additions & 0 deletions .github/workflows/bump-version.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Bump dev version on PR merge

on:
pull_request:
types: [closed]
branches: [master, main]

jobs:
bump-version:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest

permissions:
contents: write

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref }}
token: ${{ secrets.GITHUB_TOKEN }}

Comment on lines +3 to +21
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow attempts to push a commit back to the base branch on the pull_request event. For PRs from forks, GITHUB_TOKEN is typically read-only on pull_request workflows, so git push will fail. Consider triggering on push to the default branch (after merge) or using pull_request_target with appropriate hardening, so the workflow can reliably bump the version without permission issues.

Copilot uses AI. Check for mistakes.
- name: Bump dev version in DESCRIPTION
run: |
# Extract current version
current=$(grep '^Version:' DESCRIPTION | sed 's/Version: //')
echo "Current version: $current"

# Split into parts
IFS='.' read -ra parts <<< "$current"
major="${parts[0]}"
minor="${parts[1]}"
patch="${parts[2]}"
dev="${parts[3]:-0}"

# Increment dev version
new_dev=$((dev + 1))
new_version="${major}.${minor}.${patch}.${new_dev}"
echo "New version: $new_version"

# Update DESCRIPTION
sed -i "s/^Version: .*/Version: ${new_version}/" DESCRIPTION

# Configure git
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

# Commit and push
git add DESCRIPTION
git diff --cached --quiet || git commit -m "Bump version to ${new_version}"
git push
Comment on lines +16 to +50
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow directly commits & pushes to the base branch on PR merge; if multiple PRs merge close together, pushes can race and fail with a non-fast-forward error. Consider adding a concurrency group and/or pulling/rebasing before pushing (or using a dedicated version-bump action that retries) to make the bump more reliable.

Copilot uses AI. Check for mistakes.
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: did
Title: Treatment Effects with Multiple Periods and Groups
Version: 2.3.1.903
Version: 2.3.1.904
Authors@R: c(person("Brantly", "Callaway", email = "brantly.callaway@uga.edu", role = c("aut", "cre")), person("Pedro H. C.", "Sant'Anna", email="pedro.santanna@emory.edu", role = c("aut")))
URL: https://bcallaway11.github.io/did/, https://github.com/bcallaway11/did/
Description: The standard Difference-in-Differences (DID) setup involves two periods and two groups -- a treated group and untreated group. Many applications of DID methods involve more than two periods and have individuals that are treated at different points in time. This package contains tools for computing average treatment effect parameters in Difference in Differences setups with more than two periods and with variation in treatment timing using the methods developed in Callaway and Sant'Anna (2021) <doi:10.1016/j.jeconom.2020.12.001>. The main parameters are group-time average treatment effects which are the average treatment effect for a particular group at a a particular time. These can be aggregated into a fewer number of treatment effect parameters, and the package deals with the cases where there is selective treatment timing, dynamic treatment effects, calendar time effects, or combinations of these. There are also functions for testing the Difference in Differences assumption, and plotting group-time average treatment effects.
Expand Down
24 changes: 21 additions & 3 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,15 @@ export(splot)
export(test.mboot)
export(tidy)
export(trimmer)
import(BMisc)
import(data.table)
import(fastglm)
import(ggplot2)
import(stats)
import(utils)
importFrom(BMisc,TorF)
importFrom(BMisc,getListElement)
importFrom(BMisc,makeBalancedPanel)
importFrom(BMisc,multiplier_bootstrap)
importFrom(BMisc,rhs.vars)
importFrom(BMisc,toformula)
importFrom(DRDID,drdid_panel)
importFrom(DRDID,drdid_rc)
importFrom(DRDID,reg_did_panel)
Expand All @@ -55,5 +58,20 @@ importFrom(generics,glance)
importFrom(generics,tidy)
importFrom(methods,as)
importFrom(methods,is)
importFrom(stats,aggregate)
importFrom(stats,binomial)
importFrom(stats,complete.cases)
importFrom(stats,cov)
importFrom(stats,model.frame)
importFrom(stats,model.matrix)
importFrom(stats,na.pass)
importFrom(stats,nobs)
importFrom(stats,pchisq)
importFrom(stats,pnorm)
importFrom(stats,qnorm)
importFrom(stats,quantile)
importFrom(stats,rnorm)
importFrom(stats,setNames)
importFrom(stats,var)
importFrom(tidyr,gather)
importFrom(utils,globalVariables)
28 changes: 28 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
# did 2.3.1.904

* Fixed bug where `faster_mode = TRUE` and `faster_mode = FALSE` produced different ATT estimates when sampling weights (`weightsname`) vary across time. The fast path was always using first-period weights; it now correctly uses the same period's weights as the slow path

* New `fix_weights` argument in `att_gt()` gives users explicit control over how time-varying sampling weights are resolved in each 2x2 DiD comparison. Options: `NULL` (default, preserves existing behavior), `"varying"` (per-observation weights using RC estimators), `"base_period"` (fix at g-1 for all cells), `"first_period"` (fix at first period). See `?att_gt` for details

* Runtime message when time-varying weights are detected in balanced panel data, directing users to the `fix_weights` argument

* Reduced namespace pollution: replaced blanket `import(stats)`, `import(utils)`, and `import(BMisc)` with selective `importFrom()` calls. The `did` package no longer re-exports `stats::filter` or `stats::lag`, which previously masked `dplyr::filter` and `dplyr::lag` when both packages were loaded

* Fixed `aggte()` crash (`"Error in get(gname): invalid first argument"`) when the user's group column is literally named `gname` and `dreamerr` >= 1.5.0 is installed. The issue was `dreamerr` intercepting `data.table`'s `get()` inside `[.data.table`; replaced with `set()` which is immune to this

* Expanded `weightsname` documentation explaining how time-varying weights are handled differently for balanced panels vs. repeated cross sections and unbalanced panels

* Added `nobs()` S3 methods for `MP` and `AGGTEobj` objects, returning the number of unique cross-sectional units as an integer

* Added `statistic` (t-statistic) and `p.value` (pointwise, two-sided) columns to `tidy()` output for both `MP` and `AGGTEobj` objects, following `broom` conventions

* Added `broom` to `Suggests`

# did 2.3.1.903

* Added `nobs()` S3 methods and `statistic`/`p.value` columns to `tidy()` output (superseded by 2.3.1.904 entry above)

# did 2.3.1.902

* Bug fixes, diagnostic improvements, and JEL replication tests

# did 2.3.1.901

* `att_gt()` now accepts `...` (dots) for passing additional arguments to custom `est_method` functions
Expand Down
2 changes: 2 additions & 0 deletions R/DIDparams.R
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ DIDparams <- function(yname,
control_group,
anticipation=0,
weightsname=NULL,
fix_weights=NULL,
alp=0.05,
bstrap=TRUE,
biters=1000,
Expand Down Expand Up @@ -54,6 +55,7 @@ DIDparams <- function(yname,
control_group=control_group,
anticipation=anticipation,
weightsname=weightsname,
fix_weights=fix_weights,
alp=alp,
bstrap=bstrap,
biters=biters,
Expand Down
4 changes: 4 additions & 0 deletions R/DIDparams2.R
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ DIDparams2 <- function(did_tensors, args, call=NULL) {
covariates_matrix <- did_tensors$covariates_matrix
cluster_vector <- did_tensors$cluster
weights_vector <- did_tensors$weights
weights_tensor <- did_tensors$weights_tensor
fix_weights <- args$fix_weights


out <- list(yname=yname,
Expand Down Expand Up @@ -89,6 +91,8 @@ DIDparams2 <- function(did_tensors, args, call=NULL) {
covariates_matrix = covariates_matrix,
cluster_vector=cluster_vector,
weights_vector=weights_vector,
weights_tensor=weights_tensor,
fix_weights=fix_weights,
call=call)
class(out) <- "DIDparams"
return(out)
Expand Down
56 changes: 55 additions & 1 deletion R/att_gt.R
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,50 @@
#' It defines which "group" a unit belongs to. It should be 0 for units
#' in the untreated group.
#' @param weightsname The name of the column containing the sampling weights.
#' If not set, all observations have same weight.
#' If not set, all observations have same weight. When weights are
#' time-invariant (constant within each unit across periods), all
#' \code{fix_weights} options produce identical results and no special
#' handling is needed.
#'
#' When weights vary across time (e.g., time-varying population sizes),
#' the default behavior differs by panel type:
#' \describe{
#' \item{Balanced panel}{Each 2x2 DiD comparison uses the weight from the
#' earlier of the two time periods involved. For post-treatment cells,
#' this is the base period (g-1). For pre-treatment cells with
#' \code{base_period="varying"}, this is the pre-treatment period itself.
#' The panel DRDID estimators are used.}
#' \item{Repeated cross sections and unbalanced panels}{Both periods'
#' per-observation weights are passed directly to the RC DRDID estimators,
#' so each observation carries its own period-specific weight.}
#' }
#' Use the \code{fix_weights} argument to override the default behavior.
#' @param fix_weights Controls how time-varying sampling weights are resolved.
#' Only relevant when weights vary across time; with time-invariant weights,
#' all options produce identical results. Options:
#' \describe{
#' \item{\code{NULL} (default)}{For balanced panel: uses the weight from
#' the earlier of the two time periods in each 2x2 comparison. For
#' post-treatment cells, this is the base period (g-1). For
#' pre-treatment cells, this depends on the \code{base_period} setting.
#' For RC/unbalanced panel: uses per-observation weights from both
#' periods.}
#' \item{\code{"varying"}}{Uses per-observation, period-specific weights
#' for all panel types. For balanced panel data, this switches to the
#' repeated cross-section DRDID estimators so that pre-period and
#' post-period observations each carry their own weight. This is the
#' most flexible option but sacrifices the efficiency of the panel
#' estimator. For RC/unbalanced panel, this is identical to the
#' default.}
#' \item{\code{"base_period"}}{Fixes weights at the base period (g-1) for
#' all (g,t) cells within a group, for both pre-treatment and
#' post-treatment comparisons. Ensures all cells within a group use the
#' same weights. For RC/unbalanced panel, units not observed in the base
#' period are dropped with a warning.}
#' \item{\code{"first_period"}}{Fixes weights at the first time period in
#' the dataset for all (g,t) cells. For RC/unbalanced panel, units not
#' observed in the first period are dropped with a warning.}
#' }
#' @param alp the significance level, default is 0.05
#' @param bstrap Boolean for whether or not to compute standard errors using
#' the multiplier bootstrap. If standard errors are clustered, then one
Expand Down Expand Up @@ -195,6 +238,7 @@ att_gt <- function(yname,
control_group = c("nevertreated", "notyettreated"),
anticipation = 0,
weightsname = NULL,
fix_weights = NULL,
alp = 0.05,
bstrap = TRUE,
cband = TRUE,
Expand All @@ -217,6 +261,14 @@ att_gt <- function(yname,
"\". Extra arguments are only passed to custom est_method functions.")
}

# Validate fix_weights
if (!is.null(fix_weights)) {
if (!is.character(fix_weights) || length(fix_weights) != 1 ||
!(fix_weights %in% c("varying", "base_period", "first_period"))) {
stop("fix_weights must be NULL or one of \"varying\", \"base_period\", or \"first_period\".")
}
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix_weights is validated for allowed strings, but not for compatibility with panel = FALSE true repeated cross-sections. With current implementation, fix_weights = "base_period"/"first_period" can effectively drop one period’s observations (since IDs don’t persist across time), producing invalid/NA results. Please add validation to disallow these options when panel = FALSE and the data are true repeated cross-sections (or clearly document/implement a different RC-compatible definition).

Suggested change
}
}
if (!panel && !is.null(fix_weights) &&
fix_weights %in% c("base_period", "first_period")) {
stop(paste0(
"fix_weights = \"", fix_weights, "\" is not supported when panel = FALSE ",
"(true repeated cross-sections). Use fix_weights = \"varying\" or NULL ",
"for repeated cross-section data."
))
}

Copilot uses AI. Check for mistakes.

# Validate est_method
if (!inherits(est_method, "function")) {
if (!is.character(est_method) || length(est_method) != 1) {
Expand Down Expand Up @@ -249,6 +301,7 @@ att_gt <- function(yname,
control_group = control_group,
anticipation = anticipation,
weightsname = weightsname,
fix_weights = fix_weights,
alp = alp,
bstrap = bstrap,
cband = cband,
Expand Down Expand Up @@ -284,6 +337,7 @@ att_gt <- function(yname,
control_group = control_group,
anticipation = anticipation,
weightsname = weightsname,
fix_weights = fix_weights,
alp = alp,
bstrap = bstrap,
cband = cband,
Expand Down
2 changes: 1 addition & 1 deletion R/compute.aggte.R
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ compute.aggte <- function(MP,
}
if (isTRUE(dp$faster_mode)) {
dt <- dp$data
dt[get(gname) == Inf, (gname) := 0] # going back to the old way
set(dt, i = which(dt[[gname]] == Inf), j = gname, value = 0) # going back to the old way
data <- as.data.frame(dt)
rm(dt)
tlist <- dp$time_periods
Expand Down
Loading
Loading