Repository: scipy/scipy
Pull request: MAINT: stats.wilcoxon: fix attempt to access np.AxisError
#### Reference issue
Closes gh-22051

#### What does this implement/fix?
`scipy.stats.wilcoxon` attempted to raise `np.AxisError`, but `AxisError` is located in `np.exceptions` rather than the base NumPy namespace in recent versions of NumPy. This fixes the problem with ~~`getattr(np, 'AxisError', np.exceptions.AxisError)`~~`getattr(np, 'AxisError', None) or np.exceptions.AxisError`.


Problem statement:
BUG: AttributeError: module 'numpy' has no attribute 'AxisError' in stats.wilcoxon
### Describe your issue.

Hi,
There is a bug using scipy.stats.wilcoxon on vectors with different sizes.
The try/catch uses np.AxisError while it should be (I guess) np.exceptions.AxisError.

### Reproducing Code Example

```python
from scipy import stats

a = [1, 2, 3, 4]
b = [1, 2, 3]
res = stats.wilcoxon(a, b)
```


### Error message

```shell
File scipy\stats\_wilcoxon.py:69, in _wilcoxon_iv(x, y, zero_method, correction, alternative, method, axis)
AttributeError: module 'numpy' has no attribute 'AxisError'
```


### SciPy/NumPy/Python version and system information

```shell
1.14.1 2.1.3 sys.version_info(major=3, minor=12, micro=7, releaselevel='final', serial=0)
Build Dependencies:
  blas:
    detection method: pkgconfig
    found: true
    include directory: C:/Users/runneradmin/AppData/Local/Temp/cibw-run-do_lqnow/cp312-win_amd64/build/venv/Lib/site-packages/scipy_openblas32/include
    lib directory: C:/Users/runneradmin/AppData/Local/Temp/cibw-run-do_lqnow/cp312-win_amd64/build/venv/Lib/site-packages/scipy_openblas32/lib
    name: scipy-openblas
    openblas configuration: OpenBLAS 0.3.27.dev DYNAMIC_ARCH NO_AFFINITY Zen MAX_THREADS=24
    pc file directory: D:/a/scipy/scipy
    version: 0.3.27.dev
  lapack:
    detection method: pkgconfig
    found: true
    include directory: C:/Users/runneradmin/AppData/Local/Temp/cibw-run-do_lqnow/cp312-win_amd64/build/venv/Lib/site-packages/scipy_openblas32/include
    lib directory: C:/Users/runneradmin/AppData/Local/Temp/cibw-run-do_lqnow/cp312-win_amd64/build/venv/Lib/site-packages/scipy_openblas32/lib
    name: scipy-openblas
    openblas configuration: OpenBLAS 0.3.27.dev DYNAMIC_ARCH NO_AFFINITY Zen MAX_THREADS=24
    pc file directory: D:/a/scipy/scipy
    version: 0.3.27.dev
  pybind11:
    detection method: config-tool
    include directory: unknown
    name: pybind11
    version: 2.12.0
Compilers:
  c:
    commands: cc
    linker: ld.bfd
    name: gcc
    version: 10.3.0
  c++:
    commands: c++
    linker: ld.bfd
    name: gcc
    version: 10.3.0
  cython:
    commands: cython
    linker: cython
    name: cython
    version: 3.0.11
  fortran:
    commands: gfortran
    linker: ld.bfd
    name: gcc
    version: 10.3.0
  pythran:
    include directory: C:\Users\runneradmin\AppData\Local\Temp\pip-build-env-sf8gymnb\overlay\Lib\site-packages/pythran       
    version: 0.16.1
Machine Information:
  build:
    cpu: x86_64
    endian: little
    family: x86_64
    system: windows
  cross-compiled: false
  host:
    cpu: x86_64
    endian: little
    family: x86_64
    system: windows
Python Information:
  path: C:\Users\runneradmin\AppData\Local\Temp\cibw-run-do_lqnow\cp312-win_amd64\build\venv\Scripts\python.exe
  version: '3.12'
```

Hints from issue discussion:
Thanks. This a NumPy version-dependent thing. I'll fix this today.
Actually, this is already fixed, at least publicly.
```python3
res = stats.wilcoxon(a, b)
# ValueError: Array shapes are incompatible for broadcasting.
res = stats.wilcoxon([1, 2, 3], [4, 5, 6], axis=3)
# numpy.exceptions.AxisError: source: axis 3 is out of bounds for array of dimension 1
```
We get the error about missing `AxisErrorr` if we pass `_no_deco=True`, so it's not actually fixed in `_wilcoxon_iv` but in the axis/nan-policy decorator. I'm not sure if there are any cases that might be missed by the decorator, so I'll go ahead and fix it in `_wilcoxon_iv`.

Commit message: MAINT: stats.wilcoxon: fix attempt to access np.AxisError

Patch to review:
diff --git a/scipy/stats/_wilcoxon.py b/scipy/stats/_wilcoxon.py
index bdb475614d7d..fe1383a32d34 100644
--- a/scipy/stats/_wilcoxon.py
+++ b/scipy/stats/_wilcoxon.py
@@ -57,6 +57,7 @@ def _wilcoxon_iv(x, y, zero_method, correction, alternative, method, axis):
         raise ValueError(message)
 
     message = '`axis` must be compatible with the shape(s) of `x` (and `y`)'
+    AxisError = getattr(np, 'AxisError', np.exceptions.AxisError)
     try:
         if y is None:
             x = np.asarray(x)
@@ -65,8 +66,8 @@ def _wilcoxon_iv(x, y, zero_method, correction, alternative, method, axis):
             x, y = _broadcast_arrays((x, y), axis=axis)
             d = x - y
         d = np.moveaxis(d, axis, -1)
-    except np.AxisError as e:
-        raise ValueError(message) from e
+    except AxisError as e:
+        raise AxisError(message) from e
 
     message = "`x` and `y` must have the same length along `axis`."
     if y is not None and x.shape[axis] != y.shape[axis]:
diff --git a/scipy/stats/tests/test_morestats.py b/scipy/stats/tests/test_morestats.py
index 253ed563e4bc..aeea4c9f4d4c 100644
--- a/scipy/stats/tests/test_morestats.py
+++ b/scipy/stats/tests/test_morestats.py
@@ -1768,6 +1768,27 @@ def test_all_zeros_exact(self, method):
         res = stats.wilcoxon(np.zeros(5), method=method)
         assert_allclose(res, [0, 1])
 
+    def test_wilcoxon_axis_broadcasting_errors_gh22051(self):
+        # In previous versions of SciPy, `wilcoxon` gave an incorrect error
+        # message when `AxisError` was not found in the base NumPy namespace.
+        # Check that this is resolved with and without the ANP decorator.
+        message = "Array shapes are incompatible for broadcasting."
+        with pytest.raises(ValueError, match=message):
+            stats.wilcoxon([1, 2, 3], [4, 5])
+
+        message = "operands could not be broadcast together with..."
+        with pytest.raises(ValueError, match=message):
+            stats.wilcoxon([1, 2, 3], [4, 5], _no_deco=True)
+
+        AxisError = getattr(np, 'AxisError', np.exceptions.AxisError)
+        message = "source: axis 3 is out of bounds for array of dimension 1"
+        with pytest.raises(AxisError, match=message):
+            stats.wilcoxon([1, 2, 3], [4, 5, 6], axis=3)
+
+        message = "`axis` must be compatible with the shape..."
+        with pytest.raises(AxisError, match=message):
+            stats.wilcoxon([1, 2, 3], [4, 5, 6], axis=3, _no_deco=True)
+
 
 # data for k-statistics tests from
 # https://cran.r-project.org/web/packages/kStatistics/kStatistics.pdf


Code review comments:
diff_hunk: @@ -1768,6 +1768,27 @@ def test_all_zeros_exact(self, method):
         res = stats.wilcoxon(np.zeros(5), method=method)
         assert_allclose(res, [0, 1])
 
+    def test_wilcoxon_axis_broadcasting_errors_gh22051(self):
+        # In previous versions of SciPy, `wilcoxon` gave an incorrect error
+        # message when `AxisError` was not found in the base NumPy namespace.
+        # Check that this is resolved with and without the ANP decorator.
+        message = "Array shapes are incompatible for broadcasting."
+        with pytest.raises(ValueError, match=message):
+            stats.wilcoxon([1, 2, 3], [4, 5])
+
+        message = "operands could not be broadcast together with..."
+        with pytest.raises(ValueError, match=message):
+            stats.wilcoxon([1, 2, 3], [4, 5], _no_deco=True)
+
+        AxisError = getattr(np, 'AxisError', np.exceptions.AxisError)
path: scipy/stats/tests/test_morestats.py
text: @author:
```suggestion
        AxisError = getattr(np, 'AxisError', None) or np.exceptions.AxisError
```

diff_hunk: @@ -57,6 +57,7 @@ def _wilcoxon_iv(x, y, zero_method, correction, alternative, method, axis):
         raise ValueError(message)
 
     message = '`axis` must be compatible with the shape(s) of `x` (and `y`)'
+    AxisError = getattr(np, 'AxisError', np.exceptions.AxisError)
path: scipy/stats/_wilcoxon.py
text: @author:
```suggestion
    AxisError = getattr(np, 'AxisError', None) or np.exceptions.AxisError
```