Repository: bashtage/linearmodels
Pull request #576 (2024-01-02 12:49:50)

MAINT: Add NumPy 2 Compat

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/bashtage/linearmodels/pull/576?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kevin+Sheppard) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`285e6f4`)](https://app.codecov.io/gh/bashtage/linearmodels/commit/285e6f41e931bea6733ba876f7e2d4549eba1371?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kevin+Sheppard) 99.59% compared to head [(`e637875`)](https://app.codecov.io/gh/bashtage/linearmodels/pull/576?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kevin+Sheppard) 99.60%.


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main     #576   +/-   ##
=======================================
  Coverage   99.59%   99.60%           
=======================================
  Files          98       99    +1     
  Lines       17238    17254   +16     
  Branches     2459     2460    +1     
=======================================
+ Hits        17169    17185   +16     
  Misses         30       30           
  Partials       39       39           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/bashtage/linearmodels/pull/576?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kevin+Sheppard).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kevin+Sheppard).


Patch:
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index d2105166a..6b4685902 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -10,7 +10,6 @@ variables:
   VML_NUM_THREADS: 1
   OPENBLAS_NUM_THREADS: 1
   PYTHONHASHSEED: 12345678 # Ensure tests are correctly gathered by xdist
-  SETUPTOOLS_USE_DISTUTILS: "stdlib"
   TEST_INSTALL: false
   MPLBACKEND: agg
   coverage: true
diff --git a/ci/azure_template_posix.yml b/ci/azure_template_posix.yml
index 234448f30..22231d966 100644
--- a/ci/azure_template_posix.yml
+++ b/ci/azure_template_posix.yml
@@ -60,10 +60,16 @@ jobs:
       python311_latest:
         python.version: '3.11'
         XXHASH: true
-      python311_copy_on_write:
-        python.version: '3.11'
+      python312_latest:
+        python.version: '3.12'
+        XXHASH: true
+      python312_copy_on_write:
+        python.version: '3.12'
         XXHASH: true
         LM_TEST_COPY_ON_WRITE: 1
+      python312_pre:
+        python.version: '3.12'
+        PIP_PRE: true
     maxParallel: 10
 
   steps:
diff --git a/ci/install-posix.sh b/ci/install-posix.sh
index 06073d6b3..11dc3272a 100644
--- a/ci/install-posix.sh
+++ b/ci/install-posix.sh
@@ -13,3 +13,9 @@ if [[ -n ${FORMULAIC} ]]; then CMD="$CMD formulaic~=${FORMULAIC}"; fi
 if [[ -n ${XXHASH} ]]; then CMD="$CMD xxhash"; fi
 echo "$CMD"
 eval "$CMD"
+
+if [ "${PIP_PRE}" = true ]; then
+  python -m pip uninstall -y numpy pandas scipy matplotlib statsmodels
+  python -m pip install -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy pandas scipy matplotlib --upgrade --use-deprecated=legacy-resolver
+  python -m pip install git+https://github.com/statsmodels/statsmodels.git --upgrade --no-build-isolation -v
+fi
\ No newline at end of file
diff --git a/linearmodels/compat/pandas.py b/linearmodels/compat/pandas.py
new file mode 100644
index 000000000..3b424f218
--- /dev/null
+++ b/linearmodels/compat/pandas.py
@@ -0,0 +1,9 @@
+from packaging.version import parse
+import pandas as pd
+
+PANDAS_VERSION = parse(pd.__version__)
+PD_GTE_21 = not (PANDAS_VERSION <= parse("2.0.99"))
+PD_LT_22 = PANDAS_VERSION <= parse("2.1.99")
+ANNUAL_FREQ = "A-DEC" if PD_LT_22 else "YE-DEC"
+
+__all__ = ["ANNUAL_FREQ", "PD_GTE_21", "PD_LT_22"]
diff --git a/linearmodels/iv/results.py b/linearmodels/iv/results.py
index 663ecdc9c..bde9620d2 100644
--- a/linearmodels/iv/results.py
+++ b/linearmodels/iv/results.py
@@ -764,7 +764,11 @@ class FirstStageResults(_SummaryStr):
         ]
         out_df = out_df[cols]
         for col in out_df:
-            out_df[col] = to_numeric(out_df[col], errors="ignore")
+            try:
+                out_df[col] = to_numeric(out_df[col])
+            except ValueError:
+                # If an error is raised, ignore and keep the column
+                pass
 
         return out_df
 
diff --git a/linearmodels/panel/data.py b/linearmodels/panel/data.py
index 64e7d4cf4..153bdff27 100644
--- a/linearmodels/panel/data.py
+++ b/linearmodels/panel/data.py
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+from linearmodels.compat.pandas import PD_GTE_21
+
 from collections.abc import Hashable, Sequence
 from itertools import product
 from typing import Literal, Union, cast, overload
@@ -227,7 +229,8 @@ class PanelData:
                 if copy:
                     self._frame = self._frame.copy()
             else:
-                self._frame = DataFrame({var_name: x.T.stack(dropna=False)})
+                options = {"future_stack": True} if PD_GTE_21 else {"dropna": False}
+                self._frame = DataFrame({var_name: x.T.stack(**options)})
         elif isinstance(x, np.ndarray):
             if x.ndim not in (2, 3):
                 raise ValueError("2 or 3-d array required for numpy input")
@@ -298,6 +301,8 @@ class PanelData:
             Boolean array indicating observations to drop with reference to
             the dataframe view of the data
         """
+        if isinstance(locs, Series):
+            locs = np.asarray(locs)
         self._frame = self._frame.loc[~locs.ravel()]
         self._frame = self._minimize_multiindex(self._frame)
         # Reset panel and shape after a drop
diff --git a/linearmodels/panel/utility.py b/linearmodels/panel/utility.py
index 1aa7a5a78..ec5a1c805 100644
--- a/linearmodels/panel/utility.py
+++ b/linearmodels/panel/utility.py
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+from linearmodels.compat.pandas import ANNUAL_FREQ
+
 from collections import defaultdict
 from typing import NamedTuple, TypeVar, cast
 
@@ -613,7 +615,7 @@ def generate_panel_data(
         x.flat[locs] = np.nan
 
     entities = [f"firm{i}" for i in range(n)]
-    time = [dt for dt in date_range("1-1-1900", periods=t, freq="A-DEC")]
+    time = [dt for dt in date_range("1-1-1900", periods=t, freq=ANNUAL_FREQ)]
     var_names = [f"x{i}" for i in range(k)]
     if const:
         var_names[1:] = var_names[:-1]
diff --git a/linearmodels/shared/hypotheses.py b/linearmodels/shared/hypotheses.py
index 18a55cc21..05b61c59b 100644
--- a/linearmodels/shared/hypotheses.py
+++ b/linearmodels/shared/hypotheses.py
@@ -117,13 +117,13 @@ class InvalidTestStatistic(WaldTestStatistic):
 
     def __init__(self, reason: str, *, name: str | None = None) -> None:
         self._reason = reason
-        super().__init__(np.NaN, "", df=1, df_denom=1, name=name)
+        super().__init__(np.nan, "", df=1, df_denom=1, name=name)
         self.dist_name = "None"
 
     @property
     def pval(self) -> float:
-        """Always returns np.NaN"""
-        return np.NaN
+        """Always returns np.nan"""
+        return np.nan
 
     @property
     def critical_values(self) -> None:
@@ -158,13 +158,13 @@ class InapplicableTestStatistic(WaldTestStatistic):
         if reason is None:
             self._reason = "Test is not applicable to model specification"
 
-        super().__init__(np.NaN, "", df=1, df_denom=1, name=name)
+        super().__init__(np.nan, "", df=1, df_denom=1, name=name)
         self.dist_name = "None"
 
     @property
     def pval(self) -> float:
-        """Always returns np.NaN"""
-        return np.NaN
+        """Always returns np.nan"""
+        return np.nan
 
     @property
     def critical_values(self) -> None:
diff --git a/pyproject.toml b/pyproject.toml
index 5d0d5e529..3426fa102 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ requires = [
   "wheel",
   "setuptools_scm[toml]>=7,<8",
   "oldest-supported-numpy",
-  "numpy; python_version>='3.12'",
+  "numpy; python_version>='3.13'",
   "cython>=0.29.34"
 ]
 build-backend = "setuptools.build_meta"


Test patch:
diff --git a/linearmodels/tests/panel/_utility.py b/linearmodels/tests/panel/_utility.py
index bbd771d38..9825f4c96 100644
--- a/linearmodels/tests/panel/_utility.py
+++ b/linearmodels/tests/panel/_utility.py
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+from linearmodels.compat.pandas import ANNUAL_FREQ
+
 from typing import Literal
 
 import numpy as np
@@ -121,7 +123,7 @@ def generate_data(
         return AttrDict(y=y, x=x, w=w, c=c, vc1=vc1, vc2=vc2)
 
     entities = ["firm" + str(i) for i in range(n)]
-    time = date_range("1-1-1900", periods=t, freq="A-DEC")
+    time = date_range("1-1-1900", periods=t, freq=ANNUAL_FREQ)
     var_names = ["x" + str(i) for i in range(k)]
     # y = DataFrame(y, index=time, columns=entities)
     y_df = panel_to_frame(
diff --git a/linearmodels/tests/shared/test_utility.py b/linearmodels/tests/shared/test_utility.py
index 76f383d39..abe6ae762 100644
--- a/linearmodels/tests/shared/test_utility.py
+++ b/linearmodels/tests/shared/test_utility.py
@@ -1,3 +1,5 @@
+from linearmodels.compat.pandas import ANNUAL_FREQ
+
 import pickle
 import random
 import string
@@ -224,7 +226,7 @@ def test_panel_to_midf():
             ],
         )
     )
-    times = pd.date_range("1999-12-31", freq="A-DEC", periods=7)
+    times = pd.date_range("1999-12-31", freq=ANNUAL_FREQ, periods=7)
     var_names = [f"x.{i}" for i in range(1, 4)]
     df3 = panel_to_frame(x, var_names, times, entities, True)
     mi = pd.MultiIndex.from_product([times, entities])
diff --git a/requirements-test.txt b/requirements-test.txt
index 9011e87a5..f05e387f5 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -2,6 +2,7 @@ black[jupyter]==23.11.0
 coverage
 flake8
 isort
+colorama
 matplotlib
 pytest>=7.3.0
 pytest-xdist