fixest is the dominant R package for applied IV
estimation. This vignette shows the drop-in integration: fit an IV model
with feols(), pass it to iv_check(), and get
every applicable IV-validity test in one call.
If you are already using fixest for your paper, nothing
about your workflow changes. Add one line and your IV estimate now comes
with a published falsification test.
Card’s (1995) classic IV for the return to schooling uses proximity
to a four-year college as an instrument for completed schooling. The
bundled card1995 dataset is a cleaned extract from the
National Longitudinal Survey of Young Men.
data(card1995)
head(card1995[, c("lwage", "educ", "college", "near_college",
"age", "black", "south")])
#> lwage educ college near_college age black south
#> 1 6.306275 7 0 0 29 1 0
#> 2 6.175867 12 0 0 27 0 0
#> 3 6.580639 12 0 0 34 0 0
#> 4 5.521461 11 0 1 27 0 0
#> 5 6.591674 12 0 1 34 0 0
#> 6 6.214608 12 0 1 26 0 0Two variants are included: the continuous educ (years of
schooling) and a binary college indicator
(educ >= 16) for use with tests that require a binary
treatment.
We start with the simplest specification: no exogenous controls in
the structural equation. This is the case iv_kitagawa() is
designed for.
m_uncond <- feols(
lwage ~ 1 | college ~ near_college,
data = card1995
)
summary(m_uncond)
#> TSLS estimation - Dep. Var.: lwage
#> Endo. : college
#> Instr. : near_college
#> Second stage: Dep. Var.: lwage
#> Observations: 3,003
#> Standard-errors: IID
#> Estimate Std. Error t value Pr(>|t|)
#> (Intercept) 5.65161 0.155590 36.32367 < 2.2e-16 ***
#> fit_college 2.24687 0.568671 3.95109 7.9588e-05 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> RMSE: 0.996225 Adj. R2: 0.02591
#> F-test (1st stage), college: stat = 15.589, p = 8.052e-5, on 1 and 3,001 DoF.
#> Wu-Hausman: stat = 68.919, p < 2.2e-16 , on 1 and 3,000 DoF.chk <- iv_check(m_uncond, n_boot = 500, parallel = FALSE)
print(chk)
#>
#> ── IV validity diagnostic ──────────────────────────────────────────────────────
#> Kitagawa (2015): stat = "5.25", p = "0", reject
#> Mourifie-Wan (2017): stat = "5.25", p = "0", reject
#> Overall: at least one test rejects IV validity at 0.05.iv_check() inspects the model, detects that
college is binary and near_college is a
discrete instrument, and runs Kitagawa (2015) and Mourifie-Wan (2017).
With no covariates the two are numerically identical (Mourifie-Wan
reduces exactly to the variance-weighted Kitagawa test,
unit-tested).
Card’s identification strategy is more naturally read as “valid
conditional on demographic and regional controls”. In that
setting the right test is Mourifie and Wan’s (2017) conditional version:
same testable family of inequalities as Kitagawa, but the conditional
CDFs are estimated by series regression on X rather than
treated as unconditional.
In ivcheck v0.1.2, the conditional path supports a
single covariate (multivariate via tensor-product basis is planned for
v0.2.0).
iv_kitagawa() is strictly the unconditional test and
refuses fitted models that carry controls:
iv_kitagawa(m_cond, n_boot = 100, parallel = FALSE)
#> Error in `abort_if_controls_present()`:
#> ! `iv_kitagawa()` is the unconditional Kitagawa (2015) test; it does not
#> condition on controls.
#> ✖ Your fitted model has 1 exogenous control(s): 'age'.
#> ℹ Use `iv_mw()` on the same model for the conditional Mourifie-Wan (2017) test.
#> ℹ To force the unconditional test on these data, call `iv_kitagawa()` on raw
#> `(y, d, z)` vectors.The conditional test is iv_mw(). Dispatched on the same
fitted model, it picks up the single covariate automatically and runs
the Chernozhukov-Lee-Rosen series-regression test with Andrews-Soares
adaptive moment selection:
mw <- iv_mw(m_cond, n_boot = 200, parallel = FALSE)
print(mw)
#>
#> ── Mourifie-Wan (2017) ─────────────────────────────────────────────────────────
#> Sample size: 3003
#> Statistic: "79.5", p-value: "0.675"
#> Verdict: cannot reject IV validity at 0.05iv_check() does the right thing automatically: it
detects that the model has controls, drops Kitagawa from the applicable
list with an informational message, and reports MW alone:
iv_check(m_cond, n_boot = 200, parallel = FALSE)
#> ℹ Kitagawa test skipped: fitted model has exogenous controls and
#> `iv_kitagawa()` is unconditional.
#> ℹ The conditional Mourifie-Wan test is the right object here.
#>
#>
#> ── IV validity diagnostic ──────────────────────────────────────────────────────
#>
#> Mourifie-Wan (2017): stat = "79.5", p = "0.695", pass
#>
#> Overall: cannot reject IV validity at 0.05.When the structural equation carries more than one exogenous control,
iv_mw() in v0.1.2 does not yet support the multivariate
conditioning required for a valid conditional test. Both Kitagawa and MW
are skipped, and iv_check() reports back with informational
messages:
m_multi <- feols(
lwage ~ age + black + south | college ~ near_college,
data = card1995
)
iv_check(m_multi, n_boot = 100, parallel = FALSE)
#> ℹ Kitagawa test skipped: fitted model has exogenous controls and
#> `iv_kitagawa()` is unconditional.
#> ℹ The conditional Mourifie-Wan test is the right object here.
#> ℹ Mourifie-Wan test skipped: multivariate `X` (>1 control) is not yet supported
#> in v0.1.2.
#> ℹ Planned for v0.2.0 via tensor-product series basis.
#> ℹ Workaround: reduce `X` to a single propensity index and call `iv_mw()`
#> directly.
#> Error in `iv_check()`:
#> ! Could not detect an applicable IV-validity test for this model.
#> ℹ The model does not appear to be an IV model, or the treatment is not binary.
#> ℹ Supported: `fixest::feols()` with formula `y ~ x | d ~ z` or
#> `ivreg::ivreg()`.Multivariate conditioning via tensor-product series basis is planned for v0.2.0. Until then, two workarounds:
X to a single index. Fit a
propensity score Pr(D = 1 | X) or another scalar summary of
the controls, then refit the IV model with that index as the single
exogenous control and call iv_mw() on the result.X and call iv_kitagawa() on raw
vectors within each cell, applying a Bonferroni adjustment across
cells.modelsummaryIf you have modelsummary installed,
iv_check results are picked up automatically through
broom::glance registered on package load. This lets you put
a validity p-value directly in a regression table footer:
In your paper’s replication code:
library(fixest)
library(ivcheck)
# ... data loading ...
# IV estimate (conditional on a single control)
m <- feols(y ~ x | d ~ z, data = df)
# IV validity diagnostic
chk <- iv_check(m)
# Report both in the paper
knitr::kable(chk$table)Three lines of code, a falsification test the referee is almost
guaranteed to ask about, and a citation-ready result. That is the whole
point of ivcheck.
Card, D. (1995). Using Geographic Variation in College Proximity to Estimate the Return to Schooling.
Kitagawa, T. (2015). A Test for Instrument Validity. Econometrica 83(5): 2043-2063.
Mourifie, I. and Wan, Y. (2017). Testing Local Average Treatment Effect Assumptions. Review of Economics and Statistics 99(2): 305-313.