@@ -153,14 +153,69 @@ Each estimator in diff-diff should be periodically reviewed to ensure:
153153| Module | ` twfe.py ` |
154154| Primary Reference | Wooldridge (2010), Ch. 10 |
155155| R Reference | ` fixest::feols() ` |
156- | Status | Not Started |
157- | Last Review | - |
156+ | Status | ** Complete** |
157+ | Last Review | 2026-02-08 |
158+
159+ ** Verified Components:**
160+ - [x] Within-transformation algebra: ` y_it - ȳ_i - ȳ_t + ȳ ` matches hand calculation (rtol=1e-12)
161+ - [x] ATT matches manual demeaned OLS (rtol=1e-10)
162+ - [x] ATT matches ` DifferenceInDifferences ` on 2-period data (rtol=1e-10)
163+ - [x] Covariates are also within-transformed (sum to zero within unit/time groups)
164+ - [x] R comparison: ATT matches ` fixest::feols(y ~ treated:post | unit + post, cluster=~unit) ` (rtol<0.1%)
165+ - [x] R comparison: Cluster-robust SE match (rtol<1%)
166+ - [x] R comparison: P-value match (atol<0.01)
167+ - [x] R comparison: CI bounds match (rtol<1%)
168+ - [x] R comparison: ATT and SE match with covariate (same tolerances)
169+ - [x] Edge case: Staggered treatment triggers ` UserWarning `
170+ - [x] Edge case: Auto-clusters at unit level (SE matches explicit ` cluster="unit" ` )
171+ - [x] Edge case: DF adjustment for absorbed FE matches manual ` solve_ols() ` with ` df_adjustment `
172+ - [x] Edge case: Covariate collinear with interaction raises ` ValueError ` ("cannot be identified")
173+ - [x] Edge case: Covariate collinearity warns but ATT remains finite
174+ - [x] Edge case: ` rank_deficient_action="error" ` raises ` ValueError `
175+ - [x] Edge case: ` rank_deficient_action="silent" ` emits no warnings
176+ - [x] Edge case: Unbalanced panel produces valid results (finite ATT, positive SE)
177+ - [x] Edge case: Missing unit column raises ` ValueError `
178+ - [x] Integration: ` decompose() ` returns ` BaconDecompositionResults `
179+ - [x] SE: Cluster-robust SE >= HC1 SE
180+ - [x] SE: VCoV positive semi-definite
181+ - [x] Wild bootstrap: Valid inference (finite SE, p-value in [ 0,1] )
182+ - [x] Wild bootstrap: All weight types (rademacher, mammen, webb) produce valid inference
183+ - [x] Wild bootstrap: ` inference="wild_bootstrap" ` routes correctly
184+ - [x] Params: ` get_params() ` returns all inherited parameters
185+ - [x] Params: ` set_params() ` modifies attributes
186+ - [x] Results: ` summary() ` contains "ATT"
187+ - [x] Results: ` to_dict() ` contains att, se, t_stat, p_value, n_obs
188+ - [x] Results: residuals + fitted = demeaned outcome (not raw)
189+ - [x] Edge case: Multi-period time emits UserWarning advising binary post indicator
190+ - [x] Edge case: Non-{0,1} binary time emits UserWarning (ATT still correct)
191+ - [x] Edge case: ATT invariant to time encoding ({0,1} vs {2020,2021} produces identical results)
192+
193+ ** Key Implementation Detail:**
194+ The interaction term ` D_i × Post_t ` must be within-transformed (demeaned) alongside the outcome,
195+ consistent with the Frisch-Waugh-Lovell (FWL) theorem: all regressors and the outcome must be
196+ projected out of the fixed effects space. R's ` fixest::feols() ` does this automatically when
197+ variables appear to the left of the ` | ` separator.
158198
159199** Corrections Made:**
160- - (None yet)
200+ - ** Bug fix: interaction term must be within-transformed** (found during review). The previous
201+ implementation used raw (un-demeaned) ` D_i × Post_t ` in the demeaned regression. This gave
202+ correct results only for 2-period panels where ` post == period ` . For multi-period panels
203+ (e.g., 4 periods with binary ` post ` ), the raw interaction had incorrect correlation with
204+ demeaned Y, producing ATT approximately 1/3 of the true value. Fixed by applying the same
205+ within-transformation to the interaction term before regression. This matches R's
206+ ` fixest::feols() ` behavior. (` twfe.py ` lines 99-113)
161207
162208** Outstanding Concerns:**
163- - (None yet)
209+ - ** Multi-period ` time ` parameter** : Multi-period time values (e.g., 1,2,3,4) produce
210+ ` treated × period_number ` instead of ` treated × post_indicator ` , which is not the standard
211+ D_it treatment indicator. A ` UserWarning ` is emitted when ` time ` has >2 unique values.
212+ For binary time with non-{0,1} values (e.g., {2020, 2021}), the ATT is mathematically
213+ correct (the within-transformation absorbs the scaling), but a warning recommends 0/1
214+ encoding for clarity. Users with multi-period data should create a binary ` post ` column.
215+ - ** Staggered treatment warning** : The warning only fires when ` time ` has >2 unique values
216+ (i.e., actual period numbers). With binary ` time="post" ` , all treated units appear to start
217+ treatment at ` time=1 ` , making staggering undetectable. Users with staggered designs should
218+ use ` decompose() ` or ` CallawaySantAnna ` directly for proper diagnostics.
164219
165220---
166221
0 commit comments