Skip to content

Commit 334aeda

Browse files
authored
Merge pull request #46 from igerber/claude/plan-v1.2.1-release-pCTqu
2 parents 47b7446 + 38c4856 commit 334aeda

8 files changed

Lines changed: 997 additions & 32 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.2.1] - 2026-01-08
9+
10+
### Added
11+
- **Expanded test coverage** for edge cases:
12+
- Wild bootstrap with very few clusters (< 5), including 2-3 cluster scenarios
13+
- Unbalanced panels with missing periods across units
14+
- Single treated unit scenarios for DiD and Synthetic DiD
15+
- Perfect collinearity detection (validates clear error messages)
16+
- CallawaySantAnna with single treatment cohort
17+
- SyntheticDiD with insufficient pre-treatment periods
18+
19+
### Changed
20+
- **Refactored CallawaySantAnna bootstrap**: Extracted `_compute_effect_bootstrap_stats()` helper method for cleaner code and reduced duplication in bootstrap statistics computation.
21+
822
## [1.2.0] - 2026-01-07
923

1024
### Added
@@ -234,6 +248,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
234248
- `to_dict()` and `to_dataframe()` export methods
235249
- `is_significant` and `significance_stars` properties
236250

251+
[1.2.1]: https://github.com/igerber/diff-diff/compare/v1.2.0...v1.2.1
237252
[1.2.0]: https://github.com/igerber/diff-diff/compare/v1.1.1...v1.2.0
238253
[1.1.1]: https://github.com/igerber/diff-diff/compare/v1.1.0...v1.1.1
239254
[1.1.0]: https://github.com/igerber/diff-diff/compare/v1.0.2...v1.1.0

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ Edge cases needing tests:
7474

7575
## Documentation Improvements
7676

77-
- [ ] Troubleshooting section for common errors
77+
- [x] Troubleshooting section for common errors (added in v1.0.0)
7878
- [ ] Comparison of estimator outputs on same data
7979
- [ ] Real-world data examples (currently synthetic only)
8080
- [ ] Performance benchmarks vs. R packages

diff_diff/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
plot_sensitivity,
9999
)
100100

101-
__version__ = "1.2.0"
101+
__version__ = "1.2.1"
102102
__all__ = [
103103
# Estimators
104104
"DifferenceInDifferences",

diff_diff/staggered.py

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,30 +1603,25 @@ def _run_multiplier_bootstrap(
16031603
weights * bootstrap_atts_gt[b, gt_indices]
16041604
)
16051605

1606-
# Compute bootstrap statistics
1607-
# ATT(g,t) statistics
1606+
# Compute bootstrap statistics for ATT(g,t)
16081607
gt_ses = {}
16091608
gt_cis = {}
16101609
gt_p_values = {}
16111610

16121611
for j, gt in enumerate(gt_pairs):
1613-
original_effect = original_atts[j]
1614-
boot_dist = bootstrap_atts_gt[:, j]
1615-
1616-
se = float(np.std(boot_dist, ddof=1))
1617-
ci = self._compute_percentile_ci(boot_dist, self.alpha)
1618-
p_value = self._compute_bootstrap_pvalue(original_effect, boot_dist)
1619-
1612+
se, ci, p_value = self._compute_effect_bootstrap_stats(
1613+
original_atts[j], bootstrap_atts_gt[:, j]
1614+
)
16201615
gt_ses[gt] = se
16211616
gt_cis[gt] = ci
16221617
gt_p_values[gt] = p_value
16231618

1624-
# Overall ATT statistics
1625-
overall_se = float(np.std(bootstrap_overall, ddof=1))
1626-
overall_ci = self._compute_percentile_ci(bootstrap_overall, self.alpha)
1627-
overall_p_value = self._compute_bootstrap_pvalue(original_overall, bootstrap_overall)
1619+
# Compute bootstrap statistics for overall ATT
1620+
overall_se, overall_ci, overall_p_value = self._compute_effect_bootstrap_stats(
1621+
original_overall, bootstrap_overall
1622+
)
16281623

1629-
# Event study statistics
1624+
# Compute bootstrap statistics for event study effects
16301625
event_study_ses = None
16311626
event_study_cis = None
16321627
event_study_p_values = None
@@ -1637,16 +1632,14 @@ def _run_multiplier_bootstrap(
16371632
event_study_p_values = {}
16381633

16391634
for e in rel_periods:
1640-
original_effect = event_study_info[e]['effect']
1641-
boot_dist = bootstrap_event_study[e]
1642-
1643-
event_study_ses[e] = float(np.std(boot_dist, ddof=1))
1644-
event_study_cis[e] = self._compute_percentile_ci(boot_dist, self.alpha)
1645-
event_study_p_values[e] = self._compute_bootstrap_pvalue(
1646-
original_effect, boot_dist
1635+
se, ci, p_value = self._compute_effect_bootstrap_stats(
1636+
event_study_info[e]['effect'], bootstrap_event_study[e]
16471637
)
1638+
event_study_ses[e] = se
1639+
event_study_cis[e] = ci
1640+
event_study_p_values[e] = p_value
16481641

1649-
# Group effect statistics
1642+
# Compute bootstrap statistics for group effects
16501643
group_effect_ses = None
16511644
group_effect_cis = None
16521645
group_effect_p_values = None
@@ -1657,14 +1650,12 @@ def _run_multiplier_bootstrap(
16571650
group_effect_p_values = {}
16581651

16591652
for g in groups:
1660-
original_effect = group_agg_info[g]['effect']
1661-
boot_dist = bootstrap_group[g]
1662-
1663-
group_effect_ses[g] = float(np.std(boot_dist, ddof=1))
1664-
group_effect_cis[g] = self._compute_percentile_ci(boot_dist, self.alpha)
1665-
group_effect_p_values[g] = self._compute_bootstrap_pvalue(
1666-
original_effect, boot_dist
1653+
se, ci, p_value = self._compute_effect_bootstrap_stats(
1654+
group_agg_info[g]['effect'], bootstrap_group[g]
16671655
)
1656+
group_effect_ses[g] = se
1657+
group_effect_cis[g] = ci
1658+
group_effect_p_values[g] = p_value
16681659

16691660
return CSBootstrapResults(
16701661
n_bootstrap=self.n_bootstrap,
@@ -1817,6 +1808,35 @@ def _compute_bootstrap_pvalue(
18171808

18181809
return float(p_value)
18191810

1811+
def _compute_effect_bootstrap_stats(
1812+
self,
1813+
original_effect: float,
1814+
boot_dist: np.ndarray,
1815+
) -> Tuple[float, Tuple[float, float], float]:
1816+
"""
1817+
Compute bootstrap statistics for a single effect.
1818+
1819+
Parameters
1820+
----------
1821+
original_effect : float
1822+
Original point estimate.
1823+
boot_dist : np.ndarray
1824+
Bootstrap distribution of the effect.
1825+
1826+
Returns
1827+
-------
1828+
se : float
1829+
Bootstrap standard error.
1830+
ci : Tuple[float, float]
1831+
Percentile confidence interval.
1832+
p_value : float
1833+
Bootstrap p-value.
1834+
"""
1835+
se = float(np.std(boot_dist, ddof=1))
1836+
ci = self._compute_percentile_ci(boot_dist, self.alpha)
1837+
p_value = self._compute_bootstrap_pvalue(original_effect, boot_dist)
1838+
return se, ci, p_value
1839+
18201840
def get_params(self) -> Dict[str, Any]:
18211841
"""Get estimator parameters (sklearn-compatible)."""
18221842
return {

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "diff-diff"
7-
version = "1.2.0"
7+
version = "1.2.1"
88
description = "A library for Difference-in-Differences causal inference analysis"
99
readme = "README.md"
1010
license = "MIT"

0 commit comments

Comments
 (0)