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

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 

5 

6 

7def _nanvariation(a, *, axis=0, ddof=0, keepdims=False): 

8 """ 

9 Private version of `variation` that ignores nan. 

10 

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 

40 

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 

49 

50 is_const = _nan_allsame(a, axis=axis, keepdims=True) 

51 

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) 

58 

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 

66 

67 sum_zero = np.nansum(a, axis=axis, keepdims=True) == 0 

68 

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 

72 

73 # Here--finally!--is the calculation of the variation. 

74 result = std_a / mean_a 

75 

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 

82 

83 if not keepdims: 

84 result = np.squeeze(result, axis=axis) 

85 if result.shape == (): 

86 result = result[()] 

87 

88 return result 

89 

90 

91def variation(a, axis=0, nan_policy='propagate', ddof=0, *, keepdims=False): 

92 """ 

93 Compute the coefficient of variation. 

94 

95 The coefficient of variation is the standard deviation divided by the 

96 mean. This function is equivalent to:: 

97 

98 np.std(x, axis=axis, ddof=ddof) / np.mean(x) 

99 

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``. 

103 

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. 

106 

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: 

117 

118 * 'propagate': return ``nan`` 

119 * 'raise': raise an exception 

120 * 'omit': perform the calculation with ``nan`` values omitted 

121 

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. 

137 

138 Returns 

139 ------- 

140 variation : ndarray 

141 The calculated variation along the requested axis. 

142 

143 Notes 

144 ----- 

145 There are several edge cases that are handled without generating a 

146 warning: 

147 

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. 

156 

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. 

162 

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 

169 

170 Compute the variation along a given dimension of an array that contains 

171 a few ``nan`` values: 

172 

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 ]) 

178 

179 """ 

180 a, axis = _chk_asarray(a, axis) 

181 axis = normalize_axis_index(axis, ndim=a.ndim) 

182 n = a.shape[axis] 

183 

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) 

187 

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) 

200 

201 return result 

202 

203 mean_a = a.mean(axis, keepdims=True) 

204 

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 

213 

214 with np.errstate(divide='ignore', invalid='ignore'): 

215 std_a = a.std(axis, ddof=ddof, keepdims=True) 

216 result = std_a / mean_a 

217 

218 if not keepdims: 

219 result = np.squeeze(result, axis=axis) 

220 if result.shape == (): 

221 result = result[()] 

222 

223 return result