Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
county event studies -12%/-13% (rust/python), with no scenario regressing. Rust outputs
move only at the cross-backend parity tolerances (REGISTRY-documented; never
bit-identical across backends).
- **Internal: cleaned package-source Ruff static-analysis findings** across forward-reference,
stale f-string, invalid `noqa`, and audited unused-local sites. No public API or numerical
behavior change.
- **FE-absorption demeaning rewritten: factorize-once + `np.bincount` method of alternating
projections** (`demean_by_groups` / `within_transform`). Each absorbed dimension is factorized
once and group means are formed via `np.bincount` instead of re-hashing the group keys with
Expand Down
5 changes: 4 additions & 1 deletion diff_diff/bootstrap_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
"""

import warnings
from typing import Optional, Tuple
from typing import TYPE_CHECKING, Optional, Tuple

import numpy as np

from diff_diff._backend import HAS_RUST_BACKEND, _rust_bootstrap_weights

if TYPE_CHECKING:
from diff_diff.survey import ResolvedSurveyDesign

__all__ = [
"generate_bootstrap_weights",
"generate_bootstrap_weights_batch",
Expand Down
1 change: 0 additions & 1 deletion diff_diff/chaisemartin_dhaultfoeuille.py
Original file line number Diff line number Diff line change
Expand Up @@ -5219,7 +5219,6 @@ def _compute_heterogeneity_test(
# count, typically 30-300 groups).
from diff_diff.linalg import _detect_rank_deficiency

n_params = design.shape[1]
rank, _dropped, _pivot = _detect_rank_deficiency(design)

# Guard: need MORE observations than rank for a well-defined
Expand Down
2 changes: 0 additions & 2 deletions diff_diff/honest_did.py
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,6 @@ def _largest_consecutive_block(times, boundary_val):
)

# Build beta_hat and sigma (diagonal - no full VCV for dCDH)
all_times = pre_times + post_times
effects = []
ses = []
for h in pre_times:
Expand Down Expand Up @@ -2143,7 +2142,6 @@ def _arp_confidence_set(
ci_ub : float
Upper bound of confidence set.
"""
num_post = len(beta_hat) - num_pre
beta_post = beta_hat[num_pre:]

# Point estimate and SE for grid centering
Expand Down
1 change: 0 additions & 1 deletion diff_diff/imputation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1305,7 +1305,6 @@ def _impute_treatment_effects(
Imputed counterfactual Y(0).
"""
df_1 = df.loc[omega_1_mask]
n_1 = len(df_1)

# Look up unit and time FE
alpha_i = df_1[unit].map(unit_fe).values
Expand Down
14 changes: 6 additions & 8 deletions diff_diff/power.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,7 @@ def _build_survey_design(self) -> Any:
return self.survey_design
from diff_diff.survey import SurveyDesign

return SurveyDesign(
weights="weight", strata="stratum", psu="psu", fpc="fpc"
)
return SurveyDesign(weights="weight", strata="stratum", psu="psu", fpc="fpc")

@property
def min_viable_n(self) -> int:
Expand Down Expand Up @@ -2267,14 +2265,14 @@ def simulate_power(
if control_group in ("not_yet_treated", "last_cohort"):
raise ValueError(
f"survey_config does not support control_group='{control_group}' "
f"(requires multi-cohort DGP). Use the custom data_generator "
f"path for survey power with this control-group design."
"(requires multi-cohort DGP). Use the custom data_generator "
"path for survey power with this control-group design."
)
if clean_control == "strict":
raise ValueError(
f"survey_config does not support clean_control='strict' "
f"(requires multi-cohort DGP). Use the custom data_generator "
f"path for survey power with strict clean controls."
"survey_config does not support clean_control='strict' "
"(requires multi-cohort DGP). Use the custom data_generator "
"path for survey power with strict clean controls."
)

# SyntheticDiD placebo variance requires n_control > n_treated.
Expand Down
1 change: 0 additions & 1 deletion diff_diff/prep_dgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,6 @@ def generate_factor_data(
"""
rng = np.random.default_rng(seed)

n_control = n_units - n_treated
n_periods = n_pre + n_post

if n_treated > n_units:
Expand Down
2 changes: 1 addition & 1 deletion diff_diff/spillover.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def _compute_nearest_treated_distance_sparse(
"""
# Imported lazily to mirror conley.py's lazy-scipy pattern and keep
# module import cheap when the sparse path isn't exercised.
from scipy.spatial import cKDTree # noqa: WPS433 (deferred import)
from scipy.spatial import cKDTree # deferred import

n_units = all_coords.shape[0]
if metric == "haversine":
Expand Down
13 changes: 7 additions & 6 deletions diff_diff/staggered.py
Original file line number Diff line number Diff line change
Expand Up @@ -4062,15 +4062,16 @@ def _doubly_robust_rc(
X_ct_int = np.column_stack([np.ones(n_ct), X_ct])
X_cs_int = np.column_stack([np.ones(n_cs), X_cs])

# Control OR predictions for all groups
# Control OR predictions used downstream
mu0_post_gt = X_gt_int @ beta_ct # mu_{0,1}(X) for treated-post
mu0_pre_gt = X_gt_int @ beta_cs # mu_{0,0}(X) for treated-post
mu0_post_gs = X_gs_int @ beta_ct # mu_{0,1}(X) for treated-pre
mu0_pre_gs = X_gs_int @ beta_cs # mu_{0,0}(X) for treated-pre
mu0_post_ct = X_ct_int @ beta_ct # mu_{0,1}(X) for control-post
mu0_pre_ct = X_ct_int @ beta_cs # mu_{0,0}(X) for control-post
mu0_post_cs = X_cs_int @ beta_ct # mu_{0,1}(X) for control-pre
mu0_pre_cs = X_cs_int @ beta_cs # mu_{0,0}(X) for control-pre
# The full DRDID R-convention grid also names mu0_pre_ct and
# mu0_post_cs. They are intentionally not materialized here because no
# residual, adjustment, or influence-function term consumes them.

# Treated OR predictions for all groups (for local efficiency adjustment)
mu1_post_gt = X_gt_int @ beta_gt # mu_{1,1}(X) for treated-post
Expand Down Expand Up @@ -4132,11 +4133,11 @@ def _doubly_robust_rc(

pscore = np.clip(pscore, self.pscore_trim, 1 - self.pscore_trim)

# Split propensity scores per group
ps_gt = pscore[:n_gt]
ps_gs = pscore[n_gt : n_gt + n_gs]
# Split control propensity scores per group.
ps_ct = pscore[n_gt + n_gs : n_gt + n_gs + n_ct]
ps_cs = pscore[n_gt + n_gs + n_ct :]
# The symmetric treated slices ps_gt and ps_gs are intentionally not
# materialized: only control propensity slices feed IPW control weights.

# =====================================================================
# 3. Group weights and R-convention means
Expand Down