Skip to content

Commit 36ae5bc

Browse files
feat(did): add explicit etwfe() API aligned with R etwfe package
etwfe() is a thin alias of wooldridge_did() that matches the naming convention of Grant McDermott's R 'etwfe' package. Same saturated TWFE estimator underneath; the new name improves discoverability for users coming from the R ecosystem and makes the Wooldridge (2021) connection explicit in the public API. Docstring includes argument mapping table (R etwfe -> sp.etwfe) and notes that xvar (covariate heterogeneity) and emfx aggregations are not yet mirrored. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2194ad2 commit 36ae5bc

3 files changed

Lines changed: 100 additions & 2 deletions

File tree

src/statspai/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
did, did_2x2, ddd, callaway_santanna, sun_abraham,
3535
bacon_decomposition, honest_did, breakdown_m, event_study,
3636
did_analysis, DIDAnalysis, did_multiplegt, did_imputation, stacked_did, cic,
37-
wooldridge_did, drdid, twfe_decomposition,
37+
wooldridge_did, etwfe, drdid, twfe_decomposition,
3838
pretrends_test, pretrends_power, sensitivity_rr, SensitivityResult, pretrends_summary,
3939
parallel_trends_plot, bacon_plot, group_time_plot, did_plot,
4040
enhanced_event_study_plot, treatment_rollout_plot,
@@ -221,6 +221,7 @@
221221
"cohort_event_study_plot",
222222
# Wooldridge / DR-DID / TWFE Decomposition
223223
"wooldridge_did",
224+
"etwfe",
224225
"drdid",
225226
"twfe_decomposition",
226227
# RD

src/statspai/did/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
SensitivityResult,
4646
pretrends_summary,
4747
)
48-
from .wooldridge_did import wooldridge_did, drdid, twfe_decomposition
48+
from .wooldridge_did import wooldridge_did, etwfe, drdid, twfe_decomposition
4949
from .continuous_did import continuous_did
5050
from .plots import (
5151
parallel_trends_plot,
@@ -384,6 +384,7 @@ def did(
384384
'cic',
385385
# Wooldridge / DR-DID / TWFE decomposition
386386
'wooldridge_did',
387+
'etwfe',
387388
'drdid',
388389
'twfe_decomposition',
389390
# Pre-trends

src/statspai/did/wooldridge_did.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,102 @@ def wooldridge_did(
349349
)
350350

351351

352+
def etwfe(
353+
data: pd.DataFrame,
354+
y: str,
355+
group: str,
356+
time: str,
357+
first_treat: str,
358+
controls: Optional[List[str]] = None,
359+
cluster: Optional[str] = None,
360+
alpha: float = 0.05,
361+
) -> CausalResult:
362+
"""
363+
Extended Two-Way Fixed Effects (ETWFE) — Wooldridge (2021).
364+
365+
Explicit API matching the R package ``etwfe`` (McDermott, 2023).
366+
This is an alias for :func:`wooldridge_did`; both estimate the same
367+
saturated TWFE regression with cohort × post interactions that
368+
recovers valid ATT under heterogeneous treatment effects.
369+
370+
Parameters
371+
----------
372+
data : pd.DataFrame
373+
Panel dataset (long format).
374+
y : str
375+
Outcome variable.
376+
group : str
377+
Unit identifier.
378+
time : str
379+
Time period variable.
380+
first_treat : str
381+
Column with first-treatment period; NaN or 0 for never-treated.
382+
controls : list of str, optional
383+
Time-varying covariates.
384+
cluster : str, optional
385+
Cluster variable for SE (defaults to ``group``).
386+
alpha : float, default 0.05
387+
Significance level.
388+
389+
Returns
390+
-------
391+
CausalResult
392+
Cohort-size-weighted ATT with cohort-level detail and event-study
393+
coefficients in ``model_info['event_study']``.
394+
395+
Notes
396+
-----
397+
Naming map to the R ``etwfe`` package:
398+
399+
============================ ========================================
400+
R ``etwfe`` argument ``sp.etwfe`` argument
401+
============================ ========================================
402+
``fml = y ~ 1`` ``y='y'``
403+
``tvar = time`` ``time='time'``
404+
``gvar = first_treat`` ``first_treat='first_treat'``
405+
``ivar = unit`` ``group='unit'``
406+
``xvar`` (covariate het.) *not yet supported — use* ``controls``
407+
``vcov = ~cluster`` ``cluster='cluster'``
408+
============================ ========================================
409+
410+
For aggregated marginal effects (R ``emfx`` equivalents), combine with
411+
:func:`statspai.did.aggte` on a Callaway–Sant'Anna object, or inspect
412+
``result.model_info['event_study']`` directly.
413+
414+
References
415+
----------
416+
Wooldridge, J.M. (2021). "Two-Way Fixed Effects, the Two-Way Mundlak
417+
Regression, and Difference-in-Differences Estimators."
418+
419+
McDermott, G. (2023). ``etwfe``: Extended Two-Way Fixed Effects.
420+
https://grantmcdermott.com/etwfe/
421+
422+
Examples
423+
--------
424+
>>> import statspai as sp
425+
>>> df = sp.dgp_did(n_units=200, n_periods=10, staggered=True)
426+
>>> res = sp.etwfe(df, y='y', group='unit',
427+
... time='period', first_treat='first_treat')
428+
>>> res.summary()
429+
430+
See Also
431+
--------
432+
wooldridge_did : Identical estimator; this is a naming alias.
433+
callaway_santanna : CS (2021) group-time ATT estimator.
434+
aggte : Aggregation of group-time ATTs (event/group/calendar/simple).
435+
"""
436+
return wooldridge_did(
437+
data=data,
438+
y=y,
439+
group=group,
440+
time=time,
441+
first_treat=first_treat,
442+
controls=controls,
443+
cluster=cluster,
444+
alpha=alpha,
445+
)
446+
447+
352448
# ═══════════════════════════════════════════════════════════════════════
353449
# 2. Doubly Robust DID — Sant'Anna & Zhao (2020)
354450
# ═══════════════════════════════════════════════════════════════════════

0 commit comments

Comments
 (0)