From a0a8d364977a19e69aee371e1ae99d10001cf804 Mon Sep 17 00:00:00 2001 From: Zhang Hong <242528979+shuxue6662-a11y@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:46:57 +0800 Subject: [PATCH] fix: raise clear errors for missing values in panel index --- linearmodels/panel/data.py | 10 +++++++++ linearmodels/panel/model.py | 6 ++++++ linearmodels/tests/panel/test_data.py | 31 +++++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/linearmodels/panel/data.py b/linearmodels/panel/data.py index 092a5be18b..5db04f8848 100644 --- a/linearmodels/panel/data.py +++ b/linearmodels/panel/data.py @@ -368,6 +368,11 @@ def entity_ids(self) -> linearmodels.typing.data.IntArray: 2d array containing entity ids corresponding dataframe view """ index = self.index + if np.any(index.codes[0] < 0): + raise ValueError( + "The entity index contains missing values (NaN), which are not " + "supported and are encoded as -1 in the entity ids." + ) return np.asarray(index.codes[0])[:, None] @property @@ -381,6 +386,11 @@ def time_ids(self) -> linearmodels.typing.data.IntArray: 2d array containing time ids corresponding dataframe view """ index = self.index + if np.any(index.codes[1] < 0): + raise ValueError( + "The time index contains missing values (NaN), which are not " + "supported and are encoded as -1 in the time ids." + ) return np.asarray(index.codes[1])[:, None] def _demean_both_low_mem(self, weights: PanelData | None) -> PanelData: diff --git a/linearmodels/panel/model.py b/linearmodels/panel/model.py index 1374d7235c..9a997a9253 100644 --- a/linearmodels/panel/model.py +++ b/linearmodels/panel/model.py @@ -93,6 +93,12 @@ def _lstsq( def panel_structure_stats(ids: linearmodels.typing.data.IntArray, name: str) -> Series: + ids = np.asarray(ids) + if np.any(ids < 0): + raise ValueError( + f"{name} cannot be computed: ids must be non-negative integers. " + "Negative ids indicate missing values (NaN) in the panel index." + ) bc = np.bincount(ids) bc = bc[bc > 0] index = ["mean", "median", "max", "min", "total"] diff --git a/linearmodels/tests/panel/test_data.py b/linearmodels/tests/panel/test_data.py index 8c543c7bd5..d98dc81ad0 100644 --- a/linearmodels/tests/panel/test_data.py +++ b/linearmodels/tests/panel/test_data.py @@ -4,13 +4,13 @@ import numpy as np from numpy.linalg import lstsq, pinv from numpy.testing import assert_allclose, assert_equal -from pandas import Categorical, DataFrame, Series, date_range, get_dummies +from pandas import Categorical, DataFrame, MultiIndex, Series, date_range, get_dummies from pandas.api.types import is_string_dtype from pandas.testing import assert_frame_equal, assert_index_equal import pytest from linearmodels.panel.data import PanelData, _Panel -from linearmodels.panel.model import PanelOLS +from linearmodels.panel.model import PanelOLS, panel_structure_stats from linearmodels.shared.utility import panel_to_frame from linearmodels.tests.panel._utility import MISSING_XARRAY, datatypes, generate_data @@ -223,6 +223,33 @@ def test_ids(mi_df): assert np.ptp(tids[i::7]) == 0 +def test_entity_ids_with_missing_index(): + n, t = 4, 3 + index = MultiIndex.from_tuples( + [(np.nan if i == 0 else f"e{i}", j) for i in range(n) for j in range(1, t + 1)], + names=["entity", "time"], + ) + data = PanelData(DataFrame(np.random.standard_normal((n * t, 2)), index=index)) + with pytest.raises(ValueError, match="entity index contains missing"): + _ = data.entity_ids + + +def test_time_ids_with_missing_index(): + n, t = 4, 3 + index = MultiIndex.from_tuples( + [(f"e{i}", np.nan if j == 1 else j) for i in range(n) for j in range(1, t + 1)], + names=["entity", "time"], + ) + data = PanelData(DataFrame(np.random.standard_normal((n * t, 2)), index=index)) + with pytest.raises(ValueError, match="time index contains missing"): + _ = data.time_ids + + +def test_panel_structure_stats_negative_ids(): + with pytest.raises(ValueError, match="non-negative"): + panel_structure_stats(np.array([0, 1, -1]), "test") + + def test_str_repr(mi_df): data = PanelData(mi_df) assert "PanelData" in str(data)