Estimation code for "The Contract Year Phenomenon Revisited" (Atkins & Dikgang, working paper, 2026).
Panel of 3,654 player-seasons, 1,003 MLB hitters, 2015-2025. Two-way fixed effects on xwOBA. The pooled contract-year estimate is indistinguishable from zero; split by career stage, players with five or more years of service are associated with about six points higher xwOBA in contract years while younger players show no comparable response (difference significant at p = .004).
Every table in the paper is produced by this code.
# put the analysis panel at data/Clean_Data.csv (see data/README.md)
Rscript run_all.RBase R only. No packages: no plm, no lfe, no fixest, no tidyverse. Takes about a minute, most of it the 2,000-replicate bootstrap.
Writes to output/:
| File | Contents |
|---|---|
contract_year_analysis.log |
full transcript of the run |
contract_year_clean.csv / .rds |
the constructed panel |
results_ledger.csv |
every estimate, machine readable |
manuscript_facts.csv |
every descriptive figure quoted in the text |
tables_manuscript.tex |
Tables 1-8, ready to \input |
table1-table8, tableA1 |
individual .tex / .csv exports |
The pipeline is the nine scripts in R/, run in order by run_all.R.
contract_year.R is the same code as a single file, generated by
build_single_file.R; the two are verified to produce byte-identical output.
Edit R/ and rebuild rather than patching the single file.
run_all.R orchestrator; set PROJ, VET_CUTOFF, BOOT_B here
contract_year.R whole pipeline in one file (generated)
build_single_file.R builds contract_year.R from R/
R/00_functions.R felm, wald, tost, CP850 reader
R/01_build.R panel construction, data gate
R/02_models.R all estimates
R/03_tables.R machine-readable exports
R/04_verify.R 56-check estimate gate
R/05_table7.R career-stage ladder, bootstrap
R/06_manuscript.R descriptive figures quoted in the text
R/07_manuscript_tables.R Tables 1-8 in the paper's LaTeX style
R/08_endogeneity.R selection into contract-year status
data/ not tracked; see data/README.md
output/ not tracked; generated
xwOBA_it = a_i + g_t + b*ContractYear_it + d1*MLS_it + d2*MLS_it^2 + u_it
Player and season fixed effects, standard errors clustered on player, so b is identified from within-player variation in contract status. The pipeline also runs a lead-lag model overall and by career stage, a three-rung interaction ladder for the career-stage difference, equivalence tests and minimum detectable effects, disaggregation by contract type, survivorship checks, and tests of selection into contract-year status.
R/00_functions.R implements clustered fixed-effects estimation directly,
reproducing Stata's xtreg, fe cluster(). Two things are easy to get wrong:
- K in the small-sample adjustment excludes the absorbed fixed effects.
Including them, which is what
aregdoes, gives standard errors about 17% larger here. Player effects are nested within player clusters, soxtregis the right convention (Cameron & Miller, 2015).felm(..., dfadj = TRUE)switches, and the pipeline reports both. - Tests use df = G - 1, not N - K.
The career-stage difference is also bootstrapped over players, which avoids the degrees-of-freedom question entirely.
An earlier build of this analysis returned the opposite sign on a key coefficient with the correct N and player count, because one service-time value was wrong. Sample size is not a check. The pipeline gates itself three times and will not write tables if a gate fails:
R/01_build.Rchecks means, sums and counts of every constructed variable against known values, and checks that service time never decreases. Runs before any model is fit.R/04_verify.Rchecks 56 coefficients, standard errors, p-values and descriptive counts. It also checks that the fully interacted pooled specification reproduces the split-sample difference, which it does to about 1.6e-14.R/06_manuscript.Rchecks the nine lead-lag cells feeding Table 3 before the table is written.
The build also repairs itself: two extracts of the source file circulate,
differing in one service-time value, and 01_build.R detects and corrects the
bad one rather than stopping, so either input gives the same output and the
run reports which it found.
Not included. It comes from FanGraphs and Cot's Baseball Contracts, neither of
which is mine to redistribute. data/README.md has the sources, the schema,
the sample definition, the service-time encoding and the one known correction,
so the panel can be rebuilt and checked against the fingerprint gate.
@unpublished{atkins_dikgang_2026,
author = {Atkins, Matt and Dikgang, Johane},
title = {The Contract Year Phenomenon Revisited},
note = {Working paper},
year = {2026}
}Code is MIT (see LICENSE), written by Matt Atkins. The paper is joint work and is not distributed here. The data is not mine to license.