Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/iv/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ tests.
examples/advanced-examples.ipynb
examples/using-formulas.ipynb
examples/absorbing-regression.ipynb
weak-instruments
reference
mathematical-formula

Expand Down
48 changes: 48 additions & 0 deletions doc/source/iv/weak-instruments.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
Confidence intervals with a weak instrument
===========================================

:meth:`~linearmodels.iv.results.IVResults.tf_conf_int` constructs a conservative
95% tF interval for one endogenous regressor and one excluded instrument. It is
an explicit alternative to the conventional :meth:`~linearmodels.iv.results.IVResults.conf_int`.
It supports IV2SLS with unadjusted or heteroskedasticity-robust covariance,
``debiased=False``, and constant observation weights. Other configurations raise
``ValueError``. Included exogenous controls are allowed.

For example, use proximity to a four-year college as an instrument for education:

.. doctest::

>>> from linearmodels.datasets import card
>>> from linearmodels.iv import IV2SLS
>>> data = card.load()
>>> columns = ["lwage", "educ", "exper", "expersq", "black", "south", "smsa", "nearc4"]
>>> data = data[columns].dropna()
>>> formula = "lwage ~ 1 + exper + expersq + black + south + smsa + [educ ~ nearc4]"
>>> result = IV2SLS.from_formula(formula, data).fit(cov_type="robust")
>>> interval = result.tf_conf_int()
>>> list(interval.index), list(interval.columns)
(['educ'], ['lower', 'upper'])
>>> conventional = result.conf_int().loc["educ"]
>>> bool(interval.loc["educ", "lower"] <= conventional["lower"])
True
>>> bool(interval.loc["educ", "upper"] >= conventional["upper"])
True

The first-stage statistic is the squared t statistic for the excluded instrument,
using the fitted model's covariance convention. The coefficient and SE are reused.
Neither the original SE nor the conventional p-values and intervals are changed.

The implementation interpolates in F using the upward-rounded values in Table 3a
of the `February 16, 2022 author version
<https://eml.berkeley.edu/~jmccrary/LMMP_2_15_22.pdf>`_ of Lee, McCrary, Moreira and
Porter's `Valid t-Ratio Inference for IV <https://doi.org/10.1257/aer.20211063>`_.
Section 3.2 justifies this conservative interpolation by convexity. It uses an
unbounded interval below F=4, including the small range where the smooth tF
interval is bounded, and a critical value of 1.96 at and above F=104.67.

This adjustment is calibrated only for 95% confidence. It is not a p-value
calculation or a guarantee of finite-sample coverage. Instrument validity and the
model's other identifying and sampling assumptions still matter. The newer VtF
procedure uses additional information and can yield shorter intervals; this
method implements tF, not VtF. The existing ``anderson_rubin`` property is a test
of overidentification, not a weak-instrument confidence set for this coefficient.
119 changes: 119 additions & 0 deletions linearmodels/iv/_tf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""Conservative tabulated tF critical values for single-instrument IV."""

from numpy import array, inf, interp, isfinite

# Table 3a, Lee, McCrary, Moreira and Porter, February 16, 2022 author version:
# https://eml.berkeley.edu/~jmccrary/LMMP_2_15_22.pdf (page 16).
# Both columns are rounded upward in the paper. Section 3.2 justifies linear
# interpolation in F by convexity of the critical-value function.
_TF_TABLE = array(
[
(4.000, 18.656),
(4.008, 18.236),
(4.015, 17.826),
(4.023, 17.425),
(4.031, 17.033),
(4.040, 16.649),
(4.049, 16.275),
(4.059, 15.909),
(4.068, 15.551),
(4.079, 15.201),
(4.090, 14.859),
(4.101, 14.524),
(4.113, 14.197),
(4.125, 13.878),
(4.138, 13.566),
(4.151, 13.260),
(4.166, 12.962),
(4.180, 12.670),
(4.196, 12.385),
(4.212, 12.107),
(4.229, 11.834),
(4.247, 11.568),
(4.265, 11.308),
(4.285, 11.053),
(4.305, 10.804),
(4.326, 10.561),
(4.349, 10.324),
(4.372, 10.091),
(4.396, 9.864),
(4.422, 9.642),
(4.449, 9.425),
(4.477, 9.213),
(4.507, 9.006),
(4.538, 8.803),
(4.570, 8.605),
(4.604, 8.412),
(4.640, 8.222),
(4.678, 8.037),
(4.717, 7.856),
(4.759, 7.680),
(4.803, 7.507),
(4.849, 7.338),
(4.897, 7.173),
(4.948, 7.011),
(5.002, 6.854),
(5.059, 6.699),
(5.119, 6.549),
(5.182, 6.401),
(5.248, 6.257),
(5.319, 6.117),
(5.393, 5.979),
(5.472, 5.844),
(5.556, 5.713),
(5.644, 5.584),
(5.738, 5.459),
(5.838, 5.336),
(5.944, 5.216),
(6.056, 5.098),
(6.176, 4.984),
(6.304, 4.872),
(6.440, 4.762),
(6.585, 4.655),
(6.741, 4.550),
(6.907, 4.448),
(7.085, 4.348),
(7.276, 4.250),
(7.482, 4.154),
(7.702, 4.061),
(7.940, 3.969),
(8.196, 3.880),
(8.473, 3.793),
(8.773, 3.707),
(9.098, 3.624),
(9.451, 3.542),
(9.835, 3.463),
(10.253, 3.385),
(10.711, 3.309),
(11.214, 3.234),
(11.766, 3.161),
(12.374, 3.090),
(13.048, 3.021),
(13.796, 2.953),
(14.631, 2.886),
(15.566, 2.821),
(16.618, 2.758),
(17.810, 2.696),
(19.167, 2.635),
(20.721, 2.576),
(22.516, 2.518),
(24.605, 2.461),
(27.058, 2.406),
(29.967, 2.352),
(33.457, 2.299),
(37.699, 2.247),
(42.930, 2.197),
(49.495, 2.147),
(57.902, 2.099),
(68.930, 2.052),
(83.823, 2.006),
(104.67, 1.960),
]
)


def _tf_critical_value(f_stat: float) -> float:
"""Return the conservative 95% tF critical value from Table 3a."""
if not isfinite(f_stat) or f_stat < 0:
raise ValueError("The first-stage F statistic must be finite and nonnegative.")
return float(interp(f_stat, _TF_TABLE[:, 0], _TF_TABLE[:, 1], left=inf))
103 changes: 103 additions & 0 deletions linearmodels/iv/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
c_,
diag,
empty,
isfinite,
isnan,
log,
ndarray,
Expand All @@ -31,8 +32,13 @@
from statsmodels.iolib.table import default_txt_fmt

import linearmodels
from linearmodels.iv._tf import _tf_critical_value
from linearmodels.iv._utility import annihilate, proj
from linearmodels.iv.common import _kleibergen_paap, cragg_donald, cragg_donald_f
from linearmodels.iv.covariance import (
HeteroskedasticCovariance,
HomoskedasticCovariance,
)
from linearmodels.iv.data import IVData, IVDataLike
from linearmodels.shared.base import _ModelComparison, _SummaryStr
from linearmodels.shared.hypotheses import (
Expand Down Expand Up @@ -1312,6 +1318,103 @@ def __init__(
super().__init__(results, model)
self._kappa = results.get("kappa", 1)

def tf_conf_int(self) -> DataFrame:
"""
Construct a conservative 95% tF confidence interval.

Returns
-------
DataFrame
Lower and upper bounds for the endogenous regressor's coefficient.
The columns are ``lower`` and ``upper``. When the first-stage
statistic is below 4, the bounds are negative and positive infinity.

Raises
------
ValueError
If the result is not from unweighted IV2SLS with one endogenous
regressor, one excluded instrument, unadjusted or robust covariance,
and ``debiased=False``, or the required estimates are not finite
with strictly positive variances.

See Also
--------
conf_int
Construct conventional confidence intervals for all coefficients.
first_stage
Access first-stage regressions and diagnostics.

Notes
-----
The tF procedure of Lee et al. [1]_ adjusts the critical value using
the squared first-stage t statistic. It provides asymptotic inference
robust to weak identification under the single-instrument IV model's
assumptions. It does not address invalid instruments or guarantee
finite-sample coverage.

This implementation linearly interpolates the upward-rounded critical
values in Table 3a of the February 16, 2022 author version. Section 3.2
establishes that interpolation in F is conservative. Below the first
tabulated F of 4, the interval is the entire real line; above 104.67,
the critical value is 1.96. The extra conservatism below 4 includes
some cases where the underlying smooth tF procedure is bounded.

Only 95% confidence is supported. The adjustment does not define
p-values or other confidence levels. The coefficient estimate, reported
standard error and conventional inference are not changed.

References
----------
.. [1] Lee, D. S., J. McCrary, M. J. Moreira, and J. Porter (2022).
"Valid t-Ratio Inference for IV." American Economic Review,
112(10), 3260-3290. https://doi.org/10.1257/aer.20211063.
Author version: https://eml.berkeley.edu/~jmccrary/LMMP_2_15_22.pdf.
"""
from linearmodels.iv.model import IV2SLS

if (
not isinstance(self.model, IV2SLS)
or self.model.endog.shape[1] != 1
or self.model.instruments.shape[1] != 1
):
raise ValueError(
"tF inference requires IV2SLS with one endogenous regressor "
"and one excluded instrument."
)
if self.debiased or type(self._cov_estimator) not in (
HomoskedasticCovariance,
HeteroskedasticCovariance,
):
raise ValueError(
"tF inference requires unadjusted or robust covariance "
"with debiased=False."
)
if (self.model.weights.ndarray != 1).any():
raise ValueError("tF inference does not support nonconstant weights.")

name = self.model.endog.cols[0]
first_stage = self.first_stage.individual[str(name)]
estimate = float(self.params.iloc[-1])
variance = float(self.cov.to_numpy()[-1, -1])
first_estimate = float(first_stage.params.iloc[-1])
first_variance = float(first_stage.cov.to_numpy()[-1, -1])
if (
not isfinite([estimate, variance, first_estimate, first_variance]).all()
or variance <= 0
or first_variance <= 0
):
raise ValueError(
"tF inference requires finite estimates and positive variances."
)
first_t = float(first_stage.tstats.iloc[-1])
critical = _tf_critical_value(first_t * first_t)
half_width = critical * float(self.std_errors.iloc[-1])
return DataFrame(
[[estimate - half_width, estimate + half_width]],
index=self.params.index[-1:],
columns=["lower", "upper"],
)

@cached_property
def sargan(self) -> InvalidTestStatistic | WaldTestStatistic:
r"""
Expand Down
6 changes: 5 additions & 1 deletion linearmodels/tests/iv/test_jive.py
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,11 @@ def test_results():
# JIVE is not a k-class estimator
assert res.kappa is None
for attr in dir(res):
if attr.startswith("_") or attr in ("test_linear_constraint", "wald_test"):
if attr.startswith("_") or attr in (
"test_linear_constraint",
"wald_test",
"tf_conf_int",
):
continue
value = getattr(res, attr)
if callable(value):
Expand Down
6 changes: 5 additions & 1 deletion linearmodels/tests/iv/test_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ def model(request):

def result_checker(res):
for attr in dir(res):
if attr.startswith("_") or attr in ("test_linear_constraint", "wald_test"):
if attr.startswith("_") or attr in (
"test_linear_constraint",
"wald_test",
"tf_conf_int",
):
continue
print(attr)
if attr == "summary":
Expand Down
Loading