1"Alternative methods of calculating moving window statistics."
2
3import warnings
4
5import numpy as np
6
7__all__ = [
8 "move_sum",
9 "move_mean",
10 "move_std",
11 "move_var",
12 "move_min",
13 "move_max",
14 "move_argmin",
15 "move_argmax",
16 "move_median",
17 "move_rank",
18]
19
20
21def move_sum(a, window, min_count=None, axis=-1):
22 "Slow move_sum for unaccelerated dtype"
23 return move_func(np.nansum, a, window, min_count, axis=axis)
24
25
26def move_mean(a, window, min_count=None, axis=-1):
27 "Slow move_mean for unaccelerated dtype"
28 return move_func(np.nanmean, a, window, min_count, axis=axis)
29
30
31def move_std(a, window, min_count=None, axis=-1, ddof=0):
32 "Slow move_std for unaccelerated dtype"
33 return move_func(np.nanstd, a, window, min_count, axis=axis, ddof=ddof)
34
35
36def move_var(a, window, min_count=None, axis=-1, ddof=0):
37 "Slow move_var for unaccelerated dtype"
38 return move_func(np.nanvar, a, window, min_count, axis=axis, ddof=ddof)
39
40
41def move_min(a, window, min_count=None, axis=-1):
42 "Slow move_min for unaccelerated dtype"
43 return move_func(np.nanmin, a, window, min_count, axis=axis)
44
45
46def move_max(a, window, min_count=None, axis=-1):
47 "Slow move_max for unaccelerated dtype"
48 return move_func(np.nanmax, a, window, min_count, axis=axis)
49
50
51def move_argmin(a, window, min_count=None, axis=-1):
52 "Slow move_argmin for unaccelerated dtype"
53
54 def argmin(a, axis):
55 a = np.asarray(a)
56 flip = [slice(None)] * a.ndim
57 flip[axis] = slice(None, None, -1)
58 a = a[tuple(flip)] # if tie, pick index of rightmost tie
59 try:
60 idx = np.nanargmin(a, axis=axis)
61 except ValueError:
62 # an all nan slice encountered
63 a = a.copy()
64 mask = np.isnan(a)
65 np.copyto(a, np.inf, where=mask)
66 idx = np.argmin(a, axis=axis).astype(np.float64)
67 if idx.ndim == 0:
68 idx = np.nan
69 else:
70 mask = np.all(mask, axis=axis)
71 idx[mask] = np.nan
72 return idx
73
74 return move_func(argmin, a, window, min_count, axis=axis)
75
76
77def move_argmax(a, window, min_count=None, axis=-1):
78 "Slow move_argmax for unaccelerated dtype"
79
80 def argmax(a, axis):
81 a = np.asarray(a)
82 flip = [slice(None)] * a.ndim
83 flip[axis] = slice(None, None, -1)
84 a = a[tuple(flip)] # if tie, pick index of rightmost tie
85 try:
86 idx = np.nanargmax(a, axis=axis)
87 except ValueError:
88 # an all nan slice encountered
89 a = a.copy()
90 mask = np.isnan(a)
91 np.copyto(a, -np.inf, where=mask)
92 idx = np.argmax(a, axis=axis).astype(np.float64)
93 if idx.ndim == 0:
94 idx = np.nan
95 else:
96 mask = np.all(mask, axis=axis)
97 idx[mask] = np.nan
98 return idx
99
100 return move_func(argmax, a, window, min_count, axis=axis)
101
102
103def move_median(a, window, min_count=None, axis=-1):
104 "Slow move_median for unaccelerated dtype"
105 return move_func(np.nanmedian, a, window, min_count, axis=axis)
106
107
108def move_rank(a, window, min_count=None, axis=-1):
109 "Slow move_rank for unaccelerated dtype"
110 return move_func(lastrank, a, window, min_count, axis=axis)
111
112
113# magic utility functions ---------------------------------------------------
114
115
116def move_func(func, a, window, min_count=None, axis=-1, **kwargs):
117 "Generic moving window function implemented with a python loop."
118 a = np.asarray(a)
119 if min_count is None:
120 mc = window
121 else:
122 mc = min_count
123 if mc > window:
124 msg = "min_count (%d) cannot be greater than window (%d)"
125 raise ValueError(msg % (mc, window))
126 elif mc <= 0:
127 raise ValueError("`min_count` must be greater than zero.")
128 if a.ndim == 0:
129 raise ValueError("moving window functions require ndim > 0")
130 if axis is None:
131 raise ValueError("An `axis` value of None is not supported.")
132 if window < 1:
133 raise ValueError("`window` must be at least 1.")
134 if window > a.shape[axis]:
135 raise ValueError("`window` is too long.")
136 if issubclass(a.dtype.type, np.inexact):
137 y = np.empty_like(a)
138 else:
139 y = np.empty(a.shape)
140 idx1 = [slice(None)] * a.ndim
141 idx2 = list(idx1)
142 with warnings.catch_warnings():
143 warnings.simplefilter("ignore")
144 for i in range(a.shape[axis]):
145 win = min(window, i + 1)
146 idx1[axis] = slice(i + 1 - win, i + 1)
147 idx2[axis] = i
148 y[tuple(idx2)] = func(a[tuple(idx1)], axis=axis, **kwargs)
149 idx = _mask(a, window, mc, axis)
150 y[idx] = np.nan
151 return y
152
153
154def _mask(a, window, min_count, axis):
155 n = (a == a).cumsum(axis)
156 idx1 = [slice(None)] * a.ndim
157 idx2 = [slice(None)] * a.ndim
158 idx3 = [slice(None)] * a.ndim
159 idx1[axis] = slice(window, None)
160 idx2[axis] = slice(None, -window)
161 idx3[axis] = slice(None, window)
162 idx1 = tuple(idx1)
163 idx2 = tuple(idx2)
164 idx3 = tuple(idx3)
165 nidx1 = n[idx1]
166 nidx1 = nidx1 - n[idx2]
167 idx = np.empty(a.shape, dtype=np.bool_)
168 idx[idx1] = nidx1 < min_count
169 idx[idx3] = n[idx3] < min_count
170 return idx
171
172
173# ---------------------------------------------------------------------------
174
175
176def lastrank(a, axis=-1):
177 """
178 The ranking of the last element along the axis, ignoring NaNs.
179
180 The ranking is normalized to be between -1 and 1 instead of the more
181 common 1 and N. The results are adjusted for ties.
182
183 Parameters
184 ----------
185 a : ndarray
186 Input array. If `a` is not an array, a conversion is attempted.
187 axis : int, optional
188 The axis over which to rank. By default (axis=-1) the ranking
189 (and reducing) is performed over the last axis.
190
191 Returns
192 -------
193 d : array
194 In the case of, for example, a 2d array of shape (n, m) and
195 axis=1, the output will contain the rank (normalized to be between
196 -1 and 1 and adjusted for ties) of the the last element of each row.
197 The output in this example will have shape (n,).
198
199 Examples
200 --------
201 Create an array:
202
203 >>> y1 = larry([1, 2, 3])
204
205 What is the rank of the last element (the value 3 in this example)?
206 It is the largest element so the rank is 1.0:
207
208 >>> import numpy as np
209 >>> from la.afunc import lastrank
210 >>> x1 = np.array([1, 2, 3])
211 >>> lastrank(x1)
212 1.0
213
214 Now let's try an example where the last element has the smallest
215 value:
216
217 >>> x2 = np.array([3, 2, 1])
218 >>> lastrank(x2)
219 -1.0
220
221 Here's an example where the last element is not the minimum or maximum
222 value:
223
224 >>> x3 = np.array([1, 3, 4, 5, 2])
225 >>> lastrank(x3)
226 -0.5
227
228 """
229 a = np.asarray(a)
230 ndim = a.ndim
231 if a.size == 0:
232 # At least one dimension has length 0
233 shape = list(a.shape)
234 shape.pop(axis)
235 r = np.empty(shape, dtype=float)
236 r.fill(np.nan)
237 r = r.astype(a.dtype)
238 if (r.ndim == 0) and (r.size == 1):
239 r = np.nan
240 return r
241 indlast = [slice(None)] * ndim
242 indlast[axis] = slice(-1, None)
243 indlast = tuple(indlast)
244 indlast2 = [slice(None)] * ndim
245 indlast2[axis] = -1
246 indlast2 = tuple(indlast2)
247 n = (~np.isnan(a)).sum(axis)
248 a_indlast = a[indlast]
249 g = (a_indlast > a).sum(axis)
250 e = (a_indlast == a).sum(axis)
251 r = (g + g + e - 1.0) / 2.0
252 r = r / (n - 1.0)
253 r = 2.0 * (r - 0.5)
254 if ndim == 1:
255 if n == 1:
256 r = 0
257 if np.isnan(a[indlast2]): # elif?
258 r = np.nan
259 else:
260 np.putmask(r, n == 1, 0)
261 np.putmask(r, np.isnan(a[indlast2]), np.nan)
262 return r