Skip to content

Commit a0667a4

Browse files
committed
Preserve pyfixest as the IW event-study engine
Implement eventreg as a thin interaction-weighted layer over pyfixest. The first-stage interacted regression and second-stage cohort-share regressions run through pyfixest so native solver, vcov, demeaning backend, fixed-effect, and split behavior stay visible. IW aggregation adds the linear-combination and delta-method pieces needed for Stata/Julia eventstudyinteract parity. Constraint: pyfixest event_study saturated only exposes unit/time fixed effects, so this package keeps explicit rel_varlist plus arbitrary fixest formula fixed effects. Constraint: pyfixest does not expose multivariate cross-equation covariance for share regressions; cross blocks reuse pyfixest bread, scores, and CRV1 meat internals. Rejected: Reimplement high-dimensional demeaning in this package | would lose pyfixest performance and backend choices. Rejected: Claim R/JL vcov equality for the single-cohort saturated fixture | their saturated/small-sample paths differ or are collinear, so the script reports explicit skips. Confidence: high Scope-risk: broad Directive: Keep first-stage and share regressions pyfixest-backed; do not add a separate regression engine without a benchmark and parity reason. Tested: .venv/bin/python -m ruff check . Tested: .venv/bin/python -m ruff format --check . Tested: .venv/bin/python -m pytest (13 passed) Tested: .venv/bin/python -m compileall src tests scripts Tested: .venv/bin/python scripts/check_external_parity.py (Python/R/Stata/Julia coefficient parity; Stata V_iw parity) Not-tested: Large nlswork benchmark parity and GPU/JAX/CuPy backend runtime.
1 parent eb0c99b commit a0667a4

18 files changed

Lines changed: 2049 additions & 70 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
strategy:
1414
fail-fast: false
1515
matrix:
16-
python-version: ["3.9", "3.10", "3.11", "3.12"]
16+
python-version: ["3.10", "3.11", "3.12"]
1717

1818
steps:
1919
- uses: actions/checkout@v4

README.md

Lines changed: 78 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,92 @@
11
# EventStudyInteracts.py
22

3-
Python project scaffold for a `pyfixest`-backed implementation of the
4-
Sun and Abraham interaction-weighted event-study estimator.
5-
6-
This repository is currently in the planning/scaffold stage. The intended
7-
package import name is `eventstudyinteracts`, and the primary public function
8-
will be `eventreg(...)`, mirroring the workflow of
9-
[`EventStudyInteracts.jl`](https://github.com/xiaobaaaa/EventStudyInteracts.jl)
10-
while preserving the native features of
3+
`EventStudyInteracts.py` is a Python implementation of the Sun and Abraham
4+
interaction-weighted event-study estimator, built as a thin layer over
115
[`pyfixest`](https://github.com/py-econometrics/pyfixest).
126

13-
## Design Goal
7+
The estimator keeps `pyfixest.feols()` as the regression engine. First-stage
8+
coefficients and covariance matrices come from pyfixest, including its fast
9+
fixed-effect demeaning backends and solver choices. The second-stage cohort-share
10+
regressions are also fit with pyfixest; the package only adds the
11+
interaction-weighted aggregation and the cross-equation delta-method covariance
12+
that pyfixest does not expose directly.
1413

15-
The package should feel like a thin, Python-native layer over `pyfixest`, not a
16-
separate regression engine. In particular, the implementation plan keeps
17-
`pyfixest.feols()` as the estimation backend and forwards important backend
18-
options such as:
14+
## Installation
1915

20-
- `vcov`, `vcov_kwargs`, `ssc`, and cluster-robust inference settings
21-
- `weights` and `weights_type`
22-
- `fixef_rm`, `fixef_tol`, `fixef_maxiter`, and `collin_tol`
16+
```bash
17+
python -m pip install -e ".[dev]"
18+
```
19+
20+
The package requires Python 3.10+ and `pyfixest>=0.50.1`.
21+
22+
## Basic Usage
23+
24+
```python
25+
from eventstudyinteracts import eventreg
26+
27+
fit = eventreg(
28+
"ln_wage ~ age + tenure | idcode + year",
29+
data=df,
30+
rel_varlist=["g_4", "g_3", "g_2", "g0", "g1", "g2", "g3"],
31+
cohort="first_treat",
32+
control_cohort="never_treat",
33+
vcov={"CRV1": "idcode"},
34+
demeaner_backend="rust-cg",
35+
)
36+
37+
fit.coef()
38+
fit.se()
39+
fit.vcov()
40+
fit.tidy()
41+
```
42+
43+
`control_cohort` is a boolean column identifying never-treated/control rows.
44+
`rel_varlist` should contain the relative-time indicator columns to aggregate,
45+
in the output order you want.
46+
47+
## pyfixest Passthrough
48+
49+
`eventreg(...)` mirrors pyfixest naming where possible and forwards the important
50+
performance and inference options:
51+
52+
- `vcov`, `vcov_kwargs`, `ssc`
53+
- `weights`, `weights_type`
54+
- `fixef_rm`, `fixef_tol`, `fixef_maxiter`, `collin_tol`
2355
- `solver`
24-
- `demeaner_backend` with `numba`, `rust`, `jax`, `cupy`, `cupy32`, `cupy64`,
25-
and `scipy`
26-
- `copy_data`, `store_data`, `lean`, and `use_compression`
56+
- `demeaner_backend`, including `numba`, `rust`, `rust-cg`, `jax`, `cupy`,
57+
`cupy32`, `cupy64`, and `scipy`
58+
- `copy_data`, `store_data`, `lean`, `use_compression`
2759
- `split` and `fsplit`
2860

29-
## Current Layout
61+
The IW share covariance currently supports the EventStudyInteracts.jl parity
62+
modes: `iid`/`simple`, `hetero`/`HC1`, and one- or two-way `{"CRV1": ...}`.
3063

31-
- `src/eventstudyinteracts/`: future package source
64+
## Project Layout
65+
66+
- `src/eventstudyinteracts/`: package source
67+
- `tests/`: unit and estimator regression tests
68+
- `scripts/check_external_parity.py`: optional local parity harness for Python,
69+
R fixest, Stata `eventstudyinteract`, and Julia `EventStudyInteracts.jl`
3270
- `docs/requirements.md`: detailed requirements
33-
- `docs/design/api.md`: proposed Python API and result object design
34-
- `docs/design/upstream-analysis.md`: notes from `EventStudyInteracts.jl` and
35-
`pyfixest`
71+
- `docs/design/api.md`: API and result object design
72+
- `docs/design/upstream-analysis.md`: upstream implementation notes
3673
- `docs/implementation-plan.md`: milestone plan
37-
- `docs/workflows/`: development, maintenance, GitHub, and release workflows
38-
- `tests/`: test suite placeholder
39-
- `references/`: external parity baselines
40-
- `benchmarks/`: performance scripts and outputs
41-
- `.github/workflows/ci.yml`: initial CI skeleton
74+
- `.github/workflows/ci.yml`: lint, format, test, and compile checks
75+
76+
## Verification
77+
78+
Current local checks:
4279

43-
## Status
80+
```bash
81+
python -m ruff check src tests scripts
82+
python -m ruff format --check src tests scripts
83+
python -m pytest
84+
python -m compileall src tests
85+
python scripts/check_external_parity.py
86+
```
4487

45-
No estimator implementation is claimed yet. The next implementation step is to
46-
add `eventreg(...)` with tests that first lock the single-cohort equivalence
47-
case before implementing the full multi-cohort IW covariance.
88+
The external parity script compares coefficients against R `fixest`, Stata
89+
`eventstudyinteract`, and Julia `EventStudyInteracts.jl` when those tools are
90+
available. It also compares a single-cohort IW covariance matrix against Stata;
91+
Julia/R saturated paths use different small-sample conventions in that fixture
92+
and are reported explicitly as skipped for vcov.

docs/design/api.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ def eventreg(
3939
weights: str | None = None,
4040
ssc: dict[str, str | bool] | None = None,
4141
fixef_rm: Literal["singleton", "none"] = "singleton",
42-
fixef_tol: float = 1e-8,
43-
fixef_maxiter: int = 100_000,
42+
fixef_tol: float = 1e-6,
43+
fixef_maxiter: int = 10_000,
4444
collin_tol: float = 1e-9,
4545
drop_intercept: bool = False,
4646
copy_data: bool = True,
@@ -95,7 +95,7 @@ fit.interacted_model_
9595
fit.stage2_models_
9696
fit.esample_
9797
fit.demeaner_backend_
98-
fit.vcov_type_
98+
fit.vcov_spec
9999
```
100100

101101
### 3.3 `tidy()` 输出
@@ -140,7 +140,22 @@ y ~ __esi_n_g_2_x_1980 + __esi_n_g_2_x_1981 + ... + x1 + x2 | id + year
140140
- 公式拆分应使用稳健 parser 或严格的 formula section splitter,不能只做
141141
脆弱字符串拼接。
142142

143-
## 5. Split / Fsplit
143+
## 5. Stage 2 与 pyfixest
144+
145+
第二阶段 cohort-share 回归使用 `pyfixest.feols` 的多因变量公式:
146+
147+
```text
148+
share_1980 + share_1981 + ... ~ 0 + n_rel0 + n_rel1 + ...
149+
```
150+
151+
对角协方差块直接来自每个 pyfixest share 模型的 `_vcov`。由于 pyfixest 当前不
152+
公开多方程交叉协方差,包装层只使用 `_bread``_scores` 和 CRV1 meat 函数拼接
153+
delta-method 需要的 cross-equation blocks。
154+
如果 pyfixest 模型暴露了额外系数(例如旧版本或公式差异导致的 intercept),
155+
share covariance 会先按 `rel_varlist` 对应的内部列名做子集和重排,避免聚合块
156+
错位。
157+
158+
## 6. Split / Fsplit
144159

145160
`split``fsplit` 存在时,`eventreg(...)` 应返回
146161
`EventStudyInteractMulti`
@@ -152,10 +167,9 @@ fits["east"].tidy()
152167
fits.summary()
153168
```
154169

155-
初版可以先实现单模型路径,但必须在检测到 `split`/`fsplit` 时明确报错,并在
156-
计划中将其列为保留 pyfixest 特性的必做项。
170+
当前实现会逐组调用 `eventreg(...)`,并用 `"all"` 键保存 `fsplit` 的全样本结果。
157171

158-
## 6. 与 pyfixest 保持一致的错误策略
172+
## 7. 与 pyfixest 保持一致的错误策略
159173

160174
- 后端参数非法时,让 `pyfixest` 自身报错。
161175
- 本项目只在 IW 语义层报错,如缺少 `rel_varlist` 列、没有 treated cohort、

docs/design/upstream-analysis.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Julia 版 `eventfit.jl` 的关键路径:
7373

7474
- GitHub: <https://github.com/py-econometrics/pyfixest>
7575
- Docs: <https://pyfixest.org/>
76-
- PyPI stable package inspected: `pyfixest==0.40.1`
76+
- Local package inspected: `pyfixest==0.50.1`
7777

7878
### 2.1 `feols` 是本项目核心后端
7979

@@ -88,8 +88,8 @@ feols(
8888
weights=None,
8989
ssc=None,
9090
fixef_rm="singleton",
91-
fixef_tol=1e-08,
92-
fixef_maxiter=100_000,
91+
fixef_tol=1e-06,
92+
fixef_maxiter=10_000,
9393
collin_tol=1e-09,
9494
drop_intercept=False,
9595
copy_data=True,
@@ -113,6 +113,7 @@ feols(
113113

114114
- `numba`:默认 CPU alternating projections。
115115
- `rust`:Rust 实现的 CPU alternating projections。
116+
- `rust-cg`:Rust conjugate-gradient-schwarz 后端,适合困难稀疏固定效应。
116117
- `jax`:JAX CPU/GPU alternating projections,需要 `jax`/`jaxlib`
117118
- `cupy` / `cupy64`:CuPy GPU float64 FWL sparse backend。
118119
- `cupy32`:CuPy GPU float32 FWL sparse backend。
@@ -129,12 +130,20 @@ feols(
129130
- `estimator="did2s"`
130131
- `estimator="saturated"`
131132

133+
官方 reference 中 `event_study``estimator` 参数列出 `"did2s"`
134+
`"twfe"``"saturated"`,并示例调用 `fit_twfe_saturated.aggregate()`
135+
本地 `pyfixest.did.saturated_twfe.SaturatedEventStudy.aggregate()` 的核心做法是
136+
构造线性组合向量 `R`,再用 `R @ model._vcov @ R` 计算聚合标准误。
137+
132138
但该接口按 `yname/idname/tname/gname` 组织,不是
133139
`rel_varlist + cohort + control_cohort + formula` 的 Stata/Julia 工作流。因此:
134140

135141
- 本项目应提供显式 `rel_varlist` 工作流。
136142
- 后续可以用 `pyfixest.event_study(..., estimator="saturated")` 做 sanity check,
137143
但不能直接替代 `eventreg(...)`
144+
- 当前 Python 实现已保存 `aggregation_matrix_`,即与 saturated `aggregate()` 同类
145+
的线性组合矩阵;第一阶段聚合协方差走 `R @ V @ R.T`,再叠加 Stata/Julia
146+
`eventstudyinteract` 要求的 cohort-share 不确定性。
138147

139148
## 3. 初步风险
140149

docs/implementation-plan.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Phase 0: 项目骨架
44

5-
状态:已开始
5+
状态:已完成
66

77
交付物:
88

@@ -16,10 +16,12 @@
1616

1717
- `python -m compileall src` 通过。
1818
- `pytest` 最小导入测试通过。
19-
- 文档清楚标注尚未实现 estimator
19+
- 文档清楚标注已实现范围和剩余外部 parity 缺口
2020

2121
## Phase 1: API 与结果对象骨架
2222

23+
状态:已完成。
24+
2325
任务:
2426

2527
- 新增 `eventstudyinteracts.api.eventreg`
@@ -34,6 +36,8 @@
3436

3537
## Phase 2: 数据准备与第一阶段 pyfixest 回归
3638

39+
状态:已完成。
40+
3741
任务:
3842

3943
- 实现安全临时列前缀。
@@ -50,6 +54,8 @@
5054

5155
## Phase 3: 单 cohort 等价测试
5256

57+
状态:已完成基础路径。
58+
5359
任务:
5460

5561
- 构造 deterministic panel fixture。
@@ -65,10 +71,12 @@
6571

6672
## Phase 4: 多 cohort IW 权重与协方差
6773

74+
状态:已完成 MVP。
75+
6876
任务:
6977

7078
- 实现 treated sample cohort-share 估计。
71-
- 实现 share 协方差:
79+
- 实现 pyfixest-backed share 协方差:
7280
- iid
7381
- hetero/HC1
7482
- one-way/two-way CRV1
@@ -83,13 +91,15 @@
8391

8492
## Phase 5: pyfixest 特性完整化
8593

94+
状态:部分完成。
95+
8696
任务:
8797

88-
- `split`/`fsplit` 返回 `EventStudyInteractMulti`
98+
- `split`/`fsplit` 返回 `EventStudyInteractMulti`已完成。
8999
- `use_compression=True` 路径验证。
90100
- `lean``store_data``copy_data` 行为验证。
91101
- `demeaner_backend` 覆盖测试:
92-
- 必测:`numba``rust``scipy`
102+
- 必测:`numba``rust-cg`/`rust``scipy`
93103
- 可选环境:`jax``cupy*`
94104

95105
验收标准:
@@ -99,11 +109,16 @@
99109

100110
## Phase 6: 外部基准与文档示例
101111

112+
状态:进行中。
113+
102114
任务:
103115

104116
-`EventStudyInteracts.jl``nlswork` 数据/结果转成 Python reference。
105117
- 增加 `references/` 中的 baseline 文件。
106-
- 编写 README usage 示例。
118+
- 编写 README usage 示例。已完成基础示例。
119+
- 增加 `scripts/check_external_parity.py`。已完成 Python fixture、R fixest
120+
Sun-Abraham parity、Stata `eventstudyinteract` parity、Julia
121+
`EventStudyInteracts.jl` parity。
107122
- 增加 `examples/` 脚本或 notebook。
108123

109124
验收标准:

docs/requirements.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ interaction-weighted (IW) 估计流程,但底层回归、固定效应吸收、
2121
`method``nthreads``tol``maxiter``weights``drop_singletons`
2222
后端参数透传。
2323
- Python 版应以 `pyfixest.feols(...)` 为唯一高维固定效应回归后端。
24-
- `pyfixest` 当前稳定版为 `0.40.1``feols` 暴露了 `demeaner_backend`
25-
`numba``rust``jax``cupy``cupy32``cupy64``scipy`
24+
- 本项目当前以 `pyfixest>=0.50.1` 为基线,`feols` 暴露了
25+
`demeaner_backend``numba``rust``rust-cg``jax``cupy`
26+
`cupy32``cupy64``scipy`
2627
- `pyfixest` 已有 `event_study(..., estimator="saturated")`,但它不是
2728
Stata/Julia `eventstudyinteract` 风格的显式 `rel_varlist` 工作流。本项目
2829
不替代它,而是补足更贴近原 Stata/Jl 包的接口。
@@ -46,8 +47,8 @@ eventreg(
4647
weights=None,
4748
ssc=None,
4849
fixef_rm="singleton",
49-
fixef_tol=1e-8,
50-
fixef_maxiter=100_000,
50+
fixef_tol=1e-6,
51+
fixef_maxiter=10_000,
5152
collin_tol=1e-9,
5253
drop_intercept=False,
5354
copy_data=True,
@@ -71,7 +72,8 @@ eventreg(
7172
`"ln_wage ~ controls | idcode + year"`
7273
- `rel_varlist` 是已在数据中生成好的相对时间 dummy 列名列表,和 Julia 版
7374
`rel_varlist::Vector{Symbol}` 对齐。
74-
- `cohort` 是首次处理时间 cohort 列;从未处理样本应为缺失值。
75+
- `cohort` 是首次处理时间 cohort 列;控制组的 cohort 值不会进入 treated
76+
cohort 列表。
7577
- `control_cohort` 是控制组二元列,可以表示 never-treated 或 last-treated
7678
控制组。
7779
- 所有 `pyfixest.feols` 后端参数必须透传,不能被本包装层吞掉。
@@ -112,7 +114,8 @@ eventreg(
112114
- 收敛控制:`fixef_tol``fixef_maxiter` 必须透传。
113115
- 固定效应样本处理:`fixef_rm` 必须透传。
114116
- 推断:`vcov``vcov_kwargs``ssc` 必须透传至第一阶段;第二阶段
115-
cohort-share 协方差应实现与主 `vcov` 兼容的路径。
117+
cohort-share 回归也由 `pyfixest.feols` 估计,IW 包装层只补 pyfixest 未暴露的
118+
cross-equation delta-method 协方差。
116119
- 权重:`weights``weights_type` 必须透传;第二阶段 share 估计必须明确处理
117120
加权样本。
118121
- 性能:`solver``use_compression``copy_data``store_data``lean` 必须可用。

0 commit comments

Comments
 (0)