TWFE, Sun–Abraham estimates, and a joint test of cohort deviations
Published
October 8, 2026
Noack and Costello (2024) study credit markets and property rights in fisheries. This example uses their prepared stock–country panel to estimate the effect of property-rights adoption on fishing mortality. It compares static TWFE, dynamic TWFE, and Sun–Abraham estimates, then tests whether post-treatment effects differ across adoption cohorts.
1. Source data and variable map
The source is the prepared panel CMPRC-balanced-original.csv. The archive builder, scripts/estimation/papers_group_c.py::commons(), defines its variables.
Show Python
df = pl.read_csv(SOURCE)df.head()
shape: (5, 9)
obs_id
unit
time
assignment
cluster
y
d
weight
cohort
i64
i64
i64
i64
i64
f64
i64
f64
i64
1327
1686
1960
1686
5
1.137982
0
1.0
1967
1328
1686
1961
1686
5
1.137982
0
1.0
1967
1329
1686
1962
1686
5
1.719052
0
1.0
1967
1330
1686
1963
1686
5
1.078451
0
1.0
1967
1331
1686
1964
1686
5
3.163037
0
1.0
1967
Role
Archived variable
Meaning and handling
Outcome, Y
y
One outcome: asinh(ffmsy), already computed by the archive builder as log(ffmsy + sqrt(1 + ffmsy**2)). ffmsy measures fishing mortality relative to its maximum-sustainable-yield benchmark. No further rescaling is applied.
Treatment, W
d
Binary, cardinality two: 0 and 1. The builder assigns EEZ declaration time to units with max(pr) > 0.5, then sets d = 1 from that time onward. The underlying continuous pr measure is not in this prepared file.
Unit, i
unit
Integer code for a stock–country pair; assignment duplicates this identifier.
Period, t
time
Calendar year. There are no gaps in the annual time grid.
Covariates, X_it
None
The prepared specification is unconditional apart from fixed effects.
Inference
cluster
Country identifier. Country effects are absorbed by stock–country unit effects, but uncertainty is clustered at country level.
Weights
weight
Always 1: estimation is unweighted.
Timing/provenance
cohort, obs_id
Supplied adoption year (cohort = 0 for never-treated) and source-row identifier. Neither is an additional treatment variant.
For each unit, adoption time g is the minimum observed year with d = 1. Never-treated units receive PyFixest’s sentinel g = 0; their event time remains missing. On the annual grid, relative time is simply time - g.
Common estimation sample: 38,319 observations, 723 stock–country units, 47 countries; 529 eventually treated units in 15 adoption cohorts and 194 never-treated units. The 21 always-treated units (1,113 observations) are excluded from all models. Their supplied adoption years are 1950/1952, before the panel begins; the within-panel minimum would incorrectly suggest 1960. There are no treatment reversals or additional missing-value exclusions.
Figure 1: Purple = untreated; yellow = treated. A seeded sample of 100 eligible stock–country units, sorted by adoption timing. The regressions use all 723 eligible units.
Subsampling applies only to this plot: 100 units without replacement, seed 20261008. It does not change the estimation sample, cohort weights, or covariance calculations.
3. Static TWFE and dynamic specifications
Static two-way fixed effects
This benchmark regresses y on d with stock–country and calendar-year effects. CRV1 standard errors are clustered by country. Under heterogeneous effects with staggered adoption, the coefficient need not equal a convex average ATT.
Show Python
static = pf.feols("y ~ d | unit + time", data=data, vcov={"CRV1": "cluster"})table(static.tidy().reset_index())
Coefficient
Estimate
Std. Error
t value
Pr(>
t
)
d
-0.60473
0.092302
-6.5516
4.2968e-08
-0.79052
-0.41893
Dynamic TWFE
The dynamic specification includes relative-time indicators interacted with ever-treated status, with year −1 omitted.
The saturated model estimates cohort-by-relative-time effects with year −1 omitted for each cohort. .aggregate(agg="period", weighting="shares") returns event-time effects weighted by each contributing cohort’s observation share. Never-treated observations anchor the comparison.
Figure 2: Dynamic TWFE and share-weighted Sun–Abraham estimates. Error bars are pointwise 95% normal intervals using country-clustered covariance; not simultaneous bands. The displayed window is −20 to +20, but both models were fitted on all observed event times.
Aggregate post-treatment effects
The scalar ATT averages post-treatment effects using treated-unit-year shares. Its standard error uses the full coefficient covariance matrix. Dynamic TWFE uses the same post-period weights.
These scalar intervals use a normal critical value for comparability with the event-time aggregation; the earlier native static output uses PyFixest’s finite-cluster t reference. The Sun–Abraham scalar weights cohort/event-time cells in proportion to treated observations. Earlier cohorts contribute more post-treatment years; this is not an equal-cohort or common-horizon average.
4. Joint F-test of cohort deviations
The TestingInEventStudies implementation constructs common event-time indicators and interactions for all treated cohorts except the first. The interactions are deviations from the reference cohort’s effect path.
The imported design has 780 columns after absorbing unit and year effects. The restricted regression removes 379 post-treatment deviations. F(379, 36764) = 2.941, p = 5.81e-72.
This is the homoskedastic nested-model F-test, computed from the two residual sums of squares. The coefficient tables above retain country-clustered standard errors.
Download the F-test runner. It imports test_treatment_heterogeneity(..., retmod=True) and uses the resulting deviations design; the source repository is unchanged.
5. Results and reproducibility record
The static TWFE coefficient is -0.605 (cluster SE 0.092); the saturated model’s treated-unit-year-weighted post ATT is -0.683 (cluster SE 0.085). The models use the same observations, but do not impose the same effect structure or assign the same implicit treatment-effect weights.