From a7cdfe8d6af3c1e423c67898b181271f6d397ecf Mon Sep 17 00:00:00 2001 From: Kevin Sheppard Date: Wed, 8 Jan 2025 13:19:29 +0000 Subject: [PATCH 1/2] BUG: Correct handling of q in SUR constraints Correct reindexing of q so that NaN is not introduced closes #633 --- linearmodels/panel/model.py | 8 ++----- linearmodels/shared/hypotheses.py | 2 +- linearmodels/system/_utility.py | 3 ++- linearmodels/system/model.py | 2 +- linearmodels/tests/system/test_sur.py | 34 +++++++++++++++++++++++++++ requirements-dev.txt | 4 ++-- 6 files changed, 42 insertions(+), 11 deletions(-) diff --git a/linearmodels/panel/model.py b/linearmodels/panel/model.py index 1071e13d01..f1a60e5cc7 100644 --- a/linearmodels/panel/model.py +++ b/linearmodels/panel/model.py @@ -771,9 +771,7 @@ def _setup_clusters( cat = Categorical(formatted_clusters.dataframe[col]) # TODO: Bug in pandas-stubs # https://github.com/pandas-dev/pandas-stubs/issues/111 - formatted_clusters.dataframe[col] = cat.codes.astype( - np.int64 - ) # type: ignore + formatted_clusters.dataframe[col] = cat.codes.astype(np.int64) # type: ignore clusters_frame = formatted_clusters.dataframe cluster_entity = bool(cov_config_upd.pop("cluster_entity", False)) @@ -2184,9 +2182,7 @@ def _setup_clusters( cluster_max.T, index=index, columns=clusters_panel.vars ) # TODO: Bug in pandas-stubs prevents using Hashable | None - clusters_frame = clusters_frame.loc[reindex].astype( - np.int64 - ) # type: ignore + clusters_frame = clusters_frame.loc[reindex].astype(np.int64) # type: ignore cov_config_upd["clusters"] = clusters_frame return cov_config_upd diff --git a/linearmodels/shared/hypotheses.py b/linearmodels/shared/hypotheses.py index 96aa544cc0..16580e0a66 100644 --- a/linearmodels/shared/hypotheses.py +++ b/linearmodels/shared/hypotheses.py @@ -198,7 +198,7 @@ def _parse_single(constraint: str) -> tuple[str, float]: def _reparse_constraint_formula( - formula: str | list[str] | dict[str, float] + formula: str | list[str] | dict[str, float], ) -> str | dict[str, float]: # TODO: Test against variable names constaining , or = if isinstance(formula, Mapping): diff --git a/linearmodels/system/_utility.py b/linearmodels/system/_utility.py index 1dd5186f6a..f79be8cfa7 100644 --- a/linearmodels/system/_utility.py +++ b/linearmodels/system/_utility.py @@ -262,7 +262,8 @@ def __init__( raise TypeError("q must be a Series or an array") if r.shape[0] != q.shape[0]: raise ValueError("Constraint inputs are not shape compatible") - q_pd = pd.Series(q, index=r_pd.index) + q_pd = pd.Series(q) + q_pd.index = r_pd.index else: q_pd = pd.Series(np.zeros(r_pd.shape[0]), index=r_pd.index) self._q_pd = q_pd diff --git a/linearmodels/system/model.py b/linearmodels/system/model.py index 7c854e2f1a..6c067b613e 100644 --- a/linearmodels/system/model.py +++ b/linearmodels/system/model.py @@ -96,7 +96,7 @@ def _missing_weights( - weights: Mapping[str, linearmodels.typing.data.ArrayLike | None] + weights: Mapping[str, linearmodels.typing.data.ArrayLike | None], ) -> None: """Raise warning if missing weighs found""" missing = [key for key in weights if weights[key] is None] diff --git a/linearmodels/tests/system/test_sur.py b/linearmodels/tests/system/test_sur.py index 03c4102623..178499c707 100644 --- a/linearmodels/tests/system/test_sur.py +++ b/linearmodels/tests/system/test_sur.py @@ -909,3 +909,37 @@ def test_unknown_method(): mod = SUR(generate_data(k=3)) with pytest.raises(ValueError, match="method must be 'ols' or 'gls'"): mod.fit(method="other") + + +def test_sur_contraint_with_value(): + n = 100 + rg = np.random.RandomState(np.random.MT19937(12345)) + x1 = rg.normal(size=n) + x2 = rg.normal(size=n) + x3 = rg.normal(size=n) + + y1 = 3 + 1.5 * x1 - 2.0 * x2 + np.random.normal(size=n) + y2 = -1 + 0.5 * x2 + 1.2 * x3 + np.random.normal(size=n) + + data = DataFrame({"x1": x1, "x2": x2, "x3": x3, "y1": y1, "y2": y2}) + + equations = {"eq1": "y1 ~ x1 + x2", "eq2": "y2 ~ x2 + x3"} + + model = SUR.from_formula(equations, data) + + # coefficients of eq1_x1 and eq2_x2 are equal + r = DataFrame( + [[0] * 4], columns=model.param_names, index=["rest"], dtype=np.float64 + ) + r.iloc[0, 0] = -1 + r.iloc[0, 2] = 1 + + q = Series([0]) + model.add_constraints(r, q) + result = model.fit() + + # No error without q + model = SUR.from_formula(equations, data) + model.add_constraints(r) + result_without_q = model.fit() + assert_allclose(result.params, result_without_q.params) diff --git a/requirements-dev.txt b/requirements-dev.txt index 288ce30697..b64564a15e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,11 +1,11 @@ xarray>=0.16 mypy>=1.3 -black[jupyter]==24.4.0 +black[jupyter]==24.10.0 pytest>=7.3.0,<8 isort>=5.12 ipython matplotlib -ruff +ruff>=0.8.6 jupyterlab-code-formatter flake8 jupyter From fffcae3af04f7daeb8b6c557f6d716343c3cbed0 Mon Sep 17 00:00:00 2001 From: Kevin Sheppard Date: Wed, 8 Jan 2025 13:32:55 +0000 Subject: [PATCH 2/2] CI: Update to inlucde Python 3.12 Update CI jobs --- ci/azure_template_posix.yml | 4 ++++ ci/azure_template_windows.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/ci/azure_template_posix.yml b/ci/azure_template_posix.yml index be0286c18a..b77cc3600e 100644 --- a/ci/azure_template_posix.yml +++ b/ci/azure_template_posix.yml @@ -67,6 +67,10 @@ jobs: python.version: '3.12' XXHASH: true PYARROW: true + python313_latest: + python.version: '3.13' + XXHASH: true + PYARROW: true python312_copy_on_write: python.version: '3.12' XXHASH: true diff --git a/ci/azure_template_windows.yml b/ci/azure_template_windows.yml index 508fe7ae07..e802f1718d 100644 --- a/ci/azure_template_windows.yml +++ b/ci/azure_template_windows.yml @@ -22,6 +22,10 @@ jobs: python.version: '3.10' python311_win_latest: python.version: '3.11' + python312_win_latest: + python.version: '3.12' + python313_win_latest: + python.version: '3.13' maxParallel: 10 steps: