test_variation.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import math
  2. import numpy as np
  3. import pytest
  4. from scipy.stats import variation
  5. from scipy._lib._util import AxisError
  6. from scipy._lib._array_api import make_xp_test_case, eager_warns
  7. from scipy._lib._array_api_no_0d import xp_assert_equal, xp_assert_close
  8. from scipy.stats._axis_nan_policy import (too_small_nd_omit, too_small_nd_not_omit,
  9. SmallSampleWarning)
  10. skip_xp_backends = pytest.mark.skip_xp_backends
  11. @make_xp_test_case(variation)
  12. class TestVariation:
  13. """
  14. Test class for scipy.stats.variation
  15. """
  16. def test_ddof(self, xp):
  17. x = xp.arange(9.0)
  18. xp_assert_close(variation(x, ddof=1), xp.asarray(math.sqrt(60/8)/4))
  19. @pytest.mark.parametrize('sgn', [1, -1])
  20. def test_sign(self, sgn, xp):
  21. x = xp.asarray([1., 2., 3., 4., 5.])
  22. v = variation(sgn*x)
  23. expected = xp.asarray(sgn*math.sqrt(2)/3)
  24. xp_assert_close(v, expected, rtol=1e-10)
  25. @skip_xp_backends(np_only=True, reason="test plain python scalar input")
  26. def test_scalar(self, xp):
  27. # A scalar is treated like a 1-d sequence with length 1.
  28. assert variation(4.0) == 0.0
  29. @pytest.mark.parametrize('nan_policy, expected',
  30. [('propagate', np.nan),
  31. ('omit', np.sqrt(20/3)/4)])
  32. @skip_xp_backends(np_only=True,
  33. reason='`nan_policy` only supports NumPy backend')
  34. def test_variation_nan(self, nan_policy, expected, xp):
  35. x = xp.arange(10.)
  36. x[9] = xp.nan
  37. xp_assert_close(variation(x, nan_policy=nan_policy), expected)
  38. @skip_xp_backends(np_only=True,
  39. reason='`nan_policy` only supports NumPy backend')
  40. def test_nan_policy_raise(self, xp):
  41. x = xp.asarray([1.0, 2.0, xp.nan, 3.0])
  42. with pytest.raises(ValueError, match='input contains nan'):
  43. variation(x, nan_policy='raise')
  44. @skip_xp_backends(np_only=True,
  45. reason='`nan_policy` only supports NumPy backend')
  46. def test_bad_nan_policy(self, xp):
  47. with pytest.raises(ValueError, match='must be one of'):
  48. variation([1, 2, 3], nan_policy='foobar')
  49. @skip_xp_backends(np_only=True,
  50. reason='`keepdims` only supports NumPy backend')
  51. def test_keepdims(self, xp):
  52. x = xp.reshape(xp.arange(10), (2, 5))
  53. y = variation(x, axis=1, keepdims=True)
  54. expected = np.array([[np.sqrt(2)/2],
  55. [np.sqrt(2)/7]])
  56. xp_assert_close(y, expected)
  57. @skip_xp_backends(np_only=True,
  58. reason='`keepdims` only supports NumPy backend')
  59. @pytest.mark.parametrize('axis, expected',
  60. [(0, np.empty((1, 0))),
  61. (1, np.full((5, 1), fill_value=np.nan))])
  62. def test_keepdims_size0(self, axis, expected, xp):
  63. x = xp.zeros((5, 0))
  64. if axis == 1:
  65. with pytest.warns(SmallSampleWarning, match=too_small_nd_not_omit):
  66. y = variation(x, axis=axis, keepdims=True)
  67. else:
  68. y = variation(x, axis=axis, keepdims=True)
  69. xp_assert_equal(y, expected)
  70. @skip_xp_backends(np_only=True,
  71. reason='`keepdims` only supports NumPy backend')
  72. @pytest.mark.parametrize('incr, expected_fill', [(0, np.inf), (1, np.nan)])
  73. def test_keepdims_and_ddof_eq_len_plus_incr(self, incr, expected_fill, xp):
  74. x = xp.asarray([[1, 1, 2, 2], [1, 2, 3, 3]])
  75. y = variation(x, axis=1, ddof=x.shape[1] + incr, keepdims=True)
  76. xp_assert_equal(y, xp.full((2, 1), fill_value=expected_fill))
  77. @skip_xp_backends(np_only=True,
  78. reason='`nan_policy` only supports NumPy backend')
  79. def test_propagate_nan(self, xp):
  80. # Check that the shape of the result is the same for inputs
  81. # with and without nans, cf gh-5817
  82. a = xp.reshape(xp.arange(8, dtype=float), (2, -1))
  83. a[1, 0] = xp.nan
  84. v = variation(a, axis=1, nan_policy="propagate")
  85. xp_assert_close(v, [math.sqrt(5/4)/1.5, xp.nan], atol=1e-15)
  86. @skip_xp_backends(np_only=True, reason='Python list input uses NumPy backend')
  87. def test_axis_none(self, xp):
  88. # Check that `variation` computes the result on the flattened
  89. # input when axis is None.
  90. y = variation([[0, 1], [2, 3]], axis=None)
  91. xp_assert_close(y, math.sqrt(5/4)/1.5)
  92. def test_bad_axis(self, xp):
  93. # Check that an invalid axis raises np.exceptions.AxisError.
  94. x = xp.asarray([[1, 2, 3], [4, 5, 6]])
  95. with pytest.raises((AxisError, IndexError)):
  96. variation(x, axis=10)
  97. @pytest.mark.filterwarnings("ignore:divide by zero encountered:RuntimeWarning:dask")
  98. def test_mean_zero(self, xp):
  99. # Check that `variation` returns inf for a sequence that is not
  100. # identically zero but whose mean is zero.
  101. x = xp.asarray([10., -3., 1., -4., -4.])
  102. y = variation(x)
  103. xp_assert_equal(y, xp.asarray(xp.inf))
  104. x2 = xp.stack([x, -10.*x])
  105. y2 = variation(x2, axis=1)
  106. xp_assert_equal(y2, xp.asarray([xp.inf, xp.inf]))
  107. @pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning:dask")
  108. @pytest.mark.parametrize('x', [[0.]*5, [1, 2, np.inf, 9]])
  109. def test_return_nan(self, x, xp):
  110. x = xp.asarray(x)
  111. # Test some cases where `variation` returns nan.
  112. y = variation(x)
  113. xp_assert_equal(y, xp.asarray(xp.nan, dtype=x.dtype))
  114. @pytest.mark.filterwarnings('ignore:Invalid value encountered:RuntimeWarning:dask')
  115. @pytest.mark.parametrize('axis, expected',
  116. [(0, []), (1, [np.nan]*3), (None, np.nan)])
  117. def test_2d_size_zero_with_axis(self, axis, expected, xp):
  118. x = xp.empty((3, 0))
  119. if axis != 0:
  120. # specific message depends on `axis`, and `SmallSampleWarning`
  121. # is specific enough.
  122. with eager_warns(SmallSampleWarning, xp=xp):
  123. y = variation(x, axis=axis)
  124. else:
  125. y = variation(x, axis=axis)
  126. xp_assert_equal(y, xp.asarray(expected))
  127. @pytest.mark.filterwarnings('ignore:divide by zero encountered:RuntimeWarning:dask')
  128. def test_neg_inf(self, xp):
  129. # Edge case that produces -inf: ddof equals the number of non-nan
  130. # values, the values are not constant, and the mean is negative.
  131. x1 = xp.asarray([-3., -5.])
  132. xp_assert_equal(variation(x1, ddof=2), xp.asarray(-xp.inf))
  133. @skip_xp_backends(np_only=True,
  134. reason='`nan_policy` only supports NumPy backend')
  135. def test_neg_inf_nan(self, xp):
  136. x2 = xp.asarray([[xp.nan, 1, -10, xp.nan],
  137. [-20, -3, xp.nan, xp.nan]])
  138. xp_assert_equal(variation(x2, axis=1, ddof=2, nan_policy='omit'),
  139. [-xp.inf, -xp.inf])
  140. @skip_xp_backends(np_only=True,
  141. reason='`nan_policy` only supports NumPy backend')
  142. @pytest.mark.parametrize("nan_policy", ['propagate', 'omit'])
  143. def test_combined_edge_cases(self, nan_policy, xp):
  144. x = xp.asarray([[0, 10, xp.nan, 1],
  145. [0, -5, xp.nan, 2],
  146. [0, -5, xp.nan, 3]])
  147. if nan_policy == 'omit':
  148. with pytest.warns(SmallSampleWarning, match=too_small_nd_omit):
  149. y = variation(x, axis=0, nan_policy=nan_policy)
  150. else:
  151. y = variation(x, axis=0, nan_policy=nan_policy)
  152. xp_assert_close(y, [xp.nan, xp.inf, xp.nan, math.sqrt(2/3)/2])
  153. @skip_xp_backends(np_only=True,
  154. reason='`nan_policy` only supports NumPy backend')
  155. @pytest.mark.parametrize(
  156. 'ddof, expected',
  157. [(0, [np.sqrt(1/6), np.sqrt(5/8), np.inf, 0, np.nan, 0.0, np.nan]),
  158. (1, [0.5, np.sqrt(5/6), np.inf, 0, np.nan, 0, np.nan]),
  159. (2, [np.sqrt(0.5), np.sqrt(5/4), np.inf, np.nan, np.nan, 0, np.nan])]
  160. )
  161. def test_more_nan_policy_omit_tests(self, ddof, expected, xp):
  162. # The slightly strange formatting in the follow array is my attempt to
  163. # maintain a clean tabular arrangement of the data while satisfying
  164. # the demands of pycodestyle. Currently, E201 and E241 are not
  165. # disabled by the `noqa` annotation.
  166. nan = xp.nan
  167. x = xp.asarray([[1.0, 2.0, nan, 3.0],
  168. [0.0, 4.0, 3.0, 1.0],
  169. [nan, -.5, 0.5, nan],
  170. [nan, 9.0, 9.0, nan],
  171. [nan, nan, nan, nan],
  172. [3.0, 3.0, 3.0, 3.0],
  173. [0.0, 0.0, 0.0, 0.0]])
  174. with pytest.warns(SmallSampleWarning, match=too_small_nd_omit):
  175. v = variation(x, axis=1, ddof=ddof, nan_policy='omit')
  176. xp_assert_close(v, expected)
  177. @skip_xp_backends(np_only=True,
  178. reason='`nan_policy` only supports NumPy backend')
  179. def test_variation_ddof(self, xp):
  180. # test variation with delta degrees of freedom
  181. # regression test for gh-13341
  182. a = xp.asarray([1., 2., 3., 4., 5.])
  183. nan_a = xp.asarray([1, 2, 3, xp.nan, 4, 5, xp.nan])
  184. y = variation(a, ddof=1)
  185. nan_y = variation(nan_a, nan_policy="omit", ddof=1)
  186. xp_assert_close(y, math.sqrt(5/2)/3)
  187. assert y == nan_y