Mincer returns, PyFixest, T-Rex GMM, and many-instrument asymptotics
Author
Krabbs
Published
August 11, 2026
Question
Suppose a Mincer wage equation has one credible excluded instrument: the Card (1993) indicator for growing up near a four-year college. Can we manufacture more identifying information by interacting that one instrument with a growing collection of baseline covariates?
where only \(Z_i\) has a nonzero population first-stage coefficient. The interactions are valid under the deliberately favorable simulation assumptions, but they are synthetic moments, not new sources of exogenous variation. Each irrelevant interaction can fit a little endogenous first-stage noise in finite samples. When \(K\) grows in proportion to \(n\), those small overfits accumulate and conventional 2SLS drifts toward OLS.
The exercise uses PyFixest for formula-based IV estimation and Apoorva Lal’s T-Rex GMM library for the same linear moment problem.
Ability \(A_i\) is unobserved and enters \(u_i=0.20A_i+\varepsilon_i\), so schooling is endogenous. Proximity \(Z_i\) is independent of \((A_i,v_i,\varepsilon_i,W_i)\) and affects wages only through schooling. The auxiliary covariates \(W_{ij}\) are independent standard normals. Consequently every \(Z_iW_{ij}\) is a valid instrument, but its population first-stage coefficient is zero.
Show Python
import platformimport warningsimport matplotlib.pyplot as pltimport numpy as npimport pandas as pdimport pyfixest as pfimport scipyimport torchfrom trex.gmm import GMMEstimatorwarnings.filterwarnings("ignore", category=FutureWarning)plt.style.use("seaborn-v0_8-whitegrid")BETA =0.08versions = pd.DataFrame( {"package": ["Python", "NumPy", "SciPy", "PyFixest", "PyTorch"],"version": [ platform.python_version(), np.__version__, scipy.__version__, pf.__version__, torch.__version__, ], })versions
package
version
0
Python
3.12.9
1
NumPy
2.5.2
2
SciPy
1.18.0
3
PyFixest
0.60.0
4
PyTorch
2.13.0
Show Python
def make_mincer(n=2_000, k_fake=20, seed=93_0811): rng = np.random.default_rng(seed) ability = rng.normal(size=n) near = rng.binomial(1, 0.45, size=n) experience = rng.uniform(5, 35, size=n) exp_c = (experience -20) /10 W = rng.normal(size=(n, k_fake)) schooling = (12+0.75* near+0.80* ability+0.15* exp_c+ rng.normal(scale=1.20, size=n) ) log_wage = (1.5+ BETA * schooling+0.35* exp_c-0.06* exp_c**2+0.20* ability+ rng.normal(scale=0.35, size=n) ) data = pd.DataFrame( {"log_wage": log_wage,"schooling": schooling,"exp_c": exp_c,"exp2": exp_c**2,"near_college": near, } )for j inrange(k_fake):# Centering W makes the base instrument and interactions nearly orthogonal. data[f"zW{j +1}"] = near * (W[:, j] - W[:, j].mean())return datad = make_mincer()d.head()
log_wage
schooling
exp_c
exp2
near_college
zW1
zW2
zW3
zW4
zW5
...
zW11
zW12
zW13
zW14
zW15
zW16
zW17
zW18
zW19
zW20
0
1.859943
10.097746
0.580055
0.336464
1
1.242903
-2.701076
0.064349
0.320079
0.668261
...
-0.210513
-0.770781
-1.463736
0.915141
0.690822
0.016899
1.372583
-0.251429
0.348562
-0.248535
1
2.201412
10.711891
-0.062175
0.003866
1
-0.054603
0.006098
-1.015384
1.209893
1.932296
...
0.204189
1.180709
-0.884827
-0.335250
-1.737977
-0.159704
-0.138648
-1.177560
0.232402
-1.243181
2
2.617197
15.381796
-1.141093
1.302094
0
0.000000
-0.000000
0.000000
0.000000
-0.000000
...
-0.000000
0.000000
0.000000
-0.000000
0.000000
-0.000000
-0.000000
0.000000
0.000000
-0.000000
3
1.803065
12.867565
-1.345927
1.811519
1
0.184060
0.692842
-0.214432
-0.071429
-0.636782
...
1.187301
0.397638
1.636067
-0.607905
-1.143297
-0.311634
0.343588
-0.739469
0.369532
-0.594297
4
3.071465
14.821328
0.113232
0.012821
0
0.000000
-0.000000
-0.000000
0.000000
0.000000
...
0.000000
0.000000
-0.000000
-0.000000
-0.000000
-0.000000
-0.000000
0.000000
0.000000
0.000000
5 rows × 25 columns
The instrument list is nested: the one-instrument specification uses only near_college; the expanded specification adds 20 interactions. PyFixest automatically includes the exogenous Mincer controls as their own instruments.
Calling the interactions “fake” does not mean invalid here. It means they contain no population first-stage signal beyond the solitary instrument. Their sample first-stage coefficients are noise.
T-Rex: the same IV problem as GMM
T-Rex accepts moment functions directly. For linear IV,
\[
g_i(\theta)=Z_i^*(Y_i-X_i'\theta).
\]
We whiten the instrument matrix so that identity-weighted GMM uses the 2SLS weight \((Z'Z/n)^{-1}\). This makes the T-Rex objective numerically equivalent to 2SLS rather than to an arbitrarily scaled identity-weighted criterion.
Show Python
def iv_moment(z, y, x, beta):return z * (y - x @ beta)[:, None]X = np.column_stack( [np.ones(len(d)), d["schooling"], d["exp_c"], d["exp2"]])Z = np.column_stack( [np.ones(len(d)), d["exp_c"], d["exp2"], d["near_college"], d[fake_names]])y = d["log_wage"].to_numpy()# If Q = Z'Z/n and W = Q^{-1} = LL', then Z* = ZL gives# g*(theta)'g*(theta) = g(theta)'Wg(theta).Q = Z.T @ Z /len(Z)L = np.linalg.cholesky(np.linalg.inv(Q))Z_white = Z @ Ltrex_iv = GMMEstimator( iv_moment, weighting_matrix="identity", backend="scipy",)np.random.seed(930811) # T-Rex currently initializes the optimizer randomly.trex_iv.fit(Z_white, y, X, two_step=False, fit_method="BFGS")trex_comparison = pd.DataFrame( {"implementation": ["PyFixest 2SLS", "T-Rex identity-GMM on whitened moments"],"schooling_return": [pick(iv_many, "schooling")[0], trex_iv.theta_[1]], })trex_comparison["difference"] = ( trex_comparison["schooling_return"] - trex_comparison["schooling_return"].iloc[0])trex_comparison.round(7)
implementation
schooling_return
difference
0
PyFixest 2SLS
0.104772
0.000000
1
T-Rex identity-GMM on whitened moments
0.104739
-0.000033
The two implementations solve the same sample moment problem. PyFixest is the convenient regression interface; T-Rex exposes the GMM criterion and makes clear that every added interaction is another sample moment.
A fast 2SLS kernel for the Monte Carlo
Repeated formula parsing and numerical GMM optimization would obscure the asymptotic exercise. The Monte Carlo therefore uses the closed-form 2SLS solution. The preceding cross-check verifies that the kernel targets the same estimator.
Show Python
def residualize(a, controls):return a - controls @ np.linalg.lstsq(controls, a, rcond=None)[0]def one_replication(n, k_fake, seed): rng = np.random.default_rng(seed) ability = rng.normal(size=n) near = rng.binomial(1, 0.45, size=n) experience = rng.uniform(5, 35, size=n) exp_c = (experience -20) /10 controls = np.column_stack([np.ones(n), exp_c, exp_c**2]) W = rng.normal(size=(n, k_fake)) schooling = (12+0.75* near +0.80* ability +0.15* exp_c+ rng.normal(scale=1.20, size=n) ) wage = (1.5+ BETA * schooling +0.35* exp_c -0.06* exp_c**2+0.20* ability + rng.normal(scale=0.35, size=n) ) s = residualize(schooling, controls) y_resid = residualize(wage, controls) instruments = np.column_stack([near, near[:, None] * W]) instruments = residualize(instruments, controls) s_hat = instruments @ np.linalg.lstsq(instruments, s, rcond=None)[0] iv = (s_hat @ y_resid) / (s_hat @ s) ols_value = (s @ y_resid) / (s @ s)return ols_value, iv# Closed-form parity in the displayed sample.def closed_form_on_frame(frame, fake_names): C = np.column_stack([np.ones(len(frame)), frame["exp_c"], frame["exp2"]]) s = residualize(frame["schooling"].to_numpy(), C) y_resid = residualize(frame["log_wage"].to_numpy(), C) z = residualize(frame[["near_college", *fake_names]].to_numpy(), C) s_hat = z @ np.linalg.lstsq(z, s, rcond=None)[0]return (s_hat @ y_resid) / (s_hat @ s)pd.DataFrame( {"implementation": ["PyFixest", "closed-form kernel"],"estimate": [ pick(iv_many, "schooling")[0], closed_form_on_frame(d, fake_names), ], }).round(8)
implementation
estimate
0
PyFixest
0.104772
1
closed-form kernel
0.104772
Experiment 1: adding interactions at fixed \(n\)
At \(n=1{,}000\), add up to 200 irrelevant-but-valid interactions. Each point averages 150 replications.
fig, axes = plt.subplots(1, 2, figsize=(11, 5))colors = {"Fixed K = 10": "#0072B2", "Many IV: K/n = 0.10": "#D55E00"}for regime, group in asymptotics.groupby("regime"): group = group.sort_values("n") axes[0].plot( group["n"], group["iv_mean"], "o-", lw=2, color=colors[regime], label=regime, ) axes[1].plot( group["n"], group["sqrt_n_bias"], "o-", lw=2, color=colors[regime], label=regime, )axes[0].axhline(BETA, color="black", ls=":", lw=2, label="Truth")axes[0].set( xlabel="Sample size, n", ylabel="Mean 2SLS estimate", title="Consistency depends on K/n",)axes[1].axhline(0, color="black", ls=":", lw=2)axes[1].set( xlabel="Sample size, n", ylabel=r"$\sqrt{n}\,(E[\hat\beta]-\beta)$", title="Root-n centering fails in the many-IV sequence",)axes[0].legend(frameon=True)fig.tight_layout()plt.show()
Interpretation
Moment validity is not instrument strength. Under the simulation’s strong conditional independence assumptions, the interacted moments are valid. Their population first-stage coefficients are nevertheless zero.
With fixed \(K\), the nuisance disappears asymptotically. The true instrument’s concentration parameter grows with \(n\), while the finite collection of spurious sample correlations becomes negligible. The 2SLS mean returns toward 0.08.
With \(K/n\to\alpha>0\), overfitting does not disappear. The projection matrix spends a non-vanishing fraction of the sample degrees of freedom fitting endogenous schooling noise. Conventional 2SLS retains bias toward OLS, and \(\sqrt n(\widehat\beta-\beta)\) is not centered at zero.
Interactions require stronger identifying assumptions.\(E[Z_iu_i]=0\) alone does not imply \(E[Z_iW_{ij}u_i]=0\). The expanded moments need a conditional restriction such as \(E[u_i\mid Z_i,W_i]=E[u_i\mid W_i]\), plus correctly handled covariate main effects. This notebook grants those assumptions so that many-instrument distortion is isolated from outright invalidity.
A larger first-stage \(F\) is not automatically better evidence. Adding many columns can increase in-sample fit mechanically. The economically credible source of variation remains college proximity.
The practical response is not to forbid interactions. It is to state the conditional exclusion argument, limit or regularize the instrument basis, report how results change with \(K\), and use methods designed for many or weak instruments—such as LIML, Fuller corrections, jackknife IV, or identification-robust inference—when the instrument count is not small relative to the sample.
References
Bekker, Paul A. 1994. “Alternative Approximations to the Distributions of Instrumental Variable Estimators.” Econometrica 62 (3): 657–681.
Bound, John, David A. Jaeger, and Regina M. Baker. 1995. “Problems with Instrumental Variables Estimation When the Correlation Between the Instruments and the Endogenous Explanatory Variable Is Weak.” Journal of the American Statistical Association 90 (430): 443–450.
Card, David. 1993. “Using Geographic Variation in College Proximity to Estimate the Return to Schooling.” NBER Working Paper 4483. https://doi.org/10.3386/w4483
Hansen, Christian, Jerry Hausman, and Whitney Newey. 2008. “Estimation with Many Instrumental Variables.” Journal of Business & Economic Statistics 26 (4): 398–422.