Coverage for /usr/lib/python3/dist-packages/scipy/stats/_variation.py: 9%
65 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1import numpy as np
2from numpy.core.multiarray import normalize_axis_index
3from scipy._lib._util import _nan_allsame, _contains_nan
4from ._stats_py import _chk_asarray
7def _nanvariation(a, *, axis=0, ddof=0, keepdims=False):
8 """
9 Private version of `variation` that ignores nan.
11 `a` must be a numpy array.
12 `axis` is assumed to be normalized, i.e. 0 <= axis < a.ndim.
13 """
14 #
15 # In theory, this should be as simple as something like
16 # nanstd(a, ddof=ddof, axis=axis, keepdims=keepdims) /
17 # nanmean(a, axis=axis, keepdims=keepdims)
18 # In practice, annoying issues arise. Specifically, numpy
19 # generates warnings in certain edge cases that we don't want
20 # to propagate to the user. Unfortunately, there does not
21 # appear to be a thread-safe way to filter out the warnings,
22 # so we have to do the calculation in a way that doesn't
23 # generate numpy warnings.
24 #
25 # Let N be the number of non-nan inputs in a slice.
26 # Conditions that generate nan:
27 # * empty input (i.e. N = 0)
28 # * All non-nan values 0
29 # * N < ddof
30 # * N == ddof and the input is constant
31 # Conditions that generate inf:
32 # * non-constant input and either
33 # * the mean is 0, or
34 # * N == ddof
35 #
36 a_isnan = np.isnan(a)
37 all_nan = a_isnan.all(axis=axis, keepdims=True)
38 all_nan_full = np.broadcast_to(all_nan, a.shape)
39 all_zero = (a_isnan | (a == 0)).all(axis=axis, keepdims=True) & ~all_nan
41 # ngood is the number of non-nan values in each slice.
42 ngood = (a.shape[axis] -
43 np.expand_dims(np.count_nonzero(a_isnan, axis=axis), axis))
44 # The return value is nan where ddof > ngood.
45 ddof_too_big = ddof > ngood
46 # If ddof == ngood, the return value is nan where the input is constant and
47 # inf otherwise.
48 ddof_equal_n = ddof == ngood
50 is_const = _nan_allsame(a, axis=axis, keepdims=True)
52 a2 = a.copy()
53 # If an entire slice is nan, `np.nanmean` will generate a warning,
54 # so we replace those nan's with 1.0 before computing the mean.
55 # We'll fix the corresponding output later.
56 a2[all_nan_full] = 1.0
57 mean_a = np.nanmean(a2, axis=axis, keepdims=True)
59 # If ddof >= ngood (the number of non-nan values in the slice), `np.nanstd`
60 # will generate a warning, so set all the values in such a slice to 1.0.
61 # We'll fix the corresponding output later.
62 a2[np.broadcast_to(ddof_too_big, a2.shape) | ddof_equal_n] = 1.0
63 with np.errstate(invalid='ignore'):
64 std_a = np.nanstd(a2, axis=axis, ddof=ddof, keepdims=True)
65 del a2
67 sum_zero = np.nansum(a, axis=axis, keepdims=True) == 0
69 # Where the sum along the axis is 0, replace mean_a with 1. This avoids
70 # division by zero. We'll fix the corresponding output later.
71 mean_a[sum_zero] = 1.0
73 # Here--finally!--is the calculation of the variation.
74 result = std_a / mean_a
76 # Now fix the values that were given fake data to avoid warnings.
77 result[~is_const & sum_zero] = np.inf
78 signed_inf_mask = ~is_const & ddof_equal_n
79 result[signed_inf_mask] = np.sign(mean_a[signed_inf_mask]) * np.inf
80 nan_mask = all_zero | all_nan | ddof_too_big | (ddof_equal_n & is_const)
81 result[nan_mask] = np.nan
83 if not keepdims:
84 result = np.squeeze(result, axis=axis)
85 if result.shape == ():
86 result = result[()]
88 return result
91def variation(a, axis=0, nan_policy='propagate', ddof=0, *, keepdims=False):
92 """
93 Compute the coefficient of variation.
95 The coefficient of variation is the standard deviation divided by the
96 mean. This function is equivalent to::
98 np.std(x, axis=axis, ddof=ddof) / np.mean(x)
100 The default for ``ddof`` is 0, but many definitions of the coefficient
101 of variation use the square root of the unbiased sample variance
102 for the sample standard deviation, which corresponds to ``ddof=1``.
104 The function does not take the absolute value of the mean of the data,
105 so the return value is negative if the mean is negative.
107 Parameters
108 ----------
109 a : array_like
110 Input array.
111 axis : int or None, optional
112 Axis along which to calculate the coefficient of variation.
113 Default is 0. If None, compute over the whole array `a`.
114 nan_policy : {'propagate', 'raise', 'omit'}, optional
115 Defines how to handle when input contains ``nan``.
116 The following options are available:
118 * 'propagate': return ``nan``
119 * 'raise': raise an exception
120 * 'omit': perform the calculation with ``nan`` values omitted
122 The default is 'propagate'.
123 ddof : int, optional
124 Gives the "Delta Degrees Of Freedom" used when computing the
125 standard deviation. The divisor used in the calculation of the
126 standard deviation is ``N - ddof``, where ``N`` is the number of
127 elements. `ddof` must be less than ``N``; if it isn't, the result
128 will be ``nan`` or ``inf``, depending on ``N`` and the values in
129 the array. By default `ddof` is zero for backwards compatibility,
130 but it is recommended to use ``ddof=1`` to ensure that the sample
131 standard deviation is computed as the square root of the unbiased
132 sample variance.
133 keepdims : bool, optional
134 If this is set to True, the axes which are reduced are left in the
135 result as dimensions with size one. With this option, the result
136 will broadcast correctly against the input array.
138 Returns
139 -------
140 variation : ndarray
141 The calculated variation along the requested axis.
143 Notes
144 -----
145 There are several edge cases that are handled without generating a
146 warning:
148 * If both the mean and the standard deviation are zero, ``nan``
149 is returned.
150 * If the mean is zero and the standard deviation is nonzero, ``inf``
151 is returned.
152 * If the input has length zero (either because the array has zero
153 length, or all the input values are ``nan`` and ``nan_policy`` is
154 ``'omit'``), ``nan`` is returned.
155 * If the input contains ``inf``, ``nan`` is returned.
157 References
158 ----------
159 .. [1] Zwillinger, D. and Kokoska, S. (2000). CRC Standard
160 Probability and Statistics Tables and Formulae. Chapman & Hall: New
161 York. 2000.
163 Examples
164 --------
165 >>> import numpy as np
166 >>> from scipy.stats import variation
167 >>> variation([1, 2, 3, 4, 5], ddof=1)
168 0.5270462766947299
170 Compute the variation along a given dimension of an array that contains
171 a few ``nan`` values:
173 >>> x = np.array([[ 10.0, np.nan, 11.0, 19.0, 23.0, 29.0, 98.0],
174 ... [ 29.0, 30.0, 32.0, 33.0, 35.0, 56.0, 57.0],
175 ... [np.nan, np.nan, 12.0, 13.0, 16.0, 16.0, 17.0]])
176 >>> variation(x, axis=1, ddof=1, nan_policy='omit')
177 array([1.05109361, 0.31428986, 0.146483 ])
179 """
180 a, axis = _chk_asarray(a, axis)
181 axis = normalize_axis_index(axis, ndim=a.ndim)
182 n = a.shape[axis]
184 contains_nan, nan_policy = _contains_nan(a, nan_policy)
185 if contains_nan and nan_policy == 'omit':
186 return _nanvariation(a, axis=axis, ddof=ddof, keepdims=keepdims)
188 if a.size == 0 or ddof > n:
189 # Handle as a special case to avoid spurious warnings.
190 # The return values, if any, are all nan.
191 shp = list(a.shape)
192 if keepdims:
193 shp[axis] = 1
194 else:
195 del shp[axis]
196 if len(shp) == 0:
197 result = np.nan
198 else:
199 result = np.full(shp, fill_value=np.nan)
201 return result
203 mean_a = a.mean(axis, keepdims=True)
205 if ddof == n:
206 # Another special case. Result is either inf or nan.
207 std_a = a.std(axis=axis, ddof=0, keepdims=True)
208 result = np.full_like(std_a, fill_value=np.nan)
209 result.flat[std_a.flat > 0] = (np.sign(mean_a) * np.inf).flat
210 if result.shape == ():
211 result = result[()]
212 return result
214 with np.errstate(divide='ignore', invalid='ignore'):
215 std_a = a.std(axis, ddof=ddof, keepdims=True)
216 result = std_a / mean_a
218 if not keepdims:
219 result = np.squeeze(result, axis=axis)
220 if result.shape == ():
221 result = result[()]
223 return result