Skip to content

BUG: Use Hotelling's T-squared reference for the FamaMacBeth joint Wa… - #796

Merged
bashtage merged 5 commits into
mainfrom
lm71-fix-famamacbeth-hotelling
Oct 13, 2026
Merged

bashtage merged 5 commits into
mainfrom
lm71-fix-famamacbeth-hotelling

Conversation

@bashtage

Copy link
Copy Markdown
Owner

…ld test

f_statistic_robust of FamaMacBethResults, the Wald test that the parameters
other than the constant are zero, compared W / k with an F distribution with k
and T - 1 degrees of freedom (k and nobs - nvar in 7.0 and earlier), where k is
the number of tested parameters and T the number of periods whose
cross-section is estimated. The parameters are the average of the T
cross-sectional estimates, and their covariance is estimated from the same T
estimates, so that W is Hotelling's T-squared statistic and W / k is
(T - 1) / (T - k) times an F(k, T - k) variable. Neither F distribution is
right when k is larger than 1. With one parameter, F(1, T - 1) is the squared
t(T - 1) distribution and is right. Joint tests of several regressors reject a
true null hypothesis too often, and more with fewer periods.

The reported statistic is now F = (T - k) / (k (T - 1)) W and the p-value is
that of an F(k, T - k) distribution:

  • It is exact when the T estimates are independent draws from the same normal
    distribution and the covariance is the standard covariance of their average,
    which is cov_type "unadjusted", "robust" and their aliases. With the kernel
    covariance it is an approximation, and the documentation says so.
  • With one tested parameter F is the squared t-statistic and the p-value is that
    of the t(T - 1) test, as before.
  • T is the number of periods whose cross-section is estimated, as for the p-values
    of the parameters, so periods that are dropped are not counted. It is
    df_resid + 1, and the denominator degrees of freedom are T - k.
  • With debiased=False the statistic is W and the distribution is chi-squared
    with k degrees of freedom, as before.
  • If debiased is True and T is not larger than k, the covariance of the
    parameters has rank at most T - 1 and is singular, and the statistic was the
    result of inverting it. It is an InvalidTestStatistic that says so.

In simulations of panels of 40 entities in which the slopes change from period to
period and average to zero (the null hypothesis), the 5% tests rejected in these
shares of 20,000 samples (the standard error is 0.15 percentage points). With the
same regressors in every period the T estimates are exactly independent and
normal, and with new regressors in every period they are not exactly:

                              same regressors           new regressors
                              7.0   T - 1   now         7.0   T - 1   now
k = 1, T = 10                 8.3%  5.2%    5.2%        7.6%  4.9%    4.9%
k = 1, T = 20                 6.5%  5.0%    5.0%        6.7%  5.2%    5.2%
k = 3, T = 10                 19.7% 10.6%   5.4%        19.7% 10.5%   5.0%
k = 3, T = 20                 11.0% 7.2%    4.9%        11.2% 7.5%    5.3%
k = 5, T = 10                 40.6% 24.1%   5.0%        41.2% 24.8%   5.1%
k = 5, T = 20                 17.9% 11.1%   4.9%        18.3% 11.3%   5.1%

"7.0" is F(k, nobs - nvar), "T - 1" is F(k, T - 1) of the previous commit, and
"now" is the statistic of this commit. In the wage panel (T = 8, 4,360
observations), the test of union and married has p-value 1e-62 in 7.0 and 1.9e-6
in the previous commit, and is F = 126.5 with the F(2, 6) distribution and
p-value 1.2e-5 now; the test of black, hisp, exper, educ and union has p-value
3.1e-5 now, where it was 1.3e-11, and 0 to machine precision in 7.0.

The docstrings of FamaMacBeth, FamaMacBeth.fit and the results say what the
statistic is, its reference distribution and why, and cite Hotelling (1931).

Tests (test_fama_macbeth.py has 182, 56 of which fail on the previous commit):

  • The statistic, the degrees of freedom and the p-value equal a hand computation
    from the T cross-sectional estimates with scipy.stats.f(k, T - k), for k = 1, 2,
    3 and 5, T = 8, 10 and 20, the standard covariance and the kernel covariance
    with 0 and 2 lags.
  • They equal the one-sample Hotelling test of statsmodels, test_mvmean, for the
    slopes, including its T-squared statistic, for k = 3 and 5 and T = 10 and 20.
  • With k = 1 the statistic is the squared t-statistic and the p-value that of the
    t(T - 1) test, with the standard and the kernel covariance, for T = 4, 12, 30.
  • With 4 of 25 periods dropped T is 21, and the statistic has 19 denominator
    degrees of freedom. Weighted cross-sections, a model without a constant, and
    the summary are tested too.
  • debiased=False gives W and chi2(k) for k = 1 and 3.
  • T <= k gives an InvalidTestStatistic, T = k + 1 a valid one with one
    denominator degree of freedom, and a model with only a constant is invalid as
    before.
  • Independent normal estimates give the size of 5% for k = 3 and 5 and T = 10 and
    20, and the F(k, T - 1) test its much larger shares (1,000 samples each).

…ld test

f_statistic_robust of FamaMacBethResults, the Wald test that the parameters
other than the constant are zero, compared W / k with an F distribution with k
and T - 1 degrees of freedom (k and nobs - nvar in 7.0 and earlier), where k is
the number of tested parameters and T the number of periods whose
cross-section is estimated. The parameters are the average of the T
cross-sectional estimates, and their covariance is estimated from the same T
estimates, so that W is Hotelling's T-squared statistic and W / k is
(T - 1) / (T - k) times an F(k, T - k) variable. Neither F distribution is
right when k is larger than 1. With one parameter, F(1, T - 1) is the squared
t(T - 1) distribution and is right. Joint tests of several regressors reject a
true null hypothesis too often, and more with fewer periods.

The reported statistic is now F = (T - k) / (k (T - 1)) W and the p-value is
that of an F(k, T - k) distribution:

* It is exact when the T estimates are independent draws from the same normal
  distribution and the covariance is the standard covariance of their average,
  which is cov_type "unadjusted", "robust" and their aliases. With the kernel
  covariance it is an approximation, and the documentation says so.
* With one tested parameter F is the squared t-statistic and the p-value is that
  of the t(T - 1) test, as before.
* T is the number of periods whose cross-section is estimated, as for the p-values
  of the parameters, so periods that are dropped are not counted. It is
  df_resid + 1, and the denominator degrees of freedom are T - k.
* With debiased=False the statistic is W and the distribution is chi-squared
  with k degrees of freedom, as before.
* If debiased is True and T is not larger than k, the covariance of the
  parameters has rank at most T - 1 and is singular, and the statistic was the
  result of inverting it. It is an InvalidTestStatistic that says so.

In simulations of panels of 40 entities in which the slopes change from period to
period and average to zero (the null hypothesis), the 5% tests rejected in these
shares of 20,000 samples (the standard error is 0.15 percentage points). With the
same regressors in every period the T estimates are exactly independent and
normal, and with new regressors in every period they are not exactly:

                                  same regressors           new regressors
                                  7.0   T - 1   now         7.0   T - 1   now
    k = 1, T = 10                 8.3%  5.2%    5.2%        7.6%  4.9%    4.9%
    k = 1, T = 20                 6.5%  5.0%    5.0%        6.7%  5.2%    5.2%
    k = 3, T = 10                 19.7% 10.6%   5.4%        19.7% 10.5%   5.0%
    k = 3, T = 20                 11.0% 7.2%    4.9%        11.2% 7.5%    5.3%
    k = 5, T = 10                 40.6% 24.1%   5.0%        41.2% 24.8%   5.1%
    k = 5, T = 20                 17.9% 11.1%   4.9%        18.3% 11.3%   5.1%

"7.0" is F(k, nobs - nvar), "T - 1" is F(k, T - 1) of the previous commit, and
"now" is the statistic of this commit. In the wage panel (T = 8, 4,360
observations), the test of union and married has p-value 1e-62 in 7.0 and 1.9e-6
in the previous commit, and is F = 126.5 with the F(2, 6) distribution and
p-value 1.2e-5 now; the test of black, hisp, exper, educ and union has p-value
3.1e-5 now, where it was 1.3e-11, and 0 to machine precision in 7.0.

The docstrings of FamaMacBeth, FamaMacBeth.fit and the results say what the
statistic is, its reference distribution and why, and cite Hotelling (1931).

Tests (test_fama_macbeth.py has 182, 56 of which fail on the previous commit):

* The statistic, the degrees of freedom and the p-value equal a hand computation
  from the T cross-sectional estimates with scipy.stats.f(k, T - k), for k = 1, 2,
  3 and 5, T = 8, 10 and 20, the standard covariance and the kernel covariance
  with 0 and 2 lags.
* They equal the one-sample Hotelling test of statsmodels, test_mvmean, for the
  slopes, including its T-squared statistic, for k = 3 and 5 and T = 10 and 20.
* With k = 1 the statistic is the squared t-statistic and the p-value that of the
  t(T - 1) test, with the standard and the kernel covariance, for T = 4, 12, 30.
* With 4 of 25 periods dropped T is 21, and the statistic has 19 denominator
  degrees of freedom. Weighted cross-sections, a model without a constant, and
  the summary are tested too.
* debiased=False gives W and chi2(k) for k = 1 and 3.
* T <= k gives an InvalidTestStatistic, T = k + 1 a valid one with one
  denominator degree of freedom, and a model with only a constant is invalid as
  before.
* Independent normal estimates give the size of 5% for k = 3 and 5 and T = 10 and
  20, and the F(k, T - 1) test its much larger shares (1,000 samples each).
The docstrings of FamaMacBeth and its results cite Hotelling, whose name the
spelling check of the documentation does not know. The docstring of the joint
test no longer uses the abbreviation HAC, which is not in the word list either.
The docstring of the helper that computes the statistic by hand in the tests
described the Wald statistic wrongly as T times Hotelling's T-squared statistic.
It is that statistic when the covariance is the sample covariance of the T
estimates with the divisor T - 1, divided by T.
…he adjustment too

The covariance of k parameters that are averages of T cross-sectional estimates
has rank at most T - 1, so it is singular when T is not larger than k, whether
or not debiased is True. The previous commit returned an InvalidTestStatistic for
that case only if debiased is True. With debiased=False, f_statistic_robust
still inverted the singular matrix. For a constant and 3 regressors and 3
periods it returned a statistic of 2.2e16 with a p-value of 0, and with 2
periods it raised a LinAlgError, which also made the summary raise. The statistic
is now invalid for both values of debiased. Models with more periods than tested
parameters are not changed. Without the adjustment the statistic is still W with
a chi-squared distribution.

The docstring of f_statistic_robust was too kind to the kernel covariance. A
kernel estimate of the long-run covariance from T estimates is much less precise
than the sample covariance, so that the F test with the kernel covariance can
reject a true null hypothesis far too often even if the estimates are
independent, which the docstring attributed to dependence. In simulations of
independent normal estimates with the default bandwidth, the 5% test rejected in
21% of the samples with k = 1 and T = 10, 37% with k = 3 and T = 10, 30% with
k = 3 and T = 20, 23% with k = 3 and T = 40 and 44% with k = 5 and T = 20, where
the test with the standard covariance rejected in 4.4% to 5.5% (4,000 samples
each). A Bartlett kernel with 2 lags rejects in 9.5% of 20,000 samples with
k = 1 and T = 10, and in 19% with k = 5 and T = 20. The docstring gives the
shares for k = 1 and T = 10, k = 3 and T = 10, and k = 3 and T = 40, and the
class docstring says that the approximation can be poor. The docstring also says
that the estimates must be draws from the same distribution, and that the
general wald_test compares its statistic with a chi-squared distribution and
does not have this correction. Hotelling T-squared is written in words, since T
is the number of periods.

The test of the size of the joint test compares with the exact shares in which the
test with F(k, T - 1) rejects, which follow from W being k (T - 1) / (T - k) times
an F(k, T - k) variable: 10.4% for k = 3 and T = 10, 24.3% for k = 5 and T = 10,
7.2% for k = 3 and T = 20 and 11.3% for k = 5 and T = 20. The tolerance is five
standard errors of the share of 1,000 samples, instead of the constants and
tolerances that were chosen from a simulation. The test that compares with
statsmodels does not guard the import of test_mvmean any more. The test of the
dropped periods compares with the statistic that counts the 25 periods of the
panel, and the helper that computes the statistic by hand describes its
covariance with and without a bandwidth.
Move the helper of the joint test of FamaMacBeth above _deferred_f, next to FInfo,
which both use. Nothing else changes. A pending change adds another helper right
below _deferred_f, where this one was, so that the two additions touched the same
lines and conflicted when they were merged.
…FamaMacBeth

The test of the joint test with dropped periods matched the warning about the
periods that cannot be estimated with "4 of the time-series regressions". The model
estimates a cross-sectional regression for each period, and another change words
the warning that way. The test matches the count of the dropped periods and the
word regressions, which both texts have, so that it passes with either.
@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.62%. Comparing base (1782b62) to head (6717c59).
⚠️ Report is 107 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #796      +/-   ##
==========================================
+ Coverage   99.59%   99.62%   +0.02%     
==========================================
  Files         128      139      +11     
  Lines       24435    28184    +3749     
  Branches     1820     1980     +160     
==========================================
+ Hits        24336    28077    +3741     
- Misses         39       41       +2     
- Partials       60       66       +6     
Flag Coverage Δ
adder 99.60% <100.00%> (+0.03%) ⬆️
subtractor 99.60% <100.00%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bashtage
bashtage merged commit 5cb3b6b into main Oct 13, 2026
24 checks passed
@bashtage
bashtage deleted the lm71-fix-famamacbeth-hotelling branch October 13, 2026 10:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant