performance tuning - How can I generate a 2n-point moving maximum of a list as efficiently as MaxFilter?
This is based on the prior question, Moving maximum function?, where @rasher provided two winning solutions (i.e. clearly the most efficient):
MaxFilter[list, 1]
and
Max /@ Transpose[{Rest[Append[#, 0]], #, Most[Prepend[#, 0]]}] &[list]
Taking the same example list,
{5, 6, 9, 3, 2, 6, 7, 8, 1, 1, 4, 7}
I'd like to be able to specify a window w such that a rolling maximum starts at index w, generating, for example for w=5 (lined up on purpose):
{9, 9, 9, 8, 8, 8, 8, 8}
That's not too hard; as @TylerDurden hinted, we can simply use MaxFilter
with range (w−1)/2=2 and drop each end ("shifting," as he imagined):
w=5;
Drop[Drop[MaxFilter[list, (w - 1)/2], (w - 1)/2], -(w - 1)/2]
(* {9, 9, 9, 8, 8, 8, 8, 8} *)
But of course, this can't be done with an even number w.
So, giving up, I decided to try to generalize @rasher's second solution—i.e. staggering w copies of the list (in his case, essentially hardcoded to w=3) and getting maximums across the transpose:
AnotherMaxFilter[list_, w_] :=
Max /@ (
Array[
ConstantArray[0, w - #]~Join~list &,
w
]~Flatten~{2}
)[[w~Range~Length@list, Range@w]]
Note: I'm using Flatten
to do a jagged transpose.
However, my attempt wasn't efficient at all:
list = RandomInteger[10, 10^7];
AbsoluteTiming[AnotherMaxFilter[list, 3]][[1]]
(* 5.257301 *)
AbsoluteTiming[AnotherMaxFilter[list, 5]][[1]]
(* 7.153409 *)
AbsoluteTiming[AnotherMaxFilter[list, 7]][[1]]
(* 14.786846 *)
Is there a way to improve my generalization to reach @rasher's original efficiency? Or, is there a clever way to get MaxFilter
to max over even-numbered windows?
I'd also very much appreciate if someone could explain what about my code could have introduced such inefficiency.
Answer
I found a clever way to make MaxFilter
work with even windows, and used conditionals to combine the odd- and even- cases. This solution is, surprisingly, more efficient than even @MrWizard's compiled function, and increasingly so as the window size increases.
MovingMax[list_, w_] := If[w > Length@list, {}, Module[{r, tmp},
r = Floor[w/2];
If[
OddQ@w
,
MaxFilter[list, r][[r + 1 ;; -(r + 1)]]
,
(* MaxFilter only supports odd windows; here's a hack for even windows. *)
tmp = MaxFilter[Riffle[list, Min@list, w], r][[r + 1 ;; -(r + 1)]];
If[
w > Length@tmp + 1
,
(* If window is greater than number of resulting elements, drop nothing. *)
tmp
,
Drop[tmp, {w, Length@tmp, w}]]
]
]
];
To create MovingMin
, it will work to just swap all references to Max
and Min
with the opposites.
If the window size w is odd, then we can just use MaxFilter
with a radius of (w−1)/2 and drop the same amount of elements (i.e. as the radius) from each end of the resulting list.
If the window size w is even, my idea was to insert a −∞ for every wth element, e.g. turning
{1, 2, 3, 4, 5, 6, 7}
into
{1, 2, 3, -∞, 4, 5, 6, -∞, 7}
if w were 4. Because then, MaxFilter
would essentially be finding the maximums of...
1, 2, 3 = 3 // incomplete list; drop
1, 2, 3, -∞ = 3 // incomplete list; drop
1, 2, 3, -∞, 4 = 4
2, 3, -∞, 4, 5 = 5
3, -∞, 4, 5, 6 = 6
-∞, 4, 5, 6, -∞ = 6 // incomplete list; drop
4, 5, 6, -∞, 7 = 7
5, 6, -∞, 7 = 7 // incomplete list; drop
6, -∞, 7 = 7 // incomplete list; drop
...so after dropping the first and last two elements (same as we do for the odd case), we just have to additionally drop every wth element, and we're left with the maximums we're looking for.
Later, I replaced −∞ with Min@list
as the former method was adding an inefficiency (see @MrWizard's comment).
Testing on a list of 20,000,000 random integers and varying window sizes, here were the results:
@MrWizard's
cf
performs well, but increases in time linearly with the window size.@rasher's
Partition
-based method ran my machine out of memory for larger window sizes. (But @rasher meant to improve on my inefficientAnotherMaxFilter
, not submit a serious competitor, so it's unfair to compare his solution; I only included it out of curiosity.)The
MaxFilter
functions (split between odd and even since they're essentially two unrelated functions), in contrast, decrease in time consumption as window size increases! I imagine this is becauseMaxFilter
probably optimizes by caching the index of its latest maximum as it runs, e.g.6 8 12 14 6 9 11 7 13 17 3 9 20 20 12 18 18 1 3 16
| |
+-------+where the first maximum is 14 and the cached index is 4. This way, as it moves forward, it only needs to compare one number, e.g.
6 8 12 14 6 9 11 7 13 17 3 9 20 20 12 18 18 1 3 16
| |
+-------+where since 6 is not greater than the current maximum of 14, there's no need to compare the new set of 4 elements. This can continue until the cached index "expires," e.g.
6 8 12 14 6 9 11 7 13 17 3 9 20 20 12 18 18 1 3 16
| |
+-------+when a new maximum must be calculated (e.g. 11, now caching index 7.)
With such an algorithm, greater window sizes would mean greater savings.
Original Code
MovingMax[list_, w_, lowerBound_: - Infinity] := Module[{r, tmp},
r = Floor[w/2];
If[
OddQ@w
,
Drop[Drop[MaxFilter[list, r], r], -r]
,
tmp = Drop[Drop[MaxFilter[Riffle[list, lowerBound, w], r], r], -r];
Drop[tmp, {w, Length@tmp, w}]
]
];
This code is much more efficient if given an actual lower bound, e.g. -10^-6
, rather than using -∞
. @MrWizard explains the reason in the comments.
Original Test Code
Here's the (unedited, messy) test code I used.
FCompiled =
Compile[{{x, _Integer, 1}, {n, _Integer}},
Module[{i = n,
a = Take[x, n]}, (a[[Mod[i, n, 1]]] = #; i++; Max[a]) & /@
Drop[x, n - 1]]];
FPartition = (Max /@ Partition[#1, #2, 1]) &;
FMaxFilterOdd =
If[OddQ@#2,
Drop[Drop[MaxFilter[#, (#2 - 1)/2], (#2 - 1)/2], -(#2 - 1)/2]] &;
FMaxFilterEven =
If[EvenQ@#2,
Module[{tmp},
Drop[tmp =
Drop[Drop[
MaxFilter[Riffle[#, -Infinity, #2], #2/2], #2/2], -#2/
2], {#2, Length@tmp, #2}]]] &;
list = RandomInteger[{-1*^6, 1*^6}, 100000];
timings = results = ConstantArray[Null, {4, 8}];
test[fn_, row_] := (
timings[[row, 1]] = ((results[[row, 1]] = fn[list, 3]); // AbsoluteTiming // First);
timings[[row, 2]] = ((results[[row, 2]] = fn[list, 6]); // AbsoluteTiming // First);
timings[[row, 3]] = ((results[[row, 3]] = fn[list, 9]); // AbsoluteTiming // First);
timings[[row, 4]] = ((results[[row, 4]] = fn[list, 12]); // AbsoluteTiming // First);
If[
! SameQ[fn, FPartition]
,
timings[[row, 5]] = ((results[[row, 5]] = fn[list, 301]); // AbsoluteTiming // First);
timings[[row, 6]] = ((results[[row, 6]] = fn[list, 602]); // AbsoluteTiming // First);
timings[[row, 7]] = ((results[[row, 7]] = fn[list, 903]); // AbsoluteTiming // First);
timings[[row, 8]] = ((results[[row, 8]] = fn[list, 1204]); // AbsoluteTiming // First);
]
);
test[FCompiled, 1];
test[FPartition, 2];
test[FMaxFilterOdd, 3];
test[FMaxFilterEven, 4];
TableForm[timings // Transpose,
TableHeadings -> {{3, 6, 9, 12, 301, 602, 903, 1204}, {"@MrWizard's",
"@rasher's", "MF-based (odd)", "MF-based (even)"}}]
Analysis of New Version
@MrWizard suggested two improvements: replacing Drop[Drop[..., r, -r]]
with [[r+1;;-r-1]]
, and using Min@list
as the lower bound. I ran some new tests:
The first improvement's impact was clear. The second improvement would of course slow it down (since computing a Min
can't be faster than a pre-supplied lower bound), but it was worth eliminating a parameter. Therefore both improvements have been incorporated into the new "tl;dr."
Comments
Post a Comment