Skip to content

Commit

Permalink
MAINT: stats.circ___: fix test failures; improvements based on furthe…
Browse files Browse the repository at this point in the history
…r consideration
  • Loading branch information
mdhaber committed Apr 27, 2024
1 parent 973480b commit 5e1cb66
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 86 deletions.
3 changes: 1 addition & 2 deletions scipy/_lib/_array_api.py
Expand Up @@ -292,8 +292,7 @@ def xp_assert_close(actual, desired, rtol=None, atol=0, check_namespace=True,
check_dtype=check_dtype, check_shape=check_shape)

xp_test = array_namespace(desired)
floating = (xp_test.isdtype(desired.dtype, 'real floating')
or xp_test.isdtype(desired.dtype, 'complex floating'))
floating = xp_test.isdtype(desired.dtype, ('real floating', 'complex floating'))
if rtol is None and floating:
rtol = xp_test.finfo(desired.dtype).eps**0.5
elif rtol is None:
Expand Down
10 changes: 5 additions & 5 deletions scipy/_lib/_util.py
Expand Up @@ -268,8 +268,8 @@ def check_random_state(seed):
if isinstance(seed, (np.random.RandomState, np.random.Generator)):
return seed

raise ValueError('%r cannot be used to seed a numpy.random.RandomState'
' instance' % seed)
raise ValueError(f'{seed} cannot be used to seed a numpy.random.RandomState'
' instance')


def _asarray_validated(a, check_finite=True,
Expand Down Expand Up @@ -718,8 +718,7 @@ def _contains_nan(a, nan_policy='propagate', use_summation=True,
if policies is None:
policies = ['propagate', 'raise', 'omit']
if nan_policy not in policies:
raise ValueError("nan_policy must be one of {%s}" %
', '.join("'%s'" % s for s in policies))
raise ValueError("nan_policy must be one of {policies}.")

inexact = (xp.isdtype(a.dtype, "real floating")
or xp.isdtype(a.dtype, "complex floating"))
Expand Down Expand Up @@ -820,7 +819,8 @@ def _get_nan(*data, xp=None):
# Get NaN of appropriate dtype for data
data = [xp.asarray(item) for item in data]
try:
dtype = xp.result_type(*data, xp.float32) # must be a float16 at least
min_float = getattr(xp, 'float16', xp.float32)
dtype = xp.result_type(*data, min_float) # must be at least a float
except DTypePromotionError:
# fallback to float64
dtype = xp.float64
Expand Down
18 changes: 9 additions & 9 deletions scipy/special/tests/test_support_alternative_backends.py
Expand Up @@ -16,15 +16,15 @@
HAVE_ARRAY_API_STRICT = False


# @pytest.mark.skipif(not HAVE_ARRAY_API_STRICT,
# reason="`array_api_strict` not installed")
# def test_dispatch_to_unrecognize_library():
# xp = array_api_strict
# f = get_array_special_func('ndtr', xp=xp, n_array_args=1)
# x = [1, 2, 3]
# res = f(xp.asarray(x))
# ref = xp.asarray(special.ndtr(np.asarray(x)))
# xp_assert_close(res, ref, xp=xp)
@pytest.mark.skipif(not HAVE_ARRAY_API_STRICT,
reason="`array_api_strict` not installed")
def test_dispatch_to_unrecognize_library():
xp = array_api_strict
f = get_array_special_func('ndtr', xp=xp, n_array_args=1)
x = [1, 2, 3]
res = f(xp.asarray(x))
ref = xp.asarray(special.ndtr(np.asarray(x)))
xp_assert_close(res, ref, xp=xp)


@array_api_compatible
Expand Down
15 changes: 6 additions & 9 deletions scipy/stats/_morestats.py
Expand Up @@ -130,8 +130,7 @@ def bayes_mvs(data, alpha=0.90):
"""
m, v, s = mvsdist(data)
if alpha >= 1 or alpha <= 0:
raise ValueError("0 < alpha < 1 is required, but alpha=%s was given."
% alpha)
raise ValueError(f"0 < alpha < 1 is required, but {alpha=} was given.")

m_res = Mean(m.mean(), m.interval(alpha))
v_res = Variance(v.mean(), v.interval(alpha))
Expand Down Expand Up @@ -454,7 +453,7 @@ def _parse_dist_kw(dist, enforce_subclass=True):
try:
dist = getattr(distributions, dist)
except AttributeError as e:
raise ValueError("%s is not a valid distribution name" % dist) from e
raise ValueError(f"{dist} is not a valid distribution name") from e
elif enforce_subclass:
msg = ("`dist` should be a stats.distributions instance or a string "
"with the name of such a distribution.")
Expand Down Expand Up @@ -831,7 +830,7 @@ def ppcc_plot(x, a, b, dist='tukeylambda', plot=None, N=80):
plot.plot(svals, ppcc, 'x')
_add_axis_labels_title(plot, xlabel='Shape Values',
ylabel='Prob Plot Corr. Coef.',
title='(%s) PPCC Plot' % dist)
title=f'({dist}) PPCC Plot')

return svals, ppcc

Expand Down Expand Up @@ -1323,7 +1322,7 @@ def _all(x):
'mle': _mle,
'all': _all}
if method not in methods.keys():
raise ValueError("Method %s not recognized." % method)
raise ValueError(f"Method {method} not recognized.")

optimfunc = methods[method]

Expand Down Expand Up @@ -4311,11 +4310,9 @@ def median_test(*samples, ties='below', correction=True, lambda_=1,
# a zero in the table of expected frequencies.
rowsums = table.sum(axis=1)
if rowsums[0] == 0:
raise ValueError("All values are below the grand median (%r)." %
grand_median)
raise ValueError(f"All values are below the grand median ({grand_median}).")
if rowsums[1] == 0:
raise ValueError("All values are above the grand median (%r)." %
grand_median)
raise ValueError(f"All values are above the grand median ({grand_median}).")
if ties == "ignore":
# We already checked that each sample has at least one value, but it
# is possible that all those values equal the grand median. If `ties`
Expand Down
95 changes: 34 additions & 61 deletions scipy/stats/tests/test_morestats.py
Expand Up @@ -21,7 +21,7 @@
from .._hypotests import _get_wilcoxon_distr, _get_wilcoxon_distr2
from scipy.stats._binomtest import _binary_search_for_binom_tst
from scipy.stats._distr_params import distcont
from scipy.conftest import array_api_compatible, skip_xp_invalid_arg
from scipy.conftest import array_api_compatible
from scipy._lib._array_api import (array_namespace, xp_assert_close, xp_assert_less,
SCIPY_ARRAY_API, is_torch, xp_assert_equal)

Expand Down Expand Up @@ -2451,23 +2451,23 @@ def test_darwin_example(self):
assert np.allclose(lmbda, 1.305, atol=1e-3)


@array_api_compatible
class TestCircFuncs:
# In gh-5747, the R package `circular` was used to calculate reference
# values for the circular variance, e.g.:
# library(circular)
# options(digits=16)
# x = c(0, 2*pi/3, 5*pi/3)
# var.circular(x)
@array_api_compatible
@pytest.mark.parametrize("test_func,expected",
[(stats.circmean, 0.167690146),
(stats.circvar, 0.006455174270186603),
(stats.circstd, 6.520702116)])
def test_circfuncs(self, test_func, expected, xp):
x = xp.asarray([355., 5., 2., 359., 10., 350.], dtype=xp.float64)
xp_assert_close(test_func(x, high=360), xp.asarray(expected, dtype=xp.float64))
xp_assert_close(test_func(x, high=360), xp.asarray(expected, dtype=xp.float64),
rtol=1e-7)

@array_api_compatible
def test_circfuncs_small(self, xp):
x = xp.asarray([20, 21, 22, 18, 19, 20.5, 19.2], dtype=xp.float64)
M1 = xp.mean(x)
Expand All @@ -2487,7 +2487,6 @@ def test_circfuncs_small(self, xp):
S2 = stats.circstd(x, high=360)
xp_assert_close(S2, S1, rtol=1e-4)

@array_api_compatible
@pytest.mark.parametrize("test_func, numpy_func",
[(stats.circmean, np.mean),
(stats.circvar, np.var),
Expand All @@ -2502,7 +2501,6 @@ def test_circfuncs_close(self, test_func, numpy_func, xp):
@pytest.mark.parametrize('circfunc', [stats.circmean,
stats.circvar,
stats.circstd])
@array_api_compatible
def test_circmean_axis(self, xp, circfunc):
x = xp.asarray([[355, 5, 2, 359, 10, 350],
[351, 7, 4, 352, 9, 349],
Expand All @@ -2517,9 +2515,8 @@ def test_circmean_axis(self, xp, circfunc):

res = circfunc(x, high=360, axis=0)
ref = [circfunc(x[:, i], high=360) for i in range(x.shape[1])]
xp_assert_close(res, xp.asarray(ref), rtol=1e-14)
xp_assert_close(res, xp.asarray(ref), rtol=1e-12)

@array_api_compatible
@pytest.mark.parametrize("test_func,expected",
[(stats.circmean, 0.167690146),
(stats.circvar, 0.006455174270186603),
Expand All @@ -2529,22 +2526,19 @@ def test_circfuncs_array_like(self, test_func, expected, xp):
rtol = 2e-5 if is_torch(xp) else 1e-7
xp_assert_close(test_func(x, high=360), xp.asarray(expected), rtol=rtol)

@array_api_compatible
@pytest.mark.parametrize("test_func", [stats.circmean, stats.circvar,
stats.circstd])
def test_empty(self, test_func, xp):
dtype = xp.float64
x = xp.asarray([], dtype=dtype)
xp_assert_equal(test_func(x), xp.asarray(xp.nan, dtype=dtype))

@array_api_compatible
@pytest.mark.parametrize("test_func", [stats.circmean, stats.circvar,
stats.circstd])
def test_nan_propagate(self, test_func, xp):
x = xp.asarray([355, 5, 2, 359, 10, 350, np.nan])
xp_assert_equal(test_func(x, high=360), xp.asarray(xp.nan))

@array_api_compatible
@pytest.mark.parametrize("test_func,expected",
[(stats.circmean,
{None: np.nan, 0: 355.66582264, 1: 0.28725053}),
Expand All @@ -2567,10 +2561,34 @@ def test_nan_propagate_array(self, test_func, expected, xp):
xp_assert_close(out[0], xp.asarray(expected[axis]), rtol=rtol)
xp_assert_equal(out[1:], xp.full_like(out[1:], xp.nan))

@pytest.mark.skip_xp_backends(np_only=True,
reasons=["Only NumPy has nan_policy='omit' for now"])
@pytest.mark.usefixtures("skip_xp_backends")
@array_api_compatible
def test_circmean_scalar(self, xp):
x = xp.asarray(1.)[()]
M1 = x
M2 = stats.circmean(x)
xp_assert_close(M2, M1, rtol=1e-5)

def test_circmean_range(self, xp):
# regression test for gh-6420: circmean(..., high, low) must be
# between `high` and `low`
m = stats.circmean(xp.arange(0, 2, 0.1), xp.pi, -xp.pi)
xp_assert_less(m, xp.asarray(xp.pi))
xp_assert_less(-m, xp.asarray(xp.pi))

def test_circfuncs_uint8(self, xp):
# regression test for gh-7255: overflow when working with
# numpy uint8 data type
x = xp.asarray([150, 10], dtype=xp.uint8)
xp_assert_close(stats.circmean(x, high=180), xp.asarray(170.0))
xp_assert_close(stats.circvar(x, high=180), xp.asarray(0.2339555554617))
xp_assert_close(stats.circstd(x, high=180), xp.asarray(20.91551378))


class TestCircFuncsNanPolicy:
# `nan_policy` is implemented by the `_axis_nan_policy` decorator, which is
# not yet array-API compatible. When it is array-API compatible, the generic
# tests run on every function will be much stronger than these, so these
# will not be necessary. So I don't see a need to make these array-API compatible;
# when the time comes, they can just be removed.
@pytest.mark.parametrize("test_func,expected",
[(stats.circmean,
{None: 359.4178026893944,
Expand All @@ -2590,7 +2608,7 @@ def test_nan_propagate_array(self, test_func, expected, xp):
3.50108929, 0.50000317,
0.50000317]),
1: np.array([6.52070212, 8.19138093])})])
def test_nan_omit_array(self, test_func, expected, xp):
def test_nan_omit_array(self, test_func, expected):
x = np.array([[355, 5, 2, 359, 10, 350, np.nan],
[351, 7, 4, 352, 9, 349, np.nan],
[np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan]])
Expand All @@ -2602,10 +2620,6 @@ def test_nan_omit_array(self, test_func, expected, xp):
assert_allclose(out[:-1], expected[axis], rtol=1e-7)
assert_(np.isnan(out[-1]))

@pytest.mark.skip_xp_backends(np_only=True,
reasons=["Only NumPy has nan_policy='omit' for now"])
@pytest.mark.usefixtures("skip_xp_backends")
@array_api_compatible
@pytest.mark.parametrize("test_func,expected",
[(stats.circmean, 0.167690146),
(stats.circvar, 0.006455174270186603),
Expand All @@ -2614,21 +2628,12 @@ def test_nan_omit(self, test_func, expected):
x = [355, 5, 2, 359, 10, 350, np.nan]
assert_allclose(test_func(x, high=360, nan_policy='omit'),
expected, rtol=1e-7)

@pytest.mark.skip_xp_backends(np_only=True,
reasons=["Only NumPy has nan_policy='omit' for now"])
@pytest.mark.usefixtures("skip_xp_backends")
@array_api_compatible
@pytest.mark.parametrize("test_func", [stats.circmean, stats.circvar,
stats.circstd])
def test_nan_omit_all(self, test_func):
x = [np.nan, np.nan, np.nan, np.nan, np.nan]
assert_(np.isnan(test_func(x, nan_policy='omit')))

@pytest.mark.skip_xp_backends(np_only=True,
reasons=["Only NumPy has nan_policy='omit' for now"])
@pytest.mark.usefixtures("skip_xp_backends")
@array_api_compatible
@pytest.mark.parametrize("test_func", [stats.circmean, stats.circvar,
stats.circstd])
def test_nan_omit_all_axis(self, test_func):
Expand All @@ -2638,10 +2643,6 @@ def test_nan_omit_all_axis(self, test_func):
assert_(np.isnan(out).all())
assert_(len(out) == 2)

@pytest.mark.skip_xp_backends(np_only=True,
reasons=["Only NumPy has nan_policy='omit' for now"])
@pytest.mark.usefixtures("skip_xp_backends")
@array_api_compatible
@pytest.mark.parametrize("x",
[[355, 5, 2, 359, 10, 350, np.nan],
np.array([[355, 5, 2, 359, 10, 350, np.nan],
Expand All @@ -2651,10 +2652,6 @@ def test_nan_omit_all_axis(self, test_func):
def test_nan_raise(self, test_func, x):
assert_raises(ValueError, test_func, x, high=360, nan_policy='raise')

@pytest.mark.skip_xp_backends(np_only=True,
reasons=["Only NumPy has nan_policy='omit' for now"])
@pytest.mark.usefixtures("skip_xp_backends")
@array_api_compatible
@pytest.mark.parametrize("x",
[[355, 5, 2, 359, 10, 350, np.nan],
np.array([[355, 5, 2, 359, 10, 350, np.nan],
Expand All @@ -2664,30 +2661,6 @@ def test_nan_raise(self, test_func, x):
def test_bad_nan_policy(self, test_func, x):
assert_raises(ValueError, test_func, x, high=360, nan_policy='foobar')

@skip_xp_invalid_arg
def test_circmean_scalar(self):
x = 1.
M1 = x
M2 = stats.circmean(x)
assert_allclose(M2, M1, rtol=1e-5)

@array_api_compatible
def test_circmean_range(self, xp):
# regression test for gh-6420: circmean(..., high, low) must be
# between `high` and `low`
m = stats.circmean(xp.arange(0, 2, 0.1), xp.pi, -xp.pi)
xp_assert_less(m, xp.asarray(xp.pi))
xp_assert_less(-m, xp.asarray(xp.pi))

@array_api_compatible
def test_circfuncs_uint8(self, xp):
# regression test for gh-7255: overflow when working with
# numpy uint8 data type
x = xp.asarray([150, 10], dtype=xp.uint8)
xp_assert_close(stats.circmean(x, high=180), xp.asarray(170.0))
xp_assert_close(stats.circvar(x, high=180), xp.asarray(0.2339555554617))
xp_assert_close(stats.circstd(x, high=180), xp.asarray(20.91551378))


class TestMedianTest:

Expand Down

0 comments on commit 5e1cb66

Please sign in to comment.