Skip to content

BUG: Raise a clear error when a panel model has no residual degrees o… - #799

Merged
bashtage merged 9 commits into
mainfrom
lm71-fix-panel-errors
Oct 13, 2026
Merged

bashtage merged 9 commits into
mainfrom
lm71-fix-panel-errors

Conversation

@bashtage

Copy link
Copy Markdown
Owner

…f freedom

A model with as many parameters as observations once the effects are removed raised
"ZeroDivisionError: division by zero" when the covariance was scaled by n / (n - k),
and when the F-statistics were computed. The panels are small, but they are what is
left of a panel after the missing values, the singletons and the absorbed effects are
dropped: PooledOLS with 2 observations, a constant and a regressor, PanelOLS with
entity or time effects for 2 entities with 2 periods and 2 regressors, BetweenOLS with
2 entities, a constant and a regressor, FirstDifferenceOLS with 1 entity of 3 periods
and 2 regressors, and FamaMacBeth with a cross-section that has as many entities as
regressors.

The covariance now raises a ValueError that says that the model has no residual degrees
of freedom, and how many observations, regressors and degrees of freedom of the effects
it has. With debiased=False the estimates and the covariance are defined, the model
F-statistic is an InvalidTestStatistic, and the summary can be printed. The pooled
F-statistic of PanelOLS, which tests that the effects are zero, is an
InapplicableTestStatistic if the effects do not use any degrees of freedom (a single
entity or a single period with a constant, where the effect is the constant) and an
InvalidTestStatistic if there are no residual degrees of freedom. The models with
residual degrees of freedom do not change.

The tests estimate each of the models with each covariance estimator, with and without
debiased, and compare the slopes of the model with a single entity with the slopes of
the pooled regression with a constant.

…f freedom

A model with as many parameters as observations once the effects are removed raised
"ZeroDivisionError: division by zero" when the covariance was scaled by n / (n - k),
and when the F-statistics were computed. The panels are small, but they are what is
left of a panel after the missing values, the singletons and the absorbed effects are
dropped: PooledOLS with 2 observations, a constant and a regressor, PanelOLS with
entity or time effects for 2 entities with 2 periods and 2 regressors, BetweenOLS with
2 entities, a constant and a regressor, FirstDifferenceOLS with 1 entity of 3 periods
and 2 regressors, and FamaMacBeth with a cross-section that has as many entities as
regressors.

The covariance now raises a ValueError that says that the model has no residual degrees
of freedom, and how many observations, regressors and degrees of freedom of the effects
it has. With debiased=False the estimates and the covariance are defined, the model
F-statistic is an InvalidTestStatistic, and the summary can be printed. The pooled
F-statistic of PanelOLS, which tests that the effects are zero, is an
InapplicableTestStatistic if the effects do not use any degrees of freedom (a single
entity or a single period with a constant, where the effect is the constant) and an
InvalidTestStatistic if there are no residual degrees of freedom. The models with
residual degrees of freedom do not change.

The tests estimate each of the models with each covariance estimator, with and without
debiased, and compare the slopes of the model with a single entity with the slopes of
the pooled regression with a constant.
…integer

BetweenOLS regresses the means of the entities. Regressors that are not collinear in the
data can be collinear in the means: there can be fewer entities than regressors, and the
dummy variables of the periods of a balanced panel all have the mean 1/T for every
entity, so that they are collinear with the constant. The rank check of the model
looks at the data, and the fit returned a minimum norm solution that is not an estimate
of the parameters. The error came later, as numpy's "LinAlgError: Singular matrix" when
the standard errors were used, and it did not say what was wrong.

fit now checks the rank of the regression of the means, as the models check the rank of
the data, and raises a ValueError that says that the regressors are collinear in the
means of the entities, when it happens, and that check_rank=False skips the check. The
other covariance estimators and the weights are not affected, and neither are the models
that have the rank.

The df_model of the results was the tuple (k,) and not the integer k that the other
models return.

The tests use a panel with more regressors than entities and the wage panel with the
dummy variables of the years, which are the two cases, and check that the same models
without the dummy variables are estimated with the degrees of freedom N - k.
…entity or period

PanelOLS with entity_effects=True and time_effects=True for a panel that has a single
entity (or a single period) raised pandas' "ValueError: No objects to concatenate". The
two effects are removed by demeaning the data in the dimension with more groups, and
then in the other dimension using the dummy variables of the dimension with fewer
groups. With one entity or one period there are no dummy variables to remove, and
concatenating the empty list failed.

The data that have been demeaned in the first dimension are now returned when there are
no dummy variables. The effects absorb all of the variation of the regressors, as they
do for entity effects alone and a single entity, and the model raises the
AbsorbingEffectError that says so, for each of the algorithms (the default, LSDV through
use_lsdv, and low_memory).

The tests check the error for panels of 1 entity and of 1 period, and that demeaning
both dimensions of such a panel gives zeros.
generate_panel_data(rng=numpy.random.default_rng(0)) raised "AttributeError: 'Generator'
object has no attribute 'randint'". The function draws the categories of the effects
and of the clusters with randint, which only a RandomState has, and the documentation
only said RandomState. Generators are what NumPy recommends, and what the rest of the
package and its tests use, so the function now draws the integers with integers for a
Generator and with randint for a RandomState. The other draws (standard_normal,
chisquare and choice) are methods of both. Calls with a RandomState, or without rng,
give the same data as before.

The existing test of the function listed random states as a parameter but never passed
them to the function. It does now, with a Generator added, and a new test checks that a
seed gives the same data, the ranges of the categories, and the values that a
RandomState gives (taken from the function before the change).
…nel_data

The clusters of generate_panel_data were the concatenation, along the rows, of a frame
with the first cluster variable and of a frame with both cluster variables. The result
had twice as many rows as the data, with an index that is not unique, and half of the
rows had a missing value of the second variable. Clustered covariances need one row of
clusters for each observation, and the frame could not be used with the data that the
function returns.

The clusters are now the frame with both variables, varcat.0 and varcat.1, for the
observations of the data. The first variable is the one that the removed frame had, so
that the values are those that a RandomState gives for the second half of the old
frame, and the draws of the other variables are unchanged.

The tests check the shape, the index, the missing values, the columns, the range of the
values, that the clusters of an entity are the same in all of its periods, the values
for a seeded RandomState (the values of the old frame), and that the clusters work in a
clustered covariance of PanelOLS.
…arnings

The two warnings of FamaMacBeth about the regressions that cannot be estimated called
them "time-series regressions" (and "time-series observation available to estimate
cross-sectional regressions"). The model estimates one cross-sectional regression for
each period, and then uses the time series of the estimates. The warnings now say how
many cross-sectional regressions cannot be estimated due to deficient rank, and how many
can be estimated when there are fewer than there are parameters, which is when
inference is not available.

The tests check the texts of the warnings.
With check_rank=False the panel models skip the computation of the rank of exog and use
the number of columns, which is the rank if the regressors are not collinear. They used
self.exog.shape[1], and the shape of the PanelData is (variables, periods, entities), so
that the rank was the number of periods. The rank is used to decide if the regressors
contain a constant that is not a column, because the regressors with a constant have the
rank of the regressors alone. The wrong rank gave the wrong answer:

* Two dummy variables that add to one, in a panel with 5 periods, were not a constant.
  The R-squared of PooledOLS was uncentered, 0.2942 instead of 0.1877, that of BetweenOLS
  0.5137 instead of 0.0709, and the F-statistics tested the model as if it had no
  constant.
* Three regressors without a constant, in a panel with 2 periods, were a constant. The
  R-squared of PooledOLS was -0.8760 instead of 0.0304 and FirstDifferenceOLS raised
  "Constants are not allowed in first difference regressions".

The models whose regressors have a constant column, which is found before the rank is
used, and the default check_rank=True do not change.

The tests compare the models with check_rank=False with those with check_rank=True,
and the R-squared of PooledOLS with the R-squared written out in NumPy, for both panels.
… not checked

test_formulas_rank_check fits each model with check_rank=False to regressors that are not
full rank, and FirstDifferenceOLS passed only because the rank that the models used when
the rank was not checked was the number of periods of the panel and not the number of
regressors. The function that looks for a constant that is not a column reads regressors
that are not full rank as containing an implicit constant, and first differences do not
allow a constant. With the number of regressors as the rank, FirstDifferenceOLS raises
"Constants are not allowed in first difference regressions." for such regressors, as
it does when the rank is computed. The test now expects the error for that model.

The other models do not change: the regressors with check_rank=False are estimated as before.
The panel models check that the weights are strictly positive, once the observations
with missing weights have been dropped, and then scale them to have a mean of one. An
infinite weight passed the check, and the scaling made it NaN and every other weight
zero. The models then failed with errors from NumPy, SciPy and pandas that do not say what
is wrong: "array must not contain infs or NaNs", "SVD did not converge", an
AbsorbingEffectError for entity effects, and "operands could not be broadcast together
with shapes (278,1) (280,2)" for FirstDifferenceOLS.

The weights have to be finite as well as strictly positive, and the models raise
"weights must be strictly positive and finite." for zero, negative and infinite weights.
Missing weights are dropped with their observations, as before.

The tests check the error of every model for -inf, inf, 0 and -1, and that missing weights
give the model of the data without those observations.
@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 (9fa3a90) to head (c3b49b6).
⚠️ Report is 53 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #799      +/-   ##
==========================================
+ Coverage   99.60%   99.62%   +0.02%     
==========================================
  Files         136      144       +8     
  Lines       26057    28302    +2245     
  Branches     1873     1983     +110     
==========================================
+ Hits        25954    28196    +2242     
- Misses         38       41       +3     
  Partials       65       65              
Flag Coverage Δ
adder 99.61% <100.00%> (+0.02%) ⬆️
subtractor 99.61% <100.00%> (+0.02%) ⬆️

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 aea66c5 into main Oct 13, 2026
24 checks passed
@bashtage
bashtage deleted the lm71-fix-panel-errors 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