Weights and IV¶
Weights¶
weights= names a column (or, in the low-level
interface, takes a Polars expression) of strictly
positive weights; rows with a missing weight are dropped. There are two kinds,
as in pyfixest, and the difference is not cosmetic:
weights_type="aweights"(the default)analytic or precision weights. N stays the number of rows.
weights_type="fweights"frequency weights: each row stands for
widentical observations, so N is the sum of the weights, and every degrees-of-freedom correction changes with it. The fit, N and every vcov equal those of the data with each row repeated.
analytic = feols_stream(fml, data, workdir="scratch", weights="wa")
frequency = feols_stream(fml, data, workdir="scratch", weights="wf",
weights_type="fweights")
model N age_squared se
----------------------------------------------------------------
unweighted 169,833 0.081721 0.013373
aweights (wa) 169,833 0.077746 0.013346
fweights (wf) 423,811 0.080056 0.008126
Frequency weights inflate N to the weight total, which is why their standard errors are smaller: the fit is told there are more observations.
Two-stage least squares¶
An IV formula has four parts:
outcome ~ exogenous | fixed effects | endogenous ~ instruments. In the
simulated data x2 is correlated with an unobservable that also enters y_iv,
so OLS on x2 is biased upward; z1 and z2 are excluded instruments, and the
true coefficient on x2 is 0.2:
iv = feols_stream("y_iv ~ age_squared | worker_id + firm_id | x2 ~ z1 + z2",
data, workdir="scratch", vcov={"CRV1": "worker_id"})
true coefficient on x2: 0.200000
OLS (biased): 0.543949
2SLS (consistent): 0.196701
The result carries its first-stage regressions, each a fitted model in its own
right (iv.first_stage[0].tidy()), and iv.f_stat_1st_stage, the Wald
statistic on the excluded instruments computed under the same vcov as the
model: the usual weak-instrument diagnostic. It is 33,476 here, against a
conventional rule of thumb of 10. Weights and IV combine.
The full script: examples/weights_and_iv.py
"""Weighted least squares and two-stage least squares.
python examples/weights_and_iv.py
"""
from hdfe_stream import feols_stream
from simulated_data import panel_path, workdir
data = panel_path()
FML = "log_earn ~ age_squared + age_cubed | worker_id + firm_id + year"
# ---------------------------------------------------------------------- weights
# weights= names a column. Two kinds, and the difference is not cosmetic:
#
# aweights (default) analytic/precision weights. N stays the number of rows.
# fweights frequency weights: each row stands for `w` identical
# observations, so N is the sum of the weights and every
# degrees-of-freedom correction changes with it.
unweighted = feols_stream(FML, data, workdir=workdir("ols"), verbose=False)
analytic = feols_stream(FML, data, workdir=workdir("aweights"),
weights="wa", verbose=False)
frequency = feols_stream(FML, data, workdir=workdir("fweights"),
weights="wf", weights_type="fweights", verbose=False)
print(f"{'model':<24}{'N':>14}{'age_squared':>14}{'se':>12}")
print("-" * 64)
for label, fit in [("unweighted", unweighted), ("aweights (wa)", analytic),
("fweights (wf)", frequency)]:
print(f"{label:<24}{fit.n_obs:>14,.0f}{fit.beta[0]:>14.6f}{fit.se[0]:>12.6f}")
print("\nfweights inflate N to the weight total, which is why its standard "
"errors\nare smaller: it is told there are more observations.")
# -------------------------------------------------------------------------- IV
# Four parts: outcome ~ exogenous | fixed effects | endogenous ~ instruments
#
# In the simulated data x2 is correlated with an unobservable that also enters
# y_iv, so OLS on x2 is biased upward; z1 and z2 are excluded instruments. The
# true coefficient on x2 is 0.2.
ols = feols_stream("y_iv ~ age_squared + x2 | worker_id + firm_id",
data, workdir=workdir("iv_ols"), vcov={"CRV1": "worker_id"},
verbose=False)
iv = feols_stream("y_iv ~ age_squared | worker_id + firm_id | x2 ~ z1 + z2",
data, workdir=workdir("iv"), vcov={"CRV1": "worker_id"},
verbose=False)
print(f"\n{'=' * 64}\n2SLS: x2 is endogenous, instrumented by z1 and z2\n{'=' * 64}")
print("true coefficient on x2: 0.200000")
print(f"OLS (biased): {ols.coef()['x2']:>9.6f}")
print(f"2SLS (consistent): {iv.coef()['x2']:>9.6f}")
iv.summary()
# The first stage is a fitted model in its own right
print("\nfirst stage (x2 on the instruments, same fixed effects):")
print(iv.first_stage[0].tidy())
# The reported F is the Wald statistic on the excluded instruments, computed
# under the same vcov as the model -- the usual weak-instrument diagnostic.
print(f"\nfirst-stage F: {iv.f_stat_1st_stage[0]:,.1f}"
" (>10 is the conventional rule of thumb)")
# Weights and IV together
iv_weighted = feols_stream("y_iv ~ age_squared | worker_id + firm_id | x2 ~ z1 + z2",
data, workdir=workdir("iv_weighted"), weights="wa",
vcov={"CRV1": "worker_id"}, verbose=False)
print(f"\nweighted 2SLS on x2: {iv_weighted.coef()['x2']:>9.6f}")