Repository: bashtage/linearmodels

Issue:
Error in SUR with a constraint if `q` is used
Thanks for a great package. In the following SUR code with a constraint , an error occurs if `q` is used in `model.add_constraints(r, q)`, but no error for `model.add_constraints(r)`. I know that `q` is optional and `q` is set to zero if it is not used. But I thought it might be a good idea to report this. 

```
from linearmodels.system import SUR
import pandas as pd
import numpy as np

n = 100
x1 = np.random.normal(size=n)
x2 = np.random.normal(size=n)
x3 = np.random.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 = pd.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 = pd.DataFrame([[0]*4],
                 columns=model.param_names,
                 index=["rest"],
                 dtype=np.float64)
r.iloc[0,0] = -1
r.iloc[0,2] = 1

# ERROR occurs if q is used
# the error remains if the value other `0` is used.
# q = pd.Series([0])
# model.add_constraints(r, q)

# No error without q
model.add_constraints(r)

results = model.fit()
```

Hints from issue comments:
@bashtage: Thanks for the report.  Just a small change in some pandas code is needed. 

As a workaround, if you reaplce `q = pd.Series([0])` with `q = pd.Series([0], index=r.index)` it will work.


Solution patch:
diff --git a/ci/azure_template_posix.yml b/ci/azure_template_posix.yml
index be0286c18..b77cc3600 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 508fe7ae0..e802f1718 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:
diff --git a/linearmodels/panel/model.py b/linearmodels/panel/model.py
index 1071e13d0..f1a60e5cc 100644
--- a/linearmodels/panel/model.py
+++ b/linearmodels/panel/model.py
@@ -771,9 +771,7 @@ class _PanelModelBase:
                 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 @@ class BetweenOLS(_PanelModelBase):
                 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 96aa544cc..16580e0a6 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 1dd5186f6..f79be8cfa 100644
--- a/linearmodels/system/_utility.py
+++ b/linearmodels/system/_utility.py
@@ -262,7 +262,8 @@ class LinearConstraint:
                 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 7c854e2f1..6c067b613 100644
--- a/linearmodels/system/model.py
+++ b/linearmodels/system/model.py
@@ -96,7 +96,7 @@ GMM_COV_EST = {
 
 
 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/requirements-dev.txt b/requirements-dev.txt
index 288ce3069..b64564a15 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


Test patch:
diff --git a/linearmodels/tests/system/test_sur.py b/linearmodels/tests/system/test_sur.py
index 03c410262..178499c70 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)