Skip to content

Commit 9dfcb52

Browse files
feat(did): etwfe final three — multi-xvar + repeated CS + cgroup
Completes R-etwfe parity by adding the last three features: 1. Multi-xvar heterogeneity: etwfe(xvar=['x1','x2',...]) - xvar now accepts a str (backward-compat) or list of strs - For each cohort g and each x_j, fits a slope coefficient - detail DataFrame gains per-xvar columns: slope_<x>, slope_<x>_se, slope_<x>_pvalue - Backward-compat: when exactly one xvar is given, adds aliases slope_wrt_x / slope_se / slope_pvalue so existing callers keep working - model_info['xvar'] is always a list; model_info['xvar_means'] is a dict of the centering means 2. Repeated cross-section: etwfe(panel=False) - Replaces unit FE with explicit cohort + time dummies - Used for repeated cross-section data where the same unit is not observed across periods (R etwfe's ivar=NULL mode) - Works with xvar, controls, and cluster as expected - model_info['panel'] = False; method string flags the mode 3. cgroup='nevertreated' — restricted control group - Fits one ETWFE regression per cohort, each using only that cohort plus never-treated units (matches R etwfe(cgroup='never')) - Cohort ATTs combined with cohort-size weighting - SE from independent-regression aggregation (conservative) - Validates cgroup ∈ {'notyet','nevertreated'} with clear error - Errors helpfully if no never-treated units exist in the data All three options compose: etwfe(xvar=['x1','x2'], cgroup='nevertreated') and etwfe(xvar='x1', panel=False) both verified to work. Tests: 4 new pytest cases (tests/test_did_summary.py): - test_etwfe_multiple_xvars — slope_x1/slope_x2 columns present - test_etwfe_repeated_cross_section — panel=False labelled + SE differs - test_etwfe_cgroup_nevertreated — per-cohort fit path verified - test_etwfe_cgroup_invalid — invalid cgroup raises Total: 15 passed in 1.43s (was 11). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8ff253a commit 9dfcb52

4 files changed

Lines changed: 373 additions & 47 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ If a method exists in R, we aim to match or exceed its feature set in Python —
177177
| `continuous_did()` | Continuous treatment DID (dose-response) | Callaway, Goodman-Bacon & Sant'Anna (2024) |
178178
| `did_multiplegt()` | DID with treatment switching | de Chaisemartin & D'Haultfoeuille (2020) |
179179
| `did_imputation()` | Imputation DID estimator | Borusyak, Jaravel & Spiess (2024) |
180-
| `wooldridge_did()` / `etwfe()` | Extended TWFE with cohort×post saturation; supports `xvar=` | Wooldridge (2021) |
180+
| `wooldridge_did()` / `etwfe()` | Extended TWFE: `xvar=` (single/multi) + `panel=` (repeated CS) + `cgroup=` (never/notyet) | Wooldridge (2021) |
181181
| `etwfe_emfx()` | R ``etwfe::emfx`` equivalent — simple/group/event/calendar aggregations | McDermott (2023) |
182182
| `drdid()` | Doubly robust 2×2 DID (OR + IPW) | Sant'Anna & Zhao (2020) |
183183
| `stacked_did()` | Stacked event-study DID | Cengiz et al. (2019); Baker, Larcker & Wang (2022) |

README_CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ StatsPAI **不是** R 的 wrapper。我们从原始论文独立重新实现每
9494
| `honest_did()` | 平行趋势假设敏感性 | Rambachan & Roth (2023) |
9595
| `continuous_did()` | 连续处理 DID(剂量反应) | Callaway, Goodman-Bacon & Sant'Anna (2024) |
9696
| `did_imputation()` | 插补 DID 估计量 | Borusyak, Jaravel & Spiess (2024) |
97-
| `wooldridge_did()` / `etwfe()` | 扩展 TWFE(cohort×post 饱和),支持 `xvar=` 异质性 | Wooldridge (2021) |
97+
| `wooldridge_did()` / `etwfe()` | 扩展 TWFE`xvar=`(单/多协变量异质性)+ `panel=`(重复截面)+ `cgroup=`(never/notyet 控制组) | Wooldridge (2021) |
9898
| `etwfe_emfx()` | R `etwfe::emfx` 等价——simple/group/event/calendar 四种聚合边际效应 | McDermott (2023) |
9999
| `drdid()` | 2×2 双重稳健 DID(OR + IPW) | Sant'Anna & Zhao (2020) |
100100
| `stacked_did()` | 堆叠事件研究 DID | Cengiz et al. (2019); Baker, Larcker & Wang (2022) |

0 commit comments

Comments
 (0)