The OpenD Programming Language

1 /++
2 This module contains algorithms for univariate descriptive statistics.
3 
4 Note that used specialized summing algorithms execute more primitive operations
5 than vanilla summation. Therefore, if in certain cases maximum speed is required
6 at expense of precision, one can use $(REF_ALTTEXT $(TT Summation.fast), Summation.fast, mir, math, sum)$(NBSP).
7 
8 $(SCRIPT inhibitQuickIndex = 1;)
9 $(DIVC quickindex,
10 $(BOOKTABLE,
11 $(TR $(TH Category) $(TH Symbols))
12     $(TR $(TD Location) $(TD
13         $(LREF gmean)
14         $(LREF hmean)
15         $(LREF mean)
16         $(LREF median)
17     ))
18     $(TR $(TD Deviation) $(TD
19         $(LREF dispersion)
20         $(LREF entropy)
21         $(LREF interquartileRange)
22         $(LREF medianAbsoluteDeviation)
23         $(LREF quantile)
24         $(LREF standardDeviation)
25         $(LREF variance)
26     ))
27     $(TR $(TD Higher Moments, etc.) $(TD
28         $(LREF kurtosis)
29         $(LREF skewness)
30     ))
31     $(TR $(TD Other Moment Functions) $(TD
32         $(LREF centralMoment)
33         $(LREF coefficientOfVariation)
34         $(LREF moment)
35         $(LREF rawMoment)
36         $(LREF standardizedMoment)
37     ))
38     $(TR $(TD Accumulators) $(TD
39         $(LREF EntropyAccumulator)
40         $(LREF GMeanAccumulator)
41         $(LREF KurtosisAccumulator)
42         $(LREF MeanAccumulator)
43         $(LREF MomentAccumulator)
44         $(LREF SkewnessAccumulator)
45         $(LREF VarianceAccumulator)
46     ))
47     $(TR $(TD Algorithms) $(TD
48         $(LREF KurtosisAlgo)
49         $(LREF MomentAlgo)
50         $(LREF QuantileAlgo)
51         $(LREF SkewnessAlgo)
52         $(LREF StandardizedMomentAlgo)
53         $(LREF VarianceAlgo)
54     ))
55     $(TR $(TD Types) $(TD
56         $(LREF entropyType)
57         $(LREF gmeanType)
58         $(LREF hmeanType)
59         $(LREF meanType)
60         $(LREF quantileType)
61         $(LREF statType)
62         $(LREF stdevType)
63     ))
64 ))
65 
66 License: $(HTTP www.apache.org/licenses/LICENSE-2.0, Apache-2.0)
67 
68 Several functions are borrowed from 
69 $(HTTP mir-algorithm.$(MIR_SITE)/mir_math_stat.html, mir.math.stat). An additional
70 $(LREF VarianceAlgo) is provided in this code, which is the new default.
71 
72 Authors: John Michael Hall, Ilya Yaroshenko
73 
74 Copyright: 2022-3 Mir Stat Authors.
75 
76 Macros:
77 SUBREF = $(REF_ALTTEXT $(TT $2), $2, mir, stat, $1)$(NBSP)
78 MATHREF = $(GREF_ALTTEXT mir-algorithm, $(TT $2), $2, mir, math, $1)$(NBSP)
79 MATHREF_ALT = $(GREF_ALTTEXT mir-algorithm, $(B $(TT $2)), $2, mir, math, $1)$(NBSP)
80 NDSLICEREF = $(GREF_ALTTEXT mir-algorithm, $(TT $2), $2, mir, ndslice, $1)$(NBSP)
81 T2=$(TR $(TDNW $(LREF $1)) $(TD $+))
82 T3=$(TR $(TDNW $(LREF $1)) $(TD $2) $(TD $3))
83 T4=$(TR $(TDNW $(LREF $1)) $(TD $2) $(TD $3) $(TD $4))
84 +/
85 
86 module mir.stat.descriptive.univariate;
87 
88 ///
89 public import mir.math.sum: Summation;
90 
91 import mir.internal.utility: isFloatingPoint;
92 import mir.math.common: fmamath;
93 import mir.math.sum: Summator, ResolveSummationType;
94 import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind;
95 import std.traits: isIterable, isMutable;
96 
97 ///
98 package(mir)
99 template statType(T, bool checkComplex = true)
100 {
101     import mir.internal.utility: isFloatingPoint;
102 
103     static if (isFloatingPoint!T) {
104         import std.traits: Unqual;
105         alias statType = Unqual!T;
106     } else static if (is(T : double)) {
107         alias statType = double;
108     } else static if (checkComplex) {
109         import mir.internal.utility: isComplex;
110         static if (isComplex!T) {
111             static if (__traits(getAliasThis, T).length == 1)
112             {
113                 alias statType = .statType!(typeof(__traits(getMember, T, __traits(getAliasThis, T)[0]))); 
114             }
115             else
116             {
117                 import std.traits: Unqual;
118                 alias statType = Unqual!T;
119             }
120         } else {
121             static assert(0, "statType: type " ~ T.stringof ~ " must be convertible to a complex floating point type");
122         }
123     } else {
124         static assert(0, "statType: type " ~ T.stringof ~ " must be convertible to a floating point type");
125     }
126 }
127 
128 version(mir_stat_test)
129 @safe pure nothrow @nogc
130 unittest
131 {
132     static assert(is(statType!int == double));
133     static assert(is(statType!uint == double));
134     static assert(is(statType!double == double));
135     static assert(is(statType!float == float));
136     static assert(is(statType!real == real));
137     
138     static assert(is(statType!(const(int)) == double));
139     static assert(is(statType!(immutable(int)) == double));
140     static assert(is(statType!(const(double)) == double));
141     static assert(is(statType!(immutable(double)) == double));
142 }
143 
144 version(mir_stat_test)
145 @safe pure nothrow @nogc
146 unittest
147 {
148     import mir.complex: Complex;
149 
150     static assert(is(statType!(Complex!float) == Complex!float));
151     static assert(is(statType!(Complex!double) == Complex!double));
152     static assert(is(statType!(Complex!real) == Complex!real));
153 }
154 
155 version(mir_stat_test)
156 @safe pure nothrow @nogc
157 unittest
158 {
159     static struct Foo {
160         float x;
161         alias x this;
162     }
163 
164     static assert(is(statType!Foo == double)); // note: this is not float
165 }
166 
167 version(mir_stat_test)
168 @safe pure nothrow @nogc
169 unittest
170 {
171     import mir.complex;
172     static struct Foo {
173         Complex!float x;
174         alias x this;
175     }
176 
177     static assert(is(statType!Foo == Complex!float));
178 }
179 
180 version(mir_stat_test)
181 @safe pure nothrow @nogc
182 unittest
183 {
184     static struct Foo {
185         double x;
186         alias x this;
187     }
188 
189     static assert(is(statType!Foo == double));
190 }
191 
192 version(mir_stat_test)
193 @safe pure nothrow @nogc
194 unittest
195 {
196     import mir.complex;
197     static struct Foo {
198         Complex!double x;
199         alias x this;
200     }
201 
202     static assert(is(statType!Foo == Complex!double));
203 }
204 
205 version(mir_stat_test)
206 @safe pure nothrow @nogc
207 unittest
208 {
209     static struct Foo {
210         real x;
211         alias x this;
212     }
213 
214     static assert(is(statType!Foo == double)); // note: this is not real
215 }
216 
217 version(mir_stat_test)
218 @safe pure nothrow @nogc
219 unittest
220 {
221     import mir.complex;
222     static struct Foo {
223         Complex!real x;
224         alias x this;
225     }
226 
227     static assert(is(statType!Foo == Complex!real));
228 }
229 
230 version(mir_stat_test)
231 @safe pure nothrow @nogc
232 unittest
233 {
234     static struct Foo {
235         int x;
236         alias x this;
237     }
238 
239     static assert(is(statType!Foo == double)); // note: this is not ints
240 }
241 
242 ///
243 package(mir)
244 template meanType(T)
245 {
246     import mir.math.sum: sumType;
247 
248     alias U = sumType!T;
249 
250     static if (__traits(compiles, {
251         auto temp = U.init + U.init;
252         auto a = temp / 2;
253         temp += U.init;
254     })) {
255         alias V = typeof((U.init + U.init) / 2);
256         alias meanType = statType!V;
257     } else {
258         static assert(0, "meanType: Can't calculate mean of elements of type " ~ U.stringof);
259     }
260 }
261 
262 version(mir_stat_test)
263 @safe pure nothrow @nogc
264 unittest
265 {
266     static assert(is(meanType!(int[]) == double));
267     static assert(is(meanType!(double[]) == double));
268     static assert(is(meanType!(float[]) == float));
269 }
270 
271 version(mir_stat_test)
272 @safe pure nothrow @nogc
273 unittest
274 {
275     import mir.complex;
276     static assert(is(meanType!(Complex!float[]) == Complex!float));
277 }
278 
279 version(mir_stat_test)
280 @safe pure nothrow @nogc
281 unittest
282 {
283     static struct Foo {
284         float x;
285         alias x this;
286     }
287 
288     static assert(is(meanType!(Foo[]) == float));
289 }
290 
291 version(mir_stat_test)
292 @safe pure nothrow @nogc
293 unittest
294 {
295     import mir.complex;
296     static struct Foo {
297         Complex!float x;
298         alias x this;
299     }
300 
301     static assert(is(meanType!(Foo[]) == Complex!float));
302 }
303 
304 /++
305 Output range for mean.
306 +/
307 struct MeanAccumulator(T, Summation summation)
308 {
309     import mir.primitives: elementCount, hasShape;
310     import std.traits: isIterable;
311 
312     ///
313     size_t count;
314     ///
315     Summator!(T, summation) summator;
316 
317     ///
318     F mean(F = T)() const @safe @property pure nothrow @nogc
319     {
320         return cast(F) summator.sum / cast(F) count;
321     }
322     
323     ///
324     F sum(F = T)() const @safe @property pure nothrow @nogc
325     {
326         return cast(F) summator.sum;
327     }
328 
329     ///
330     void put(Range)(Range r)
331         if (isIterable!Range)
332     {
333         static if (hasShape!Range)
334         {
335             count += r.elementCount;
336             summator.put(r);
337         }
338         else
339         {
340             foreach(x; r)
341             {
342                 count++;
343                 summator.put(x);
344             }
345         }
346     }
347 
348     ///
349     void put()(T x)
350     {
351         count++;
352         summator.put(x);
353     }
354     
355     ///
356     void put(F = T)(MeanAccumulator!(F, summation) m)
357     {
358         count += m.count;
359         summator.put(cast(T) m.summator);
360     }
361 }
362 
363 ///
364 version(mir_stat_test)
365 @safe pure nothrow
366 unittest
367 {
368     import mir.ndslice.slice: sliced;
369 
370     MeanAccumulator!(double, Summation.pairwise) x;
371     x.put([0.0, 1, 2, 3, 4].sliced);
372     assert(x.mean == 2);
373     x.put(5);
374     assert(x.mean == 2.5);
375 }
376 
377 version(mir_stat_test)
378 @safe pure nothrow
379 unittest
380 {
381     import mir.ndslice.slice: sliced;
382 
383     MeanAccumulator!(float, Summation.pairwise) x;
384     x.put([0, 1, 2, 3, 4].sliced);
385     assert(x.mean == 2);
386     assert(x.sum == 10);
387     x.put(5);
388     assert(x.mean == 2.5);
389 }
390 
391 version(mir_stat_test)
392 @safe pure nothrow
393 unittest
394 {
395     double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25];
396     double[] y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
397     
398     MeanAccumulator!(float, Summation.pairwise) m0;
399     m0.put(x);
400     MeanAccumulator!(float, Summation.pairwise) m1;
401     m1.put(y);
402     m0.put(m1);
403     assert(m0.mean == 29.25 / 12);
404 }
405 
406 /++
407 Computes the mean of the input.
408 
409 By default, if `F` is not floating point type or complex type, then the result
410 will have a `double` type if `F` is implicitly convertible to a floating point 
411 type or a type for which `isComplex!F` is true.
412 
413 Params:
414     F = controls type of output
415     summation = algorithm for calculating sums (default: Summation.appropriate)
416 Returns:
417     The mean of all the elements in the input, must be floating point or complex type
418 
419 See_also:
420     $(MATHREF_ALT sum, Summation)
421 +/
422 template mean(F, Summation summation = Summation.appropriate)
423 {
424     import core.lifetime: move;
425     import std.traits: isIterable;
426 
427     /++
428     Params:
429         r = range, must be finite iterable
430     +/
431     @fmamath meanType!F mean(Range)(Range r)
432         if (isIterable!Range)
433     {
434         alias G = typeof(return);
435         MeanAccumulator!(G, ResolveSummationType!(summation, Range, G)) mean;
436         mean.put(r.move);
437         return mean.mean;
438     }
439     
440     /++
441     Params:
442         ar = values
443     +/
444     @fmamath meanType!F mean(scope const F[] ar...)
445     {
446         alias G = typeof(return);
447         MeanAccumulator!(G, ResolveSummationType!(summation, const(G)[], G)) mean;
448         mean.put(ar);
449         return mean.mean;
450     }
451 }
452 
453 /// ditto
454 template mean(Summation summation = Summation.appropriate)
455 {
456     import core.lifetime: move;
457     import std.traits: isIterable;
458 
459     /++
460     Params:
461         r = range, must be finite iterable
462     +/
463     @fmamath meanType!Range mean(Range)(Range r)
464         if (isIterable!Range)
465     {
466         alias F = typeof(return);
467         return .mean!(F, summation)(r.move);
468     }
469     
470     /++
471     Params:
472         ar = values
473     +/
474     @fmamath meanType!T mean(T)(scope const T[] ar...)
475     {
476         alias F = typeof(return);
477         return .mean!(F, summation)(ar);
478     }
479 }
480 
481 /// ditto
482 template mean(F, string summation)
483 {
484     mixin("alias mean = .mean!(F, Summation." ~ summation ~ ");");
485 }
486 
487 /// ditto
488 template mean(string summation)
489 {
490     mixin("alias mean = .mean!(Summation." ~ summation ~ ");");
491 }
492 
493 ///
494 version(mir_stat_test)
495 @safe pure nothrow
496 unittest
497 {
498     import mir.ndslice.slice: sliced;
499     import mir.complex;
500     alias C = Complex!double;
501 
502     assert(mean([1.0, 2, 3]) == 2);
503     assert(mean([C(1, 3), C(2), C(3)]) == C(2, 1));
504     
505     assert(mean!float([0, 1, 2, 3, 4, 5].sliced(3, 2)) == 2.5);
506     
507     static assert(is(typeof(mean!float([1, 2, 3])) == float));
508 }
509 
510 /// Mean of vector
511 version(mir_stat_test)
512 @safe pure nothrow
513 unittest
514 {
515     import mir.ndslice.slice: sliced;
516 
517     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
518               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
519     assert(x.mean == 29.25 / 12);
520 }
521 
522 /// Mean of matrix
523 version(mir_stat_test)
524 @safe pure
525 unittest
526 {
527     import mir.ndslice.fuse: fuse;
528 
529     auto x = [
530         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
531         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
532     ].fuse;
533 
534     assert(x.mean == 29.25 / 12);
535 }
536 
537 /// Column mean of matrix
538 version(mir_stat_test)
539 @safe pure
540 unittest
541 {
542     import mir.ndslice.fuse: fuse;
543     import mir.ndslice.topology: alongDim, byDim, map;
544     import mir.algorithm.iteration: all;
545     import mir.math.common: approxEqual;
546 
547     auto x = [
548         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
549         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
550     ].fuse;
551     auto result = [1, 4.25, 3.25, 1.5, 2.5, 2.125];
552 
553     // Use byDim or alongDim with map to compute mean of row/column.
554     assert(x.byDim!1.map!mean.all!approxEqual(result));
555     assert(x.alongDim!0.map!mean.all!approxEqual(result));
556 
557     // FIXME
558     // Without using map, computes the mean of the whole slice
559     // assert(x.byDim!1.mean == x.sliced.mean);
560     // assert(x.alongDim!0.mean == x.sliced.mean);
561 }
562 
563 /// Can also set algorithm or output type
564 version(mir_stat_test)
565 @safe pure nothrow
566 unittest
567 {
568     import mir.ndslice.slice: sliced;
569     import mir.ndslice.topology: repeat;
570 
571     //Set sum algorithm or output type
572 
573     auto a = [1, 1e100, 1, -1e100].sliced;
574 
575     auto x = a * 10_000;
576 
577     assert(x.mean!"kbn" == 20_000 / 4);
578     assert(x.mean!"kb2" == 20_000 / 4);
579     assert(x.mean!"precise" == 20_000 / 4);
580     assert(x.mean!(double, "precise") == 20_000.0 / 4);
581 
582     auto y = uint.max.repeat(3);
583     assert(y.mean!ulong == 12884901885 / 3);
584 }
585 
586 /++
587 For integral slices, pass output type as template parameter to ensure output
588 type is correct.
589 +/
590 version(mir_stat_test)
591 @safe pure nothrow
592 unittest
593 {
594     import mir.math.common: approxEqual;
595     import mir.ndslice.slice: sliced;
596 
597     auto x = [0, 1, 1, 2, 4, 4,
598               2, 7, 5, 1, 2, 0].sliced;
599 
600     auto y = x.mean;
601     assert(y.approxEqual(29.0 / 12, 1.0e-10));
602     static assert(is(typeof(y) == double));
603 
604     assert(x.mean!float.approxEqual(29f / 12, 1.0e-10));
605 }
606 
607 /++
608 Mean works for complex numbers and other user-defined types (provided they
609 can be converted to a floating point or complex type)
610 +/
611 version(mir_stat_test)
612 @safe pure nothrow
613 unittest
614 {
615     import mir.complex.math: approxEqual;
616     import mir.ndslice.slice: sliced;
617     import mir.complex;
618     alias C = Complex!double;
619 
620     auto x = [C(1.0, 2), C(2, 3), C(3, 4), C(4, 5)].sliced;
621     assert(x.mean.approxEqual(C(2.5, 3.5)));
622 }
623 
624 /// Compute mean tensors along specified dimention of tensors
625 version(mir_stat_test)
626 @safe pure nothrow
627 unittest
628 {
629     import mir.ndslice: alongDim, iota, as, map;
630     /++
631       [[0,1,2],
632        [3,4,5]]
633      +/
634     auto x = iota(2, 3).as!double;
635     assert(x.mean == (5.0 / 2.0));
636 
637     auto m0 = [(0.0+3.0)/2.0, (1.0+4.0)/2.0, (2.0+5.0)/2.0];
638     assert(x.alongDim!0.map!mean == m0);
639     assert(x.alongDim!(-2).map!mean == m0);
640 
641     auto m1 = [(0.0+1.0+2.0)/3.0, (3.0+4.0+5.0)/3.0];
642     assert(x.alongDim!1.map!mean == m1);
643     assert(x.alongDim!(-1).map!mean == m1);
644 
645     assert(iota(2, 3, 4, 5).as!double.alongDim!0.map!mean == iota([3, 4, 5], 3 * 4 * 5 / 2));
646 }
647 
648 /// Arbitrary mean
649 version(mir_stat_test)
650 @safe pure nothrow @nogc
651 unittest
652 {
653     assert(mean(1.0, 2, 3) == 2);
654     assert(mean!float(1, 2, 3) == 2);
655 }
656 
657 version(mir_stat_test)
658 @safe pure nothrow
659 unittest
660 {
661     assert([1.0, 2, 3, 4].mean == 2.5);
662 }
663 
664 version(mir_stat_test)
665 @safe pure nothrow
666 unittest
667 {
668     import mir.algorithm.iteration: all;
669     import mir.math.common: approxEqual;
670     import mir.ndslice.topology: iota, alongDim, map;
671 
672     auto x = iota([2, 2], 1);
673     auto y = x.alongDim!1.map!mean;
674     assert(y.all!approxEqual([1.5, 3.5]));
675     static assert(is(meanType!(typeof(y)) == double));
676 }
677 
678 version(mir_stat_test)
679 @safe pure nothrow @nogc
680 unittest
681 {
682     import mir.ndslice.slice: sliced;
683 
684     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
685                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
686 
687     assert(x.sliced.mean == 29.25 / 12);
688     assert(x.sliced.mean!float == 29.25 / 12);
689 }
690 
691 ///
692 package(mir)
693 template hmeanType(T)
694 {
695     import mir.math.sum: sumType;
696     
697     alias U = sumType!T;
698 
699     static if (__traits(compiles, {
700         U t = U.init + cast(U) 1; //added for when U.init = 0
701         auto temp = cast(U) 1 / t + cast(U) 1 / t;
702     })) {
703         alias V = typeof(cast(U) 1 / ((cast(U) 1 / U.init + cast(U) 1 / U.init) / cast(U) 2));
704         alias hmeanType = statType!V;
705     } else {
706         static assert(0, "hmeanType: Can't calculate hmean of elements of type " ~ U.stringof);
707     }
708 }
709 
710 version(mir_stat_test)
711 @safe pure nothrow @nogc
712 unittest
713 {
714     import mir.complex;
715     static assert(is(hmeanType!(int[]) == double));
716     static assert(is(hmeanType!(double[]) == double));
717     static assert(is(hmeanType!(float[]) == float)); 
718     static assert(is(hmeanType!(Complex!float[]) == Complex!float));    
719 }
720 
721 version(mir_stat_test)
722 @safe pure nothrow @nogc
723 unittest
724 {
725     import mir.complex;
726     static struct Foo {
727         float x;
728         alias x this;
729     }
730     
731     static struct Bar {
732         Complex!float x;
733         alias x this;
734     }
735 
736     static assert(is(hmeanType!(Foo[]) == float));
737     static assert(is(hmeanType!(Bar[]) == Complex!float));
738 }
739 
740 /++
741 Computes the harmonic mean of the input.
742 
743 By default, if `F` is not floating point type or complex type, then the result
744 will have a `double` type if `F` is implicitly convertible to a floating point 
745 type or a type for which `isComplex!F` is true.
746 
747 Params:
748     F = controls type of output
749     summation = algorithm for calculating sums (default: Summation.appropriate)
750 Returns:
751     harmonic mean of all the elements of the input, must be floating point or complex type
752 
753 See_also:
754     $(MATHREF_ALT sum, Summation)
755 +/
756 template hmean(F, Summation summation = Summation.appropriate)
757 {
758     import core.lifetime: move;
759     import std.traits: isIterable;
760 
761     /++
762     Params:
763         r = range
764     +/
765     @fmamath hmeanType!F hmean(Range)(Range r)
766         if (isIterable!Range)
767     {
768         import mir.ndslice.topology: map;
769 
770         alias G = typeof(return);
771         auto numerator = cast(G) 1;
772 
773         static if (summation == Summation.fast && __traits(compiles, r.move.map!"numerator / a"))
774         {
775             return numerator / r.move.map!"numerator / a".mean!(G, summation);
776         }
777         else
778         {
779             MeanAccumulator!(G, ResolveSummationType!(summation, Range, G)) imean;
780             foreach (e; r)
781                 imean.put(numerator / e);
782             return numerator / imean.mean;
783         }
784     }
785    
786     /++
787     Params:
788         ar = values
789     +/
790     @fmamath hmeanType!F hmean(scope const F[] ar...)
791     {
792         alias G = typeof(return);
793 
794         auto numerator = cast(G) 1;
795 
796         static if (summation == Summation.fast && __traits(compiles, ar.map!"numerator / a"))
797         {
798             return numerator / ar.map!"numerator / a".mean!(G, summation);
799         }
800         else
801         {
802             MeanAccumulator!(G, ResolveSummationType!(summation, const(G)[], G)) imean;
803             foreach (e; ar)
804                 imean.put(numerator / e);
805             return numerator / imean.mean;
806         }
807     }
808 }
809 
810 /// ditto
811 template hmean(Summation summation = Summation.appropriate)
812 {
813     import core.lifetime: move;
814     import std.traits: isIterable;
815 
816     /++
817     Params:
818         r = range
819     +/
820     @fmamath hmeanType!Range hmean(Range)(Range r)
821         if (isIterable!Range)
822     {
823         alias F = typeof(return);
824         return .hmean!(F, summation)(r.move);
825     }
826     
827     /++
828     Params:
829         ar = values
830     +/
831     @fmamath hmeanType!T hmean(T)(scope const T[] ar...)
832     {
833         alias F = typeof(return);
834         return .hmean!(F, summation)(ar);
835     }
836 }
837 
838 /// ditto
839 template hmean(F, string summation)
840 {
841     mixin("alias hmean = .hmean!(F, Summation." ~ summation ~ ");");
842 }
843 
844 /// ditto
845 template hmean(string summation)
846 {
847     mixin("alias hmean = .hmean!(Summation." ~ summation ~ ");");
848 }
849 
850 /// Harmonic mean of vector
851 version(mir_stat_test)
852 @safe pure nothrow
853 unittest
854 {
855     import mir.math.common: approxEqual;
856     import mir.ndslice.slice: sliced;
857 
858     auto x = [20.0, 100.0, 2000.0, 10.0, 5.0, 2.0].sliced;
859 
860     assert(x.hmean.approxEqual(6.97269));
861 }
862 
863 /// Harmonic mean of matrix
864 version(mir_stat_test)
865 pure @safe
866 unittest
867 {
868     import mir.math.common: approxEqual;
869     import mir.ndslice.fuse: fuse;
870 
871     auto x = [
872         [20.0, 100.0, 2000.0], 
873         [10.0, 5.0, 2.0]
874     ].fuse;
875 
876     assert(x.hmean.approxEqual(6.97269));
877 }
878 
879 /// Column harmonic mean of matrix
880 version(mir_stat_test)
881 pure @safe
882 unittest
883 {
884     import mir.algorithm.iteration: all;
885     import mir.math.common: approxEqual;
886     import mir.ndslice: fuse;
887     import mir.ndslice.topology: alongDim, byDim, map;
888 
889     auto x = [
890         [20.0, 100.0, 2000.0],
891         [ 10.0, 5.0, 2.0]
892     ].fuse;
893 
894     auto y = [13.33333, 9.52381, 3.996004];
895 
896     // Use byDim or alongDim with map to compute mean of row/column.
897     assert(x.byDim!1.map!hmean.all!approxEqual(y));
898     assert(x.alongDim!0.map!hmean.all!approxEqual(y));
899 }
900 
901 /// Can also pass arguments to hmean
902 version(mir_stat_test)
903 pure @safe nothrow
904 unittest
905 {
906     import mir.math.common: approxEqual;
907     import mir.ndslice.topology: repeat;
908     import mir.ndslice.slice: sliced;
909 
910     //Set sum algorithm or output type
911     auto x = [1, 1e-100, 1, -1e-100].sliced;
912 
913     assert(x.hmean!"kb2".approxEqual(2));
914     assert(x.hmean!"precise".approxEqual(2));
915     assert(x.hmean!(double, "precise").approxEqual(2));
916 
917     //Provide the summation type
918     assert(float.max.repeat(3).hmean!double.approxEqual(float.max));
919 }
920 
921 /++
922 For integral slices, pass output type as template parameter to ensure output
923 type is correct. 
924 +/
925 version(mir_stat_test)
926 @safe pure nothrow
927 unittest
928 {
929     import mir.math.common: approxEqual;
930     import mir.ndslice.slice: sliced;
931 
932     auto x = [20, 100, 2000, 10, 5, 2].sliced;
933 
934     auto y = x.hmean;
935 
936     assert(y.approxEqual(6.97269));
937     static assert(is(typeof(y) == double));
938 
939     assert(x.hmean!float.approxEqual(6.97269));
940 }
941 
942 /++
943 hmean works for complex numbers and other user-defined types (provided they
944 can be converted to a floating point or complex type)
945 +/
946 version(mir_stat_test)
947 @safe pure nothrow
948 unittest
949 {
950     import mir.complex.math: approxEqual;
951     import mir.ndslice.slice: sliced;
952     import mir.complex;
953     alias C = Complex!double;
954 
955     auto x = [C(1, 2), C(2, 3), C(3, 4), C(4, 5)].sliced;
956     assert(x.hmean.approxEqual(C(1.97110904, 3.14849332)));
957 }
958 
959 /// Arbitrary harmonic mean
960 version(mir_stat_test)
961 @safe pure nothrow @nogc
962 unittest
963 {
964     import mir.math.common: approxEqual;
965     import mir.ndslice.slice: sliced;
966 
967     auto x = hmean(20.0, 100, 2000, 10, 5, 2);
968     assert(x.approxEqual(6.97269));
969     
970     auto y = hmean!float(20, 100, 2000, 10, 5, 2);
971     assert(y.approxEqual(6.97269));
972 }
973 
974 version(mir_stat_test)
975 @safe pure nothrow @nogc
976 unittest
977 {
978     import mir.math.common: approxEqual;
979     import mir.ndslice.slice: sliced;
980 
981     static immutable x = [20.0, 100.0, 2000.0, 10.0, 5.0, 2.0];
982 
983     assert(x.sliced.hmean.approxEqual(6.97269));
984     assert(x.sliced.hmean!float.approxEqual(6.97269));
985 }
986 
987 private
988 F nthroot(F)(in F x, in size_t n)
989     if (isFloatingPoint!F)
990 {
991     import mir.math.common: sqrt, pow;
992 
993     if (n > 2) {
994         return pow(x, cast(F) 1 / cast(F) n);
995     } else if (n == 2) {
996         return sqrt(x);
997     } else if (n == 1) {
998         return x;
999     } else {
1000         return cast(F) 1;
1001     }
1002 }
1003 
1004 version(mir_stat_test)
1005 @safe pure nothrow @nogc
1006 unittest
1007 {
1008     import mir.math.common: approxEqual;
1009 
1010     assert(nthroot(9.0, 0).approxEqual(1));
1011     assert(nthroot(9.0, 1).approxEqual(9));
1012     assert(nthroot(9.0, 2).approxEqual(3));
1013     assert(nthroot(9.5, 2).approxEqual(3.08220700));
1014     assert(nthroot(9.0, 3).approxEqual(2.08008382));
1015 }
1016 
1017 /++
1018 Output range for gmean.
1019 +/
1020 struct GMeanAccumulator(T) 
1021     if (isMutable!T && isFloatingPoint!T)
1022 {
1023     import mir.math.numeric: ProdAccumulator;
1024     import mir.primitives: elementCount, hasShape;
1025 
1026     ///
1027     size_t count;
1028     ///
1029     ProdAccumulator!T prodAccumulator;
1030 
1031     ///
1032     F gmean(F = T)() const @property
1033         if (isFloatingPoint!F)
1034     {
1035         import mir.math.common: exp2;
1036 
1037         return nthroot(cast(F) prodAccumulator.mantissa, count) * exp2(cast(F) prodAccumulator.exp / count);
1038     }
1039 
1040     ///
1041     void put(Range)(Range r)
1042         if (isIterable!Range)
1043     {
1044         static if (hasShape!Range)
1045         {
1046             count += r.elementCount;
1047             prodAccumulator.put(r);
1048         }
1049         else
1050         {
1051             foreach(x; r)
1052             {
1053                 count++;
1054                 prodAccumulator.put(x);
1055             }
1056         }
1057     }
1058 
1059     ///
1060     void put()(T x)
1061     {
1062         count++;
1063         prodAccumulator.put(x);
1064     }
1065 }
1066 
1067 ///
1068 version(mir_stat_test)
1069 @safe pure nothrow
1070 unittest
1071 {
1072     import mir.math.common: approxEqual;
1073     import mir.ndslice.slice: sliced;
1074 
1075     GMeanAccumulator!double x;
1076     x.put([1.0, 2, 3, 4].sliced);
1077     assert(x.gmean.approxEqual(2.21336384));
1078     x.put(5);
1079     assert(x.gmean.approxEqual(2.60517108));
1080 }
1081 
1082 version(mir_stat_test)
1083 @safe pure nothrow
1084 unittest
1085 {
1086     import mir.math.common: approxEqual;
1087     import mir.ndslice.slice: sliced;
1088 
1089     GMeanAccumulator!float x;
1090     x.put([1, 2, 3, 4].sliced);
1091     assert(x.gmean.approxEqual(2.21336384));
1092     x.put(5);
1093     assert(x.gmean.approxEqual(2.60517108));
1094 }
1095 
1096 ///
1097 package(mir)
1098 template gmeanType(T)
1099 {
1100     // TODO: including copy because visibility in mir.math.numeric is set to package
1101     private template prodType(T)
1102     {
1103         import mir.math.sum: elementType;
1104 
1105         alias U = elementType!T;
1106         
1107         static if (__traits(compiles, {
1108             auto temp = U.init * U.init;
1109             temp *= U.init;
1110         })) {
1111             alias V = typeof(U.init * U.init);
1112             alias prodType = statType!(V, false);
1113         } else {
1114             static assert(0, "prodType: Can't prod elements of type " ~ U.stringof);
1115         }
1116     }
1117 
1118     alias U = prodType!T;
1119 
1120     static if (__traits(compiles, {
1121         auto temp = U.init * U.init;
1122         auto a = nthroot(temp, 2);
1123         temp *= U.init;
1124     })) {
1125         alias V = typeof(nthroot(U.init * U.init, 2));
1126         alias gmeanType = statType!(V, false);
1127     } else {
1128         static assert(0, "gmeanType: Can't calculate gmean of elements of type " ~ U.stringof);
1129     }
1130 }
1131 
1132 version(mir_stat_test)
1133 @safe pure nothrow @nogc
1134 unittest
1135 {
1136     static assert(is(gmeanType!int == double));
1137     static assert(is(gmeanType!double == double));
1138     static assert(is(gmeanType!float == float));
1139     static assert(is(gmeanType!(int[]) == double));
1140     static assert(is(gmeanType!(double[]) == double));
1141     static assert(is(gmeanType!(float[]) == float));    
1142 }
1143 
1144 /++
1145 Computes the geometric average of the input.
1146 
1147 By default, if `F` is not floating point type, then the result will have a 
1148 `double` type if `F` is implicitly convertible to a floating point type.
1149 
1150 Params:
1151     r = range, must be finite iterable
1152 Returns:
1153     The geometric average of all the elements in the input, must be floating point type
1154 
1155 See_also:
1156     $(MATHREF_ALT numeric, prod)
1157 +/
1158 @fmamath gmeanType!F gmean(F, Range)(Range r)
1159     if (isFloatingPoint!F && isIterable!Range)
1160 {
1161     import core.lifetime: move;
1162 
1163     alias G = typeof(return);
1164     GMeanAccumulator!G gmean;
1165     gmean.put(r.move);
1166     return gmean.gmean;
1167 }
1168     
1169 /// ditto
1170 @fmamath gmeanType!Range gmean(Range)(Range r)
1171     if (isIterable!Range)
1172 {
1173     import core.lifetime: move;
1174 
1175     alias G = typeof(return);
1176     return .gmean!(G, Range)(r.move);
1177 }
1178 
1179 /++
1180 Params:
1181     ar = values
1182 +/
1183 @fmamath gmeanType!F gmean(F)(scope const F[] ar...)
1184     if (isFloatingPoint!F)
1185 {
1186     alias G = typeof(return);
1187     GMeanAccumulator!G gmean;
1188     gmean.put(ar);
1189     return gmean.gmean;
1190 }
1191 
1192 ///
1193 version(mir_stat_test)
1194 @safe pure nothrow
1195 unittest
1196 {
1197     import mir.math.common: approxEqual;
1198     import mir.ndslice.slice: sliced;
1199 
1200     assert(gmean([1.0, 2, 3]).approxEqual(1.81712059));
1201     
1202     assert(gmean!float([1, 2, 3, 4, 5, 6].sliced(3, 2)).approxEqual(2.99379516));
1203     
1204     static assert(is(typeof(gmean!float([1, 2, 3])) == float));
1205 }
1206 
1207 /// Geometric mean of vector
1208 version(mir_stat_test)
1209 @safe pure nothrow
1210 unittest
1211 {
1212     import mir.math.common: approxEqual;
1213     import mir.ndslice.slice: sliced;
1214 
1215     auto x = [3.0, 1.0, 1.5, 2.0, 3.5, 4.25,
1216               2.0, 7.5, 5.0, 1.0, 1.5, 2.0].sliced;
1217 
1218     assert(x.gmean.approxEqual(2.36178395));
1219 }
1220 
1221 /// Geometric mean of matrix
1222 version(mir_stat_test)
1223 @safe pure
1224 unittest
1225 {
1226     import mir.math.common: approxEqual;
1227     import mir.ndslice.fuse: fuse;
1228 
1229     auto x = [
1230         [3.0, 1.0, 1.5, 2.0, 3.5, 4.25],
1231         [2.0, 7.5, 5.0, 1.0, 1.5, 2.0]
1232     ].fuse;
1233 
1234     assert(x.gmean.approxEqual(2.36178395));
1235 }
1236 
1237 /// Column gmean of matrix
1238 version(mir_stat_test)
1239 @safe pure
1240 unittest
1241 {
1242     import mir.algorithm.iteration: all;
1243     import mir.math.common: approxEqual;
1244     import mir.ndslice.fuse: fuse;
1245     import mir.ndslice.topology: alongDim, byDim, map;
1246 
1247     auto x = [
1248         [3.0, 1.0, 1.5, 2.0, 3.5, 4.25],
1249         [2.0, 7.5, 5.0, 1.0, 1.5, 2.0]
1250     ].fuse;
1251     auto result = [2.44948974, 2.73861278, 2.73861278, 1.41421356, 2.29128784, 2.91547594];
1252 
1253     // Use byDim or alongDim with map to compute mean of row/column.
1254     assert(x.byDim!1.map!gmean.all!approxEqual(result));
1255     assert(x.alongDim!0.map!gmean.all!approxEqual(result));
1256 
1257     // FIXME
1258     // Without using map, computes the mean of the whole slice
1259     // assert(x.byDim!1.gmean.all!approxEqual(result));
1260     // assert(x.alongDim!0.gmean.all!approxEqual(result));
1261 }
1262 
1263 /// Can also set output type
1264 version(mir_stat_test)
1265 @safe pure nothrow
1266 unittest
1267 {
1268     import mir.math.common: approxEqual;
1269     import mir.ndslice.slice: sliced;
1270     import mir.ndslice.topology: repeat;
1271 
1272     auto x = [5120.0, 7340032, 32, 3758096384].sliced;
1273 
1274     assert(x.gmean!float.approxEqual(259281.45295212));
1275 
1276     auto y = uint.max.repeat(2);
1277     assert(y.gmean!float.approxEqual(cast(float) uint.max));
1278 }
1279 
1280 /++
1281 For integral slices, pass output type as template parameter to ensure output
1282 type is correct.
1283 +/
1284 version(mir_stat_test)
1285 @safe pure nothrow
1286 unittest
1287 {
1288     import mir.math.common: approxEqual;
1289     import mir.ndslice.slice: sliced;
1290 
1291     auto x = [5, 1, 1, 2, 4, 4,
1292               2, 7, 5, 1, 2, 10].sliced;
1293 
1294     auto y = x.gmean;
1295     static assert(is(typeof(y) == double));
1296     
1297     assert(x.gmean!float.approxEqual(2.79160522));
1298 }
1299 
1300 /// gean works for user-defined types, provided the nth root can be taken for them
1301 version(mir_stat_test)
1302 @safe pure nothrow
1303 unittest
1304 {
1305     static struct Foo {
1306         float x;
1307         alias x this;
1308     }
1309 
1310     import mir.math.common: approxEqual;
1311     import mir.ndslice.slice: sliced;
1312 
1313     auto x = [Foo(1.0), Foo(2.0), Foo(3.0)].sliced;
1314     assert(x.gmean.approxEqual(1.81712059));
1315 }
1316 
1317 /// Compute gmean tensors along specified dimention of tensors
1318 version(mir_stat_test)
1319 @safe pure
1320 unittest
1321 {
1322     import mir.algorithm.iteration: all;
1323     import mir.math.common: approxEqual;
1324     import mir.ndslice.fuse: fuse;
1325     import mir.ndslice.topology: alongDim, iota, map;
1326     
1327     auto x = [
1328         [1.0, 2, 3],
1329         [4.0, 5, 6]
1330     ].fuse;
1331 
1332     assert(x.gmean.approxEqual(2.99379516));
1333 
1334     auto result0 = [2.0, 3.16227766, 4.24264069];
1335     assert(x.alongDim!0.map!gmean.all!approxEqual(result0));
1336     assert(x.alongDim!(-2).map!gmean.all!approxEqual(result0));
1337 
1338     auto result1 = [1.81712059, 4.93242414];
1339     assert(x.alongDim!1.map!gmean.all!approxEqual(result1));
1340     assert(x.alongDim!(-1).map!gmean.all!approxEqual(result1));
1341 
1342     auto y = [
1343         [
1344             [1.0, 2, 3],
1345             [4.0, 5, 6]
1346         ], [
1347             [7.0, 8, 9],
1348             [10.0, 9, 10]
1349         ]
1350     ].fuse;
1351     
1352     auto result3 = [
1353         [2.64575131, 4.0,        5.19615242],
1354         [6.32455532, 6.70820393, 7.74596669]
1355     ];
1356     assert(y.alongDim!0.map!gmean.all!approxEqual(result3));
1357 }
1358 
1359 /// Arbitrary gmean
1360 version(mir_stat_test)
1361 @safe pure nothrow @nogc
1362 unittest
1363 {
1364     import mir.math.common: approxEqual;
1365 
1366     assert(gmean(1.0, 2, 3).approxEqual(1.81712059));
1367     assert(gmean!float(1, 2, 3).approxEqual(1.81712059));
1368 }
1369 
1370 version(mir_stat_test)
1371 @safe pure nothrow
1372 unittest
1373 {
1374     import mir.math.common: approxEqual;
1375 
1376     assert([1.0, 2, 3, 4].gmean.approxEqual(2.21336384));
1377 }
1378 
1379 version(mir_stat_test)
1380 @safe pure nothrow
1381 unittest
1382 {
1383     import mir.math.common: approxEqual;
1384 
1385     assert(gmean([1, 2, 3]).approxEqual(1.81712059));
1386 }
1387 
1388 version(mir_stat_test)
1389 @safe pure nothrow @nogc
1390 unittest
1391 {
1392     import mir.math.common: approxEqual;
1393     import mir.ndslice.slice: sliced;
1394 
1395     static immutable x = [3.0, 1.0, 1.5, 2.0, 3.5, 4.25,
1396                           2.0, 7.5, 5.0, 1.0, 1.5, 2.0];
1397 
1398     assert(x.sliced.gmean.approxEqual(2.36178395));
1399     assert(x.sliced.gmean!float.approxEqual(2.36178395));
1400 }
1401 
1402 /++
1403 Computes the median of `slice`.
1404 
1405 By default, if `F` is not floating point type or complex type, then the result
1406 will have a `double` type if `F` is implicitly convertible to a floating point 
1407 type or a type for which `isComplex!F` is true.
1408 
1409 Can also pass a boolean variable, `allowModify`, that allows the input slice to
1410 be modified. By default, a reference-counted copy is made. 
1411 
1412 Params:
1413     F = output type
1414     allowModify = Allows the input slice to be modified, default is false
1415 Returns:
1416     the median of the slice
1417 
1418 See_also:
1419     $(LREF mean)
1420 +/
1421 template median(F, bool allowModify = false)
1422 {
1423     import std.traits: Unqual;
1424 
1425     /++
1426     Params:
1427         slice = slice
1428     +/
1429     @nogc
1430     meanType!F median(Iterator, size_t N, SliceKind kind)(Slice!(Iterator, N, kind) slice)
1431     {
1432         static assert (!allowModify ||
1433                        isMutable!(slice.DeepElement),
1434                            "allowModify must be false or the input must be mutable");
1435         alias G = typeof(return);
1436         size_t len = slice.elementCount;
1437         assert(len > 0, "median: slice must have length greater than zero");
1438 
1439         import mir.ndslice.topology: as, flattened;
1440 
1441         static if (!allowModify) {
1442             import mir.ndslice.allocation: rcslice;
1443             
1444             if (len > 2) {
1445                 auto view = slice.lightScope;
1446                 auto val = view.as!(Unqual!(slice.DeepElement)).rcslice;
1447                 auto temp = val.lightScope.flattened;
1448                 return .median!(G, true)(temp);
1449             } else {
1450                 return mean!G(slice);
1451             }
1452         } else {
1453             import mir.ndslice.sorting: partitionAt;
1454             
1455             auto temp = slice.flattened;
1456 
1457             if (len > 5) {
1458                 size_t half_n = len / 2;
1459                 partitionAt(temp, half_n);
1460                 if (len % 2 == 1) {
1461                     return cast(G) temp[half_n];
1462                 } else {
1463                     //move largest value in first half of slice to half_n - 1
1464                     partitionAt(temp[0 .. half_n], half_n - 1);
1465                     return (temp[half_n - 1] + temp[half_n]) / cast(G) 2;
1466                 }
1467             } else {
1468                 return smallMedianImpl!(G)(temp);
1469             }
1470         }
1471     }
1472 }
1473 
1474 /// ditto
1475 template median(bool allowModify = false)
1476 {
1477     import core.lifetime: move;
1478     import mir.primitives: DeepElementType;
1479 
1480     /// ditto
1481     meanType!(Slice!(Iterator, N, kind))
1482         median(Iterator, size_t N, SliceKind kind)(Slice!(Iterator, N, kind) slice)
1483     {
1484         static assert (!allowModify ||
1485                        isMutable!(DeepElementType!(Slice!(Iterator, N, kind))),
1486                            "allowModify must be false or the input must be mutable");
1487         alias F = typeof(return);
1488         return .median!(F, allowModify)(slice.move);
1489     }
1490 }
1491 
1492 /++
1493 Params:
1494     ar = array
1495 +/
1496 meanType!(T[]) median(T)(scope const T[] ar...)
1497 {
1498     import mir.ndslice.slice: sliced;
1499 
1500     alias F = typeof(return);
1501     return median!(F, false)(ar.sliced);
1502 }
1503 
1504 /++
1505 Params:
1506     sliceLike = type that satisfies `isConvertibleToSlice!T && !isSlice!T`
1507 +/
1508 auto median(T)(T sliceLike)
1509     if (isConvertibleToSlice!T && !isSlice!T)
1510 {
1511     import mir.ndslice.slice: toSlice;
1512     return median(sliceLike.toSlice);
1513 }
1514 
1515 /// Median of vector
1516 version(mir_stat_test)
1517 @safe pure nothrow
1518 unittest
1519 {
1520     import mir.ndslice.slice: sliced;
1521 
1522     auto x0 = [9.0, 1, 0, 2, 3, 4, 6, 8, 7, 10, 5].sliced;
1523     assert(x0.median == 5);
1524 
1525     auto x1 = [9.0, 1, 0, 2, 3, 4, 6, 8, 7, 10].sliced;
1526     assert(x1.median == 5);
1527 }
1528 
1529 /// Median of dynamic array
1530 version(mir_stat_test)
1531 @safe pure nothrow
1532 unittest
1533 {
1534     auto x0 = [9.0, 1, 0, 2, 3, 4, 6, 8, 7, 10, 5];
1535     assert(x0.median == 5);
1536 
1537     auto x1 = [9.0, 1, 0, 2, 3, 4, 6, 8, 7, 10];
1538     assert(x1.median == 5);
1539 }
1540 
1541 /// Median of matrix
1542 version(mir_stat_test)
1543 @safe pure
1544 unittest
1545 {
1546     import mir.ndslice.fuse: fuse;
1547 
1548     auto x0 = [
1549         [9.0, 1, 0, 2,  3], 
1550         [4.0, 6, 8, 7, 10]
1551     ].fuse;
1552 
1553     assert(x0.median == 5);
1554 }
1555 
1556 /// Row median of matrix
1557 version(mir_stat_test)
1558 @safe pure
1559 unittest
1560 {
1561     import mir.algorithm.iteration: all;
1562     import mir.math.common: approxEqual;
1563     import mir.ndslice.fuse: fuse;
1564     import mir.ndslice.slice: sliced;
1565     import mir.ndslice.topology: alongDim, byDim, map;
1566 
1567     auto x = [
1568         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25], 
1569         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
1570     ].fuse;
1571 
1572     auto result = [1.75, 1.75].sliced;
1573 
1574     // Use byDim or alongDim with map to compute median of row/column.
1575     assert(x.byDim!0.map!median.all!approxEqual(result));
1576     assert(x.alongDim!1.map!median.all!approxEqual(result));
1577 }
1578 
1579 /// Can allow original slice to be modified or set output type
1580 version(mir_stat_test)
1581 @safe pure nothrow
1582 unittest
1583 {
1584     import mir.ndslice.slice: sliced;
1585 
1586     auto x0 = [9.0, 1, 0, 2, 3, 4, 6, 8, 7, 10, 5].sliced;
1587     assert(x0.median!true == 5);
1588     
1589     auto x1 = [9, 1, 0, 2, 3, 4, 6, 8, 7, 10].sliced;
1590     assert(x1.median!(float, true) == 5);
1591 }
1592 
1593 /// Arbitrary median
1594 version(mir_stat_test)
1595 @safe pure nothrow
1596 unittest
1597 {
1598     assert(median(0, 1, 2, 3, 4) == 2);
1599 }
1600 
1601 // @nogc test
1602 version(mir_stat_test)
1603 @safe pure nothrow @nogc
1604 unittest
1605 {
1606     import mir.ndslice.slice: sliced;
1607 
1608     static immutable x = [9.0, 1, 0, 2, 3];
1609     assert(x.sliced.median == 2);
1610 }
1611 
1612 // withAsSlice test
1613 version(mir_stat_test)
1614 @safe pure nothrow @nogc
1615 unittest
1616 {
1617     import mir.math.common: approxEqual;
1618     import mir.rc.array: RCArray;
1619 
1620     static immutable a = [9.0, 1, 0, 2, 3, 4, 6, 8, 7, 10, 5];
1621 
1622     auto x = RCArray!double(11);
1623     foreach(i, ref e; x)
1624         e = a[i];
1625 
1626     assert(x.median.approxEqual(5));
1627 }
1628 
1629 /++
1630 For integral slices, can pass output type as template parameter to ensure output
1631 type is correct
1632 +/
1633 version(mir_stat_test)
1634 @safe pure nothrow
1635 unittest
1636 {
1637     import mir.ndslice.slice: sliced;
1638 
1639     auto x = [9, 1, 0, 2, 3, 4, 6, 8, 7, 10].sliced;
1640     assert(x.median!float == 5f);
1641 
1642     auto y = x.median;
1643     assert(y == 5.0);
1644     static assert(is(typeof(y) == double));
1645 }
1646 
1647 // additional logic tests
1648 version(mir_stat_test)
1649 @safe pure nothrow
1650 unittest
1651 {
1652     import mir.math.common: approxEqual;
1653     import mir.ndslice.slice: sliced;
1654 
1655     auto x = [3, 3, 2, 0, 2, 0].sliced;
1656     assert(x.median!float.approxEqual(2));
1657 
1658     x[] = [2, 2, 4, 0, 4, 3];
1659     assert(x.median!float.approxEqual(2.5));
1660     x[] = [1, 4, 5, 4, 4, 3];
1661     assert(x.median!float.approxEqual(4));
1662     x[] = [1, 5, 3, 5, 2, 2];
1663     assert(x.median!float.approxEqual(2.5));
1664     x[] = [4, 3, 2, 1, 4, 5];
1665     assert(x.median!float.approxEqual(3.5));
1666     x[] = [4, 5, 3, 5, 5, 4];
1667     assert(x.median!float.approxEqual(4.5));
1668     x[] = [3, 3, 3, 0, 0, 1];
1669     assert(x.median!float.approxEqual(2));
1670     x[] = [4, 2, 2, 1, 2, 5];
1671     assert(x.median!float.approxEqual(2));
1672     x[] = [2, 3, 1, 4, 5, 5];
1673     assert(x.median!float.approxEqual(3.5));
1674     x[] = [1, 1, 4, 5, 5, 5];
1675     assert(x.median!float.approxEqual(4.5));
1676     x[] = [2, 4, 0, 5, 1, 0];
1677     assert(x.median!float.approxEqual(1.5));
1678     x[] = [3, 5, 2, 5, 4, 2];
1679     assert(x.median!float.approxEqual(3.5));
1680     x[] = [3, 5, 4, 1, 4, 3];
1681     assert(x.median!float.approxEqual(3.5));
1682     x[] = [4, 2, 0, 3, 1, 3];
1683     assert(x.median!float.approxEqual(2.5));
1684     x[] = [100, 4, 5, 0, 5, 1];
1685     assert(x.median!float.approxEqual(4.5));
1686     x[] = [100, 5, 4, 0, 5, 1];
1687     assert(x.median!float.approxEqual(4.5));
1688     x[] = [100, 5, 4, 0, 1, 5];
1689     assert(x.median!float.approxEqual(4.5));
1690     x[] = [4, 5, 100, 1, 5, 0];
1691     assert(x.median!float.approxEqual(4.5));
1692     x[] = [0, 1, 2, 2, 3, 4];
1693     assert(x.median!float.approxEqual(2));
1694     x[] = [0, 2, 2, 3, 4, 5];
1695     assert(x.median!float.approxEqual(2.5));
1696 }
1697 
1698 // smallMedianImpl tests
1699 version(mir_stat_test)
1700 @safe pure nothrow
1701 unittest
1702 {
1703     import mir.math.common: approxEqual;
1704     import mir.ndslice.slice: sliced;
1705 
1706     auto x0 = [9.0, 1, 0, 2, 3].sliced;
1707     assert(x0.median.approxEqual(2));
1708 
1709     auto x1 = [9.0, 1, 0, 2].sliced;
1710     assert(x1.median.approxEqual(1.5));
1711     
1712     auto x2 = [9.0, 0, 1].sliced;
1713     assert(x2.median.approxEqual(1));
1714     
1715     auto x3 = [1.0, 0].sliced;
1716     assert(x3.median.approxEqual(0.5));
1717     
1718     auto x4 = [1.0].sliced;
1719     assert(x4.median.approxEqual(1));
1720 }
1721 
1722 // Check issue #328 fixed
1723 version(mir_stat_test)
1724 @safe pure nothrow
1725 unittest {
1726     import mir.ndslice.topology: iota;
1727 
1728     auto x = iota(18);
1729     auto y = median(x);
1730     assert(y == 8.5);
1731 }
1732 
1733 private pure @trusted nothrow @nogc
1734 F smallMedianImpl(F, Iterator)(Slice!Iterator slice) 
1735 {
1736     size_t n = slice.elementCount;
1737 
1738     assert(n > 0, "smallMedianImpl: slice must have elementCount greater than 0");
1739     assert(n <= 5, "smallMedianImpl: slice must have elementCount of 5 or less");
1740 
1741     import mir.functional: naryFun;
1742     import mir.ndslice.sorting: medianOf;
1743     import mir.utility: swapStars;
1744 
1745     auto sliceI0 = slice._iterator;
1746     
1747     if (n == 1) {
1748         return cast(F) *sliceI0;
1749     }
1750 
1751     auto sliceI1 = sliceI0;
1752     ++sliceI1;
1753 
1754     if (n > 2) {
1755         auto sliceI2 = sliceI1;
1756         ++sliceI2;
1757         alias less = naryFun!("a < b");
1758 
1759         if (n == 3) {
1760             medianOf!less(sliceI0, sliceI1, sliceI2);
1761             return cast(F) *sliceI1;
1762         } else {
1763             auto sliceI3 = sliceI2;
1764             ++sliceI3;
1765             if (n == 4) {
1766                 // Put min in slice[0], lower median in slice[1]
1767                 medianOf!less(sliceI0, sliceI1, sliceI2, sliceI3);
1768                 // Ensure slice[2] < slice[3]
1769                 medianOf!less(sliceI2, sliceI3);
1770                 return cast(F) (*sliceI1 + *sliceI2) / cast(F) 2;
1771             } else {
1772                 auto sliceI4 = sliceI3;
1773                 ++sliceI4;
1774                 medianOf!less(sliceI0, sliceI1, sliceI2, sliceI3, sliceI4);
1775                 return cast(F) *sliceI2;
1776             }
1777         }
1778     } else {
1779         return cast(F) (*sliceI0 + *sliceI1) / cast(F) 2;
1780     }
1781 }
1782 
1783 // smallMedianImpl tests
1784 version(mir_stat_test)
1785 @safe pure nothrow
1786 unittest
1787 {
1788     import mir.math.common: approxEqual;
1789     import mir.ndslice.slice: sliced;
1790 
1791     auto x0 = [9.0, 1, 0, 2, 3].sliced;
1792     assert(x0.smallMedianImpl!double.approxEqual(2));
1793 
1794     auto x1 = [9.0, 1, 0, 2].sliced;
1795     assert(x1.smallMedianImpl!double.approxEqual(1.5));
1796 
1797     auto x2 = [9.0, 0, 1].sliced;
1798     assert(x2.smallMedianImpl!double.approxEqual(1));
1799 
1800     auto x3 = [1.0, 0].sliced;
1801     assert(x3.smallMedianImpl!double.approxEqual(0.5));
1802 
1803     auto x4 = [1.0].sliced;
1804     assert(x4.smallMedianImpl!double.approxEqual(1));
1805 
1806     auto x5 = [2.0, 1, 0, 9].sliced;
1807     assert(x5.smallMedianImpl!double.approxEqual(1.5));
1808 
1809     auto x6 = [1.0, 2, 0, 9].sliced;
1810     assert(x6.smallMedianImpl!double.approxEqual(1.5));
1811 
1812     auto x7 = [1.0, 0, 9, 2].sliced;
1813     assert(x7.smallMedianImpl!double.approxEqual(1.5));
1814 }
1815 
1816 /++
1817 Output range that applies function `fun` to each input before summing
1818 +/
1819 struct MapSummator(alias fun, T, Summation summation) 
1820     if (isMutable!T)
1821 {
1822     ///
1823     Summator!(T, summation) summator;
1824 
1825     ///
1826     F sum(F = T)() const @property
1827     {
1828         return cast(F) summator.sum;
1829     }
1830     
1831     ///
1832     void put(Range)(Range r)
1833         if (isIterable!Range)
1834     {
1835         import mir.ndslice.topology: map;
1836         summator.put(r.map!fun);
1837     }
1838 
1839     ///
1840     void put()(T x)
1841     {
1842         summator.put(fun(x));
1843     }
1844 }
1845 
1846 ///
1847 version(mir_stat_test)
1848 @safe pure nothrow
1849 unittest
1850 {
1851     import mir.math.common: powi;
1852     import mir.ndslice.slice: sliced;
1853 
1854     alias f = (double x) => (powi(x, 2));
1855     MapSummator!(f, double, Summation.pairwise) x;
1856     x.put([0.0, 1, 2, 3, 4].sliced);
1857     assert(x.sum == 30.0);
1858     x.put(5);
1859     assert(x.sum == 55.0);
1860 }
1861 
1862 version(mir_stat_test)
1863 @safe pure nothrow
1864 unittest
1865 {
1866     import mir.ndslice.slice: sliced;
1867 
1868     alias f = (double x) => (x + 1);
1869     MapSummator!(f, double, Summation.pairwise) x;
1870     x.put([0.0, 1, 2, 3, 4].sliced);
1871     assert(x.sum == 15.0);
1872     x.put(5);
1873     assert(x.sum == 21.0);
1874 }
1875 
1876 version(mir_stat_test)
1877 @safe pure nothrow @nogc
1878 unittest
1879 {
1880     import mir.ndslice.slice: sliced;
1881 
1882     alias f = (double x) => (x + 1);
1883     MapSummator!(f, double, Summation.pairwise) x;
1884     static immutable a = [0.0, 1, 2, 3, 4];
1885     x.put(a.sliced);
1886     assert(x.sum == 15.0);
1887     x.put(5);
1888     assert(x.sum == 21.0);
1889 }
1890 
1891 version(mir_stat_test)
1892 @safe pure
1893 unittest
1894 {
1895     import mir.ndslice.fuse: fuse;
1896     import mir.ndslice.slice: sliced;
1897 
1898     alias f = (double x) => (x + 1);
1899     MapSummator!(f, double, Summation.pairwise) x;
1900     auto a = [
1901         [0.0, 1, 2],
1902         [3.0, 4, 5]
1903     ].fuse;
1904     auto b = [6.0, 7, 8].sliced;
1905     x.put(a);
1906     assert(x.sum == 21.0);
1907     x.put(b);
1908     assert(x.sum == 45.0);
1909 }
1910 
1911 /++
1912 Variance algorithms.
1913 
1914 See Also:
1915     $(WEB en.wikipedia.org/wiki/Algorithms_for_calculating_variance, Algorithms for calculating variance).
1916 +/
1917 enum VarianceAlgo
1918 {
1919     /++
1920     Performs Welford's online algorithm for updating variance. Can also `put`
1921     another VarianceAccumulator of different types, which uses the parallel
1922     algorithm from Chan et al., described above.
1923     +/
1924     online,
1925     
1926     /++
1927     Calculates variance using E(x^^2) - E(x)^2 (alowing for adjustments for 
1928     population/sample variance). This algorithm can be numerically unstable. As
1929     in: 
1930     E(x ^^ 2) - E(x) ^^ 2
1931     +/
1932     naive,
1933 
1934     /++
1935     Calculates variance using a two-pass algorithm whereby the input is first 
1936     centered and then the sum of squares is calculated from that. As in:
1937     E((x - E(x)) ^^ 2)
1938     +/
1939     twoPass,
1940 
1941     /++
1942     Calculates variance assuming the mean of the dataseries is zero. 
1943     +/
1944     assumeZeroMean,
1945     
1946     /++
1947     When slices, slice-like objects, or ranges are the inputs, uses the two-pass
1948     algorithm. When an individual data-point is added, uses the online algorithm.
1949     +/
1950     hybrid
1951 }
1952 
1953 ///
1954 struct VarianceAccumulator(T, VarianceAlgo varianceAlgo, Summation summation)
1955     if (isMutable!T && varianceAlgo == VarianceAlgo.naive)
1956 {
1957     import mir.math.sum: Summator;
1958 
1959     ///
1960     private MeanAccumulator!(T, summation) meanAccumulator;
1961 
1962     ///
1963     private Summator!(T, summation) summatorOfSquares;
1964 
1965     ///
1966     this(Range)(Range r)
1967         if (isIterable!Range)
1968     {
1969         import core.lifetime: move;
1970         this.put(r.move);
1971     }
1972 
1973     ///
1974     this()(T x)
1975     {
1976         this.put(x);
1977     }
1978 
1979 
1980     ///
1981     void put(Range)(Range r)
1982         if (isIterable!Range)
1983     {
1984         foreach(x; r)
1985         {
1986             this.put(x);
1987         }
1988     }
1989 
1990     ///
1991     void put()(T x)
1992     {
1993         meanAccumulator.put(x);
1994         summatorOfSquares.put(x * x);
1995     }
1996 
1997     ///
1998     void put(U, Summation sumAlgo)(VarianceAccumulator!(U, varianceAlgo, sumAlgo) v)
1999     {
2000         meanAccumulator.put(v.meanAccumulator);
2001         summatorOfSquares.put(v.sumOfSquares!T);
2002     }
2003 
2004 const:
2005 
2006     ///
2007     size_t count() @property
2008     {
2009         return meanAccumulator.count;
2010     }
2011     ///
2012     F mean(F = T)() const @property
2013     {
2014         return meanAccumulator.mean!F;
2015     }
2016     ///
2017     F sumOfSquares(F = T)()
2018     {
2019         return cast(F) summatorOfSquares.sum;
2020     }
2021     ///
2022     F centeredSumOfSquares(F = T)()
2023     {
2024         return sumOfSquares!F - count * mean!F * mean!F;
2025     }
2026     ///
2027     F variance(F = T)(bool isPopulation) @property
2028     in
2029     {
2030         assert(count > 1, "VarianceAccumulator.varaince: count must be larger than one");
2031     }
2032     do
2033     {
2034         return sumOfSquares!F / (count + isPopulation - 1) - 
2035             mean!F * mean!F * count / (count + isPopulation - 1);
2036     }
2037 }
2038 
2039 /// naive
2040 version(mir_stat_test)
2041 @safe pure nothrow
2042 unittest
2043 {
2044     import mir.math.common: approxEqual;
2045     import mir.ndslice.slice: sliced;
2046 
2047     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
2048               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2049 
2050     VarianceAccumulator!(double, VarianceAlgo.naive, Summation.naive) v;
2051     v.put(x);
2052     assert(v.variance(true).approxEqual(54.76562 / 12));
2053     assert(v.variance(false).approxEqual(54.76562 / 11));
2054 
2055     v.put(4.0);
2056     assert(v.variance(true).approxEqual(57.01923 / 13));
2057     assert(v.variance(false).approxEqual(57.01923 / 12));
2058 }
2059 
2060 // Can put VarianceAccumulator
2061 version(mir_stat_test)
2062 @safe pure nothrow
2063 unittest
2064 {
2065     import mir.ndslice.slice: sliced;
2066     import mir.test: shouldApprox;
2067 
2068     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
2069     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2070 
2071     VarianceAccumulator!(double, VarianceAlgo.naive, Summation.naive) v;
2072     v.put(x);
2073     VarianceAccumulator!(double, VarianceAlgo.naive, Summation.naive) w;
2074     w.put(y);
2075     v.put(w);
2076     v.variance(true).shouldApprox == 54.76562 / 12;
2077 }
2078 
2079 // Test input range
2080 version(mir_stat_test)
2081 @safe pure nothrow
2082 unittest
2083 {
2084     import mir.math.sum: Summation;
2085     import mir.test: should;
2086     import std.range: iota;
2087     import std.algorithm: map;
2088 
2089     auto x1 = iota(0, 5);
2090     auto v1 = VarianceAccumulator!(double, VarianceAlgo.naive, Summation.naive)(x1);
2091     v1.variance(true).should == 2;
2092     v1.centeredSumOfSquares.should == 10;
2093     auto x2 = x1.map!(a => 2 * a);
2094     auto v2 = VarianceAccumulator!(double, VarianceAlgo.naive, Summation.naive)(x2);
2095     v2.variance(true).should == 8;
2096 }
2097 
2098 ///
2099 struct VarianceAccumulator(T, VarianceAlgo varianceAlgo, Summation summation)
2100     if (isMutable!T && varianceAlgo == VarianceAlgo.online)
2101 {
2102     import mir.math.sum: Summator;
2103 
2104     ///
2105     private MeanAccumulator!(T, summation) meanAccumulator;
2106 
2107     ///
2108     private Summator!(T, summation) centeredSummatorOfSquares;
2109 
2110     ///
2111     this(Range)(Range r)
2112         if (isIterable!Range)
2113     {
2114         import core.lifetime: move;
2115         this.put(r.move);
2116     }
2117 
2118     ///
2119     this()(T x)
2120     {
2121         this.put(x);
2122     }
2123 
2124     ///
2125     void put(Range)(Range r)
2126         if (isIterable!Range)
2127     {
2128         foreach(x; r)
2129         {
2130             this.put(x);
2131         }
2132     }
2133 
2134     ///
2135     void put()(T x)
2136     {
2137         T delta = x;
2138         if (count > 0) {
2139             delta -= meanAccumulator.mean;
2140         }
2141         meanAccumulator.put(x);
2142         centeredSummatorOfSquares.put(delta * (x - meanAccumulator.mean));
2143     }
2144 
2145     ///
2146     void put(U, VarianceAlgo varAlgo, Summation sumAlgo)(VarianceAccumulator!(U, varAlgo, sumAlgo) v)
2147         if (varAlgo != VarianceAlgo.assumeZeroMean)
2148     {
2149         size_t oldCount = count;
2150         T delta = v.mean!T;
2151         if (oldCount > 0) {
2152             delta -= meanAccumulator.mean;
2153         }
2154         meanAccumulator.put!T(v.meanAccumulator);
2155         centeredSummatorOfSquares.put(v.centeredSumOfSquares!T + delta * delta * v.count * oldCount / count);
2156     }
2157 
2158 const:
2159 
2160     ///
2161     size_t count() @property
2162     {
2163         return meanAccumulator.count;
2164     }
2165     ///
2166     F mean(F = T)() const @property
2167     {
2168         return meanAccumulator.mean!F;
2169     }
2170     ///
2171     F centeredSumOfSquares(F = T)()
2172     {
2173         return cast(F) centeredSummatorOfSquares.sum;
2174     }
2175     ///
2176     F variance(F = T)(bool isPopulation) @property
2177     in
2178     {
2179         assert(count > 1, "VarianceAccumulator.variance: count must be larger than one");
2180     }
2181     do
2182     {
2183         return centeredSumOfSquares!F / (count + isPopulation - 1);
2184     }
2185 }
2186 
2187 /// online
2188 version(mir_stat_test)
2189 @safe pure nothrow
2190 unittest
2191 {
2192     import mir.math.common: approxEqual;
2193     import mir.ndslice.slice: sliced;
2194 
2195     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
2196               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2197 
2198     VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive) v;
2199     v.put(x);
2200     assert(v.variance(true).approxEqual(54.76562 / 12));
2201     assert(v.variance(false).approxEqual(54.76562 / 11));
2202 
2203     v.put(4.0);
2204     assert(v.variance(true).approxEqual(57.01923 / 13));
2205     assert(v.variance(false).approxEqual(57.01923 / 12));
2206 }
2207 
2208 // can put slices
2209 version(mir_stat_test)
2210 @safe pure nothrow
2211 unittest
2212 {
2213     import mir.math.common: approxEqual;
2214     import mir.ndslice.slice: sliced;
2215 
2216     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
2217     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2218 
2219     VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive) v;
2220     v.put(x);
2221     assert(v.variance(true).approxEqual(12.55208 / 6));
2222     assert(v.variance(false).approxEqual(12.55208 / 5));
2223 
2224     v.put(y);
2225     assert(v.variance(true).approxEqual(54.76562 / 12));
2226     assert(v.variance(false).approxEqual(54.76562 / 11));
2227 }
2228 
2229 // Can put accumulator (online)
2230 version(mir_stat_test)
2231 @safe pure nothrow
2232 unittest
2233 {
2234     import mir.math.common: approxEqual;
2235     import mir.ndslice.slice: sliced;
2236 
2237     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
2238     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2239 
2240     VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive) v;
2241     v.put(x);
2242     assert(v.variance(true).approxEqual(12.55208 / 6));
2243     assert(v.variance(false).approxEqual(12.55208 / 5));
2244 
2245     VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive) w;
2246     w.put(y);
2247     v.put(w);
2248     assert(v.variance(true).approxEqual(54.76562 / 12));
2249     assert(v.variance(false).approxEqual(54.76562 / 11));
2250 }
2251 
2252 // Can put accumulator (naive)
2253 version(mir_stat_test)
2254 @safe pure nothrow
2255 unittest
2256 {
2257     import mir.math.common: approxEqual;
2258     import mir.ndslice.slice: sliced;
2259 
2260     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
2261     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2262 
2263     VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive) v;
2264     v.put(x);
2265     assert(v.variance(true).approxEqual(12.55208 / 6));
2266     assert(v.variance(false).approxEqual(12.55208 / 5));
2267 
2268     VarianceAccumulator!(double, VarianceAlgo.naive, Summation.naive) w;
2269     w.put(y);
2270     v.put(w);
2271     assert(v.variance(true).approxEqual(54.76562 / 12));
2272     assert(v.variance(false).approxEqual(54.76562 / 11));
2273 }
2274 
2275 // Can put accumulator (twoPass)
2276 version(mir_stat_test)
2277 @safe pure nothrow
2278 unittest
2279 {
2280     import mir.math.common: approxEqual;
2281     import mir.ndslice.slice: sliced;
2282 
2283     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
2284     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2285 
2286     VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive) v;
2287     v.put(x);
2288     assert(v.variance(true).approxEqual(12.55208 / 6));
2289     assert(v.variance(false).approxEqual(12.55208 / 5));
2290 
2291     auto w = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(y);
2292     v.put(w);
2293     assert(v.variance(true).approxEqual(54.76562 / 12));
2294     assert(v.variance(false).approxEqual(54.76562 / 11));
2295 }
2296 
2297 // complex
2298 version(mir_stat_test)
2299 @safe pure nothrow
2300 unittest
2301 {
2302     import mir.complex.math: approxEqual;
2303     import mir.ndslice.slice: sliced;
2304     import mir.complex: Complex;
2305 
2306     auto x = [Complex!double(1.0, 3), Complex!double(2), Complex!double(3)].sliced;
2307 
2308     VarianceAccumulator!(Complex!double, VarianceAlgo.online, Summation.naive) v;
2309     v.put(x);
2310     assert(v.variance(true).approxEqual(Complex!double(-4.0, -6) / 3));
2311     assert(v.variance(false).approxEqual(Complex!double(-4.0, -6) / 2));
2312 }
2313 
2314 // Test input range
2315 version(mir_stat_test)
2316 @safe pure nothrow
2317 unittest
2318 {
2319     import mir.math.sum: Summation;
2320     import mir.test: should;
2321     import std.range: iota;
2322     import std.algorithm: map;
2323 
2324     auto x1 = iota(0, 5);
2325     auto v1 = VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive)(x1);
2326     v1.variance(true).should == 2;
2327     v1.centeredSumOfSquares.should == 10;
2328     auto x2 = x1.map!(a => 2 * a);
2329     auto v2 = VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive)(x2);
2330     v2.variance(true).should == 8;
2331 }
2332 
2333 ///
2334 struct VarianceAccumulator(T, VarianceAlgo varianceAlgo, Summation summation)
2335     if (isMutable!T && varianceAlgo == VarianceAlgo.twoPass)
2336 {
2337     import mir.math.sum: elementType, Summator;
2338     import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind;
2339     import std.range: isInputRange;
2340 
2341     ///
2342     private MeanAccumulator!(T, summation) meanAccumulator;
2343 
2344     ///
2345     private Summator!(T, summation) centeredSummatorOfSquares;
2346 
2347     ///
2348     this(Iterator, size_t N, SliceKind kind)(
2349          Slice!(Iterator, N, kind) slice)
2350     {
2351         import mir.functional: naryFun;
2352         import mir.ndslice.internal: LeftOp;
2353         import mir.ndslice.topology: vmap, map;
2354 
2355         meanAccumulator.put(slice.lightScope);
2356         centeredSummatorOfSquares.put(slice.vmap(LeftOp!("-", T)(meanAccumulator.mean)).map!(naryFun!"a * a"));
2357     }
2358 
2359     ///
2360     this(SliceLike)(SliceLike x)
2361         if (isConvertibleToSlice!SliceLike && !isSlice!SliceLike)
2362     {
2363         import mir.ndslice.slice: toSlice;
2364         this(x.toSlice);
2365     }
2366 
2367     ///
2368     this(Range)(Range range)
2369         if (isInputRange!Range && !isConvertibleToSlice!Range && is(elementType!Range : T))
2370     {
2371         import std.algorithm: map;
2372         meanAccumulator.put(range);
2373 
2374         auto centeredRangeMultiplier = range.map!(a => (a - mean)).map!("a * a");
2375         centeredSummatorOfSquares.put(centeredRangeMultiplier);
2376     }
2377 
2378 const:
2379 
2380     ///
2381     size_t count() @property
2382     {
2383         return meanAccumulator.count;
2384     }
2385     ///
2386     F mean(F = T)() const @property
2387     {
2388         return meanAccumulator.mean;
2389     }
2390     ///
2391     F centeredSumOfSquares(F = T)() const @property
2392     {
2393         return cast(F) centeredSummatorOfSquares.sum;
2394     }
2395     ///
2396     F variance(F = T)(bool isPopulation) @property
2397     in
2398     {
2399         assert(count > 1, "SkewnessAccumulator.variance: count must be larger than one");
2400     }
2401     do
2402     {
2403         return centeredSumOfSquares!F / (count + isPopulation - 1);
2404     }
2405 }
2406 
2407 /// twoPass
2408 version(mir_stat_test)
2409 @safe pure nothrow
2410 unittest
2411 {
2412     import mir.math.common: approxEqual;
2413     import mir.ndslice.slice: sliced;
2414 
2415     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
2416               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2417 
2418     auto v = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(x);
2419     assert(v.variance(true).approxEqual(54.76562 / 12));
2420     assert(v.variance(false).approxEqual(54.76562 / 11));
2421 }
2422 
2423 // dynamic array test
2424 version(mir_stat_test)
2425 @safe pure nothrow
2426 unittest
2427 {
2428     import mir.math.common: approxEqual;
2429 
2430     double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
2431                   2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
2432 
2433     auto v = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(x);
2434     assert(v.centeredSumOfSquares.approxEqual(54.76562));
2435 }
2436 
2437 // withAsSlice test
2438 version(mir_stat_test)
2439 @safe pure nothrow @nogc
2440 unittest
2441 {
2442     import mir.math.common: approxEqual;
2443     import mir.math.sum: sum;
2444     import mir.rc.array: RCArray;
2445 
2446     static immutable a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
2447                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
2448 
2449     auto x = RCArray!double(12);
2450     foreach(i, ref e; x)
2451         e = a[i];
2452 
2453     auto v = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(x);
2454     assert(v.centeredSumOfSquares.sum.approxEqual(54.76562));
2455 }
2456 
2457 // Test input range
2458 version(mir_stat_test)
2459 @safe pure nothrow
2460 unittest
2461 {
2462     import mir.math.sum: Summation;
2463     import mir.test: should;
2464     import std.range: iota;
2465     import std.algorithm: map;
2466 
2467     auto x1 = iota(0, 5);
2468     auto v1 = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(x1);
2469     v1.variance(true).should == 2;
2470     v1.centeredSumOfSquares.should == 10;
2471     auto x2 = x1.map!(a => 2 * a);
2472     auto v2 = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(x2);
2473     v2.variance(true).should == 8;
2474 }
2475 
2476 ///
2477 struct VarianceAccumulator(T, VarianceAlgo varianceAlgo, Summation summation)
2478     if (isMutable!T && varianceAlgo == VarianceAlgo.assumeZeroMean)
2479 {
2480     import mir.math.sum: Summator;
2481     import mir.ndslice.slice: Slice, SliceKind;
2482 
2483     private size_t _count;
2484     ///
2485     private Summator!(T, summation) centeredSummatorOfSquares;
2486 
2487     ///
2488     this(Range)(Range r)
2489         if (isIterable!Range)
2490     {
2491         this.put(r);
2492     }
2493 
2494     ///
2495     this()(T x)
2496     {
2497         this.put(x);
2498     }
2499 
2500     ///
2501     void put(Range)(Range r)
2502         if (isIterable!Range)
2503     {
2504         foreach(x; r)
2505         {
2506             this.put(x);
2507         }
2508     }
2509 
2510     ///
2511     void put()(T x)
2512     {
2513         _count++;
2514         centeredSummatorOfSquares.put(x * x);
2515     }
2516 
2517     ///
2518     void put(U, Summation sumAlgo)(VarianceAccumulator!(U, varianceAlgo, sumAlgo) v)
2519     {
2520         _count += v.count;
2521         centeredSummatorOfSquares.put(v.centeredSumOfSquares!T);
2522     }
2523 
2524 const:
2525 
2526     ///
2527     size_t count() @property
2528     {
2529         return _count;
2530     }
2531     ///
2532     F mean(F = T)() const @property
2533     {
2534         return cast(F) 0;
2535     }
2536     ///
2537     MeanAccumulator!(T, summation) meanAccumulator()()
2538     {
2539         typeof(return) m = { _count, T(0) };
2540         return m;
2541     }
2542     ///
2543     F centeredSumOfSquares(F = T)() const @property
2544     {
2545         return cast(F) centeredSummatorOfSquares.sum;
2546     }
2547     ///
2548     F variance(F = T)(bool isPopulation) @property
2549     {
2550         return centeredSumOfSquares!F / (count + isPopulation - 1);
2551     }
2552 }
2553 
2554 /// assumeZeroMean
2555 version(mir_stat_test)
2556 @safe pure nothrow
2557 unittest
2558 {
2559     import mir.math.common: approxEqual;
2560     import mir.stat.transform: center;
2561     import mir.ndslice.slice: sliced;
2562 
2563     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
2564               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2565     auto x = a.center;
2566 
2567     VarianceAccumulator!(double, VarianceAlgo.assumeZeroMean, Summation.naive) v;
2568     v.put(x);
2569     assert(v.variance(true).approxEqual(54.76562 / 12));
2570     assert(v.variance(false).approxEqual(54.76562 / 11));
2571     v.put(4.0);
2572     assert(v.variance(true).approxEqual(70.76562 / 13));
2573     assert(v.variance(false).approxEqual(70.76562 / 12));
2574 }
2575 
2576 // can put slices
2577 version(mir_stat_test)
2578 @safe pure nothrow
2579 unittest
2580 {
2581     import mir.math.common: approxEqual;
2582     import mir.stat.transform: center;
2583     import mir.ndslice.slice: sliced;
2584 
2585     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
2586               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2587     auto b = a.center;
2588     auto x = b[0 .. 6];
2589     auto y = b[6 .. $];
2590 
2591     VarianceAccumulator!(double, VarianceAlgo.assumeZeroMean, Summation.naive) v;
2592     v.put(x);
2593     assert(v.variance(true).approxEqual(13.492188 / 6));
2594     assert(v.variance(false).approxEqual(13.492188 / 5));
2595 
2596     v.put(y);
2597     assert(v.variance(true).approxEqual(54.76562 / 12));
2598     assert(v.variance(false).approxEqual(54.76562 / 11));
2599 }
2600 
2601 // can put two accumulator
2602 version(mir_stat_test)
2603 @safe pure nothrow
2604 unittest
2605 {
2606     import mir.math.common: approxEqual;
2607     import mir.stat.transform: center;
2608     import mir.ndslice.slice: sliced;
2609 
2610     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
2611               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2612     auto b = a.center;
2613     auto x = b[0 .. 6];
2614     auto y = b[6 .. $];
2615 
2616     VarianceAccumulator!(double, VarianceAlgo.assumeZeroMean, Summation.naive) v;
2617     v.put(x);
2618     assert(v.variance(true).approxEqual(13.492188 / 6));
2619     assert(v.variance(false).approxEqual(13.492188 / 5));
2620 
2621     VarianceAccumulator!(double, VarianceAlgo.assumeZeroMean, Summation.naive) w;
2622     w.put(y);
2623     v.put(w);
2624     assert(v.variance(true).approxEqual(54.76562 / 12));
2625     assert(v.variance(false).approxEqual(54.76562 / 11));
2626 }
2627 
2628 // complex
2629 version(mir_stat_test)
2630 @safe pure nothrow
2631 unittest
2632 {
2633     import mir.complex: Complex;
2634     import mir.complex.math: approxEqual;
2635     import mir.ndslice.slice: sliced;
2636     import mir.stat.transform: center;
2637 
2638     auto a = [Complex!double(1.0, 3), Complex!double(2), Complex!double(3)].sliced;
2639     auto x = a.center;
2640 
2641     VarianceAccumulator!(Complex!double, VarianceAlgo.assumeZeroMean, Summation.naive) v;
2642     v.put(x);
2643     assert(v.variance(true).approxEqual(Complex!double(-4.0, -6) / 3));
2644     assert(v.variance(false).approxEqual(Complex!double(-4.0, -6) / 2));
2645 }
2646 
2647 ///
2648 struct VarianceAccumulator(T, VarianceAlgo varianceAlgo, Summation summation)
2649     if (isMutable!T && varianceAlgo == VarianceAlgo.hybrid)
2650 {
2651     import mir.math.sum: elementType, Summator;
2652     import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind;
2653     import std.range: isInputRange;
2654 
2655     ///
2656     private MeanAccumulator!(T, summation) meanAccumulator;
2657 
2658     ///
2659     private Summator!(T, summation) centeredSummatorOfSquares;
2660 
2661     ///
2662     this(Iterator, size_t N, SliceKind kind)(
2663          Slice!(Iterator, N, kind) slice)
2664     {
2665         import mir.functional: naryFun;
2666         import mir.ndslice.internal: LeftOp;
2667         import mir.ndslice.topology: vmap, map;
2668 
2669         meanAccumulator.put(slice.lightScope);
2670         centeredSummatorOfSquares.put(slice.vmap(LeftOp!("-", T)(meanAccumulator.mean)).map!(naryFun!"a * a"));
2671     }
2672 
2673     ///
2674     this(SliceLike)(SliceLike x)
2675         if (isConvertibleToSlice!SliceLike && !isSlice!SliceLike)
2676     {
2677         import mir.ndslice.slice: toSlice;
2678         this(x.toSlice);
2679     }
2680 
2681     ///
2682     this(Range)(Range range)
2683         if (isIterable!Range && !isConvertibleToSlice!Range)
2684     {
2685         static if (isInputRange!Range && is(elementType!Range : T))
2686         {
2687             import std.algorithm: map;
2688             meanAccumulator.put(range);
2689 
2690             auto centeredRangeMultiplier = range.map!(a => (a - mean)).map!("a * a");
2691             centeredSummatorOfSquares.put(centeredRangeMultiplier);
2692         } else {
2693             this.put(range);
2694         }
2695     }
2696 
2697     ///
2698     void put(Range)(Range r)
2699         if (isIterable!Range)
2700     {
2701         static if (isInputRange!Range && is(elementType!Range : T)) {
2702             auto v = typeof(this)(r);
2703             this.put(v);
2704         } else{
2705             foreach(x; r)
2706             {
2707                 this.put(x);
2708             }
2709         }
2710     }
2711 
2712     ///
2713     void put()(T x)
2714     {
2715         T delta = x;
2716         if (count > 0) {
2717             delta -= meanAccumulator.mean;
2718         }
2719         meanAccumulator.put(x);
2720         centeredSummatorOfSquares.put(delta * (x - meanAccumulator.mean));
2721     }
2722 
2723     ///
2724     void put(U, VarianceAlgo varAlgo, Summation sumAlgo)(VarianceAccumulator!(U, varAlgo, sumAlgo) v)
2725         if (varAlgo != VarianceAlgo.assumeZeroMean)
2726     {
2727         size_t oldCount = count;
2728         T delta = v.mean!T;
2729         if (oldCount > 0) {
2730             delta -= meanAccumulator.mean;
2731         }
2732         meanAccumulator.put!T(v.meanAccumulator);
2733         centeredSummatorOfSquares.put(v.centeredSumOfSquares!T + delta * delta * v.count * oldCount / count);
2734     }
2735 
2736 const:
2737 
2738     ///
2739     size_t count() @property
2740     {
2741         return meanAccumulator.count;
2742     }
2743     ///
2744     F mean(F = T)() const @property
2745     {
2746         return meanAccumulator.mean!F;
2747     }
2748     ///
2749     F centeredSumOfSquares(F = T)()
2750     {
2751         return cast(F) centeredSummatorOfSquares.sum;
2752     }
2753     ///
2754     F variance(F = T)(bool isPopulation) @property
2755     in
2756     {
2757         assert(count > 1, "VarianceAccumulator.variance: count must be larger than one");
2758     }
2759     do
2760     {
2761         return centeredSumOfSquares!F / (count + isPopulation - 1);
2762     }
2763 }
2764 
2765 /// online
2766 version(mir_stat_test)
2767 @safe pure nothrow
2768 unittest
2769 {
2770     import mir.math.common: approxEqual;
2771     import mir.ndslice.slice: sliced;
2772 
2773     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
2774               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2775 
2776     auto v = VarianceAccumulator!(double, VarianceAlgo.hybrid, Summation.naive)(x);
2777     assert(v.variance(true).approxEqual(54.76562 / 12));
2778     assert(v.variance(false).approxEqual(54.76562 / 11));
2779 
2780     v.put(4.0);
2781     assert(v.variance(true).approxEqual(57.01923 / 13));
2782     assert(v.variance(false).approxEqual(57.01923 / 12));
2783 }
2784 
2785 // can put slices
2786 version(mir_stat_test)
2787 @safe pure nothrow
2788 unittest
2789 {
2790     import mir.math.common: approxEqual;
2791     import mir.ndslice.slice: sliced;
2792 
2793     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
2794     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2795 
2796     auto v = VarianceAccumulator!(double, VarianceAlgo.hybrid, Summation.naive)(x);
2797     assert(v.variance(true).approxEqual(12.55208 / 6));
2798     assert(v.variance(false).approxEqual(12.55208 / 5));
2799 
2800     v.put(y);
2801     assert(v.variance(true).approxEqual(54.76562 / 12));
2802     assert(v.variance(false).approxEqual(54.76562 / 11));
2803 }
2804 
2805 // Can put accumulator (hybrid)
2806 version(mir_stat_test)
2807 @safe pure nothrow
2808 unittest
2809 {
2810     import mir.math.common: approxEqual;
2811     import mir.ndslice.slice: sliced;
2812 
2813     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
2814     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2815 
2816     VarianceAccumulator!(double, VarianceAlgo.hybrid, Summation.naive) v;
2817     v.put(x);
2818     assert(v.variance(true).approxEqual(12.55208 / 6));
2819     assert(v.variance(false).approxEqual(12.55208 / 5));
2820 
2821     VarianceAccumulator!(double, VarianceAlgo.hybrid, Summation.naive) w;
2822     w.put(y);
2823     v.put(w);
2824     assert(v.variance(true).approxEqual(54.76562 / 12));
2825     assert(v.variance(false).approxEqual(54.76562 / 11));
2826 }
2827 
2828 // Can put accumulator (naive)
2829 version(mir_stat_test)
2830 @safe pure nothrow
2831 unittest
2832 {
2833     import mir.math.common: approxEqual;
2834     import mir.ndslice.slice: sliced;
2835 
2836     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
2837     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2838 
2839     VarianceAccumulator!(double, VarianceAlgo.hybrid, Summation.naive) v;
2840     v.put(x);
2841     assert(v.variance(true).approxEqual(12.55208 / 6));
2842     assert(v.variance(false).approxEqual(12.55208 / 5));
2843 
2844     VarianceAccumulator!(double, VarianceAlgo.naive, Summation.naive) w;
2845     w.put(y);
2846     v.put(w);
2847     assert(v.variance(true).approxEqual(54.76562 / 12));
2848     assert(v.variance(false).approxEqual(54.76562 / 11));
2849 }
2850 
2851 // Can put accumulator (online)
2852 version(mir_stat_test)
2853 @safe pure nothrow
2854 unittest
2855 {
2856     import mir.math.common: approxEqual;
2857     import mir.ndslice.slice: sliced;
2858 
2859     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
2860     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2861 
2862     VarianceAccumulator!(double, VarianceAlgo.hybrid, Summation.naive) v;
2863     v.put(x);
2864     assert(v.variance(true).approxEqual(12.55208 / 6));
2865     assert(v.variance(false).approxEqual(12.55208 / 5));
2866 
2867     VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive) w;
2868     w.put(y);
2869     v.put(w);
2870     assert(v.variance(true).approxEqual(54.76562 / 12));
2871     assert(v.variance(false).approxEqual(54.76562 / 11));
2872 }
2873 
2874 // Can put accumulator (twoPass)
2875 version(mir_stat_test)
2876 @safe pure nothrow
2877 unittest
2878 {
2879     import mir.math.common: approxEqual;
2880     import mir.ndslice.slice: sliced;
2881 
2882     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
2883     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
2884 
2885     VarianceAccumulator!(double, VarianceAlgo.hybrid, Summation.naive) v;
2886     v.put(x);
2887     assert(v.variance(true).approxEqual(12.55208 / 6));
2888     assert(v.variance(false).approxEqual(12.55208 / 5));
2889 
2890     auto w = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(y);
2891     v.put(w);
2892     assert(v.variance(true).approxEqual(54.76562 / 12));
2893     assert(v.variance(false).approxEqual(54.76562 / 11));
2894 }
2895 
2896 // complex
2897 version(mir_stat_test)
2898 @safe pure nothrow
2899 unittest
2900 {
2901     import mir.complex.math: approxEqual;
2902     import mir.ndslice.slice: sliced;
2903     import mir.complex: Complex;
2904 
2905     auto x = [Complex!double(1.0, 3), Complex!double(2), Complex!double(3)].sliced;
2906 
2907     VarianceAccumulator!(Complex!double, VarianceAlgo.hybrid, Summation.naive) v;
2908     v.put(x);
2909     assert(v.variance(true).approxEqual(Complex!double(-4.0, -6) / 3));
2910     assert(v.variance(false).approxEqual(Complex!double(-4.0, -6) / 2));
2911 }
2912 
2913 // Test input range
2914 version(mir_stat_test)
2915 @safe pure nothrow
2916 unittest
2917 {
2918     import mir.math.sum: Summation;
2919     import mir.test: should;
2920     import std.range: chunks, iota;
2921     import std.algorithm: map;
2922 
2923     auto x1 = iota(0, 5);
2924     auto v1 = VarianceAccumulator!(double, VarianceAlgo.hybrid, Summation.naive)(x1);
2925     v1.variance(true).should == 2;
2926     v1.centeredSumOfSquares.should == 10;
2927     auto x2 = x1.map!(a => 2 * a);
2928     auto v2 = VarianceAccumulator!(double, VarianceAlgo.hybrid, Summation.naive)(x2);
2929     v2.variance(true).should == 8;
2930     VarianceAccumulator!(double, VarianceAlgo.hybrid, Summation.naive) v3;
2931     v3.put(x1.chunks(1));
2932     v3.centeredSumOfSquares.should == 10;
2933     auto v4 = VarianceAccumulator!(double, VarianceAlgo.hybrid, Summation.naive)(x1.chunks(1));
2934     v4.centeredSumOfSquares.should == 10;
2935 }
2936 
2937 /++
2938 Calculates the variance of the input
2939 
2940 By default, if `F` is not floating point type or complex type, then the result
2941 will have a `double` type if `F` is implicitly convertible to a floating point 
2942 type or a type for which `isComplex!F` is true.
2943 
2944 Params:
2945     F = controls type of output
2946     varianceAlgo = algorithm for calculating variance (default: VarianceAlgo.hybrid)
2947     summation = algorithm for calculating sums (default: Summation.appropriate)
2948 Returns:
2949     The variance of the input, must be floating point or complex type
2950 +/
2951 template variance(
2952     F, 
2953     VarianceAlgo varianceAlgo = VarianceAlgo.hybrid, 
2954     Summation summation = Summation.appropriate)
2955 {
2956     /++
2957     Params:
2958         r = range, must be finite iterable
2959         isPopulation = true if population variance, false if sample variance (default)
2960     +/
2961     @fmamath meanType!F variance(Range)(Range r, bool isPopulation = false)
2962         if (isIterable!Range)
2963     {
2964         import core.lifetime: move;
2965 
2966         alias G = typeof(return);
2967         auto varianceAccumulator = VarianceAccumulator!(G, varianceAlgo, ResolveSummationType!(summation, Range, G))(r.move);
2968         return varianceAccumulator.variance(isPopulation);
2969     }
2970 
2971     /++
2972     Params:
2973         ar = values
2974     +/
2975     @fmamath meanType!F variance(scope const F[] ar...)
2976     {
2977         alias G = typeof(return);
2978         auto varianceAccumulator = VarianceAccumulator!(G, varianceAlgo, ResolveSummationType!(summation, const(G)[], G))(ar);
2979         return varianceAccumulator.variance(false);
2980     }
2981 }
2982 
2983 /// ditto
2984 template variance(
2985     VarianceAlgo varianceAlgo = VarianceAlgo.hybrid, 
2986     Summation summation = Summation.appropriate)
2987 {
2988     /++
2989     Params:
2990         r = range, must be finite iterable
2991         isPopulation = true if population variance, false if sample variance (default)
2992     +/
2993     @fmamath meanType!Range variance(Range)(Range r, bool isPopulation = false)
2994         if (isIterable!Range)
2995     {
2996         import core.lifetime: move;
2997 
2998         alias F = typeof(return);
2999         return .variance!(F, varianceAlgo, summation)(r.move, isPopulation);
3000     }
3001 
3002     /++
3003     Params:
3004         ar = values
3005     +/
3006     @fmamath meanType!T variance(T)(scope const T[] ar...)
3007     {
3008         alias F = typeof(return);
3009         return .variance!(F, varianceAlgo, summation)(ar);
3010     }
3011 }
3012 
3013 /// ditto
3014 template variance(F, string varianceAlgo, string summation = "appropriate")
3015 {
3016     mixin("alias variance = .variance!(F, VarianceAlgo." ~ varianceAlgo ~ ", Summation." ~ summation ~ ");");
3017 }
3018 
3019 /// ditto
3020 template variance(string varianceAlgo, string summation = "appropriate")
3021 {
3022     mixin("alias variance = .variance!(VarianceAlgo." ~ varianceAlgo ~ ", Summation." ~ summation ~ ");");
3023 }
3024 
3025 ///
3026 version(mir_stat_test)
3027 @safe pure nothrow
3028 unittest
3029 {
3030     import mir.math.common: approxEqual;
3031     import mir.complex.math: capproxEqual = approxEqual;
3032     import mir.ndslice.slice: sliced;
3033     import mir.complex;
3034     alias C = Complex!double;
3035 
3036     assert(variance([1.0, 2, 3]).approxEqual(2.0 / 2));
3037     assert(variance([1.0, 2, 3], true).approxEqual(2.0 / 3));
3038 
3039     assert(variance([C(1, 3), C(2), C(3)]).capproxEqual(C(-4, -6) / 2));
3040     
3041     assert(variance!float([0, 1, 2, 3, 4, 5].sliced(3, 2)).approxEqual(17.5 / 5));
3042     
3043     static assert(is(typeof(variance!float([1, 2, 3])) == float));
3044 }
3045 
3046 /// Variance of vector
3047 version(mir_stat_test)
3048 @safe pure nothrow
3049 unittest
3050 {
3051     import mir.math.common: approxEqual;
3052     import mir.ndslice.slice: sliced;
3053 
3054     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
3055               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
3056 
3057     assert(x.variance.approxEqual(54.76562 / 11));
3058 }
3059 
3060 /// Variance of matrix
3061 version(mir_stat_test)
3062 @safe pure
3063 unittest
3064 {
3065     import mir.math.common: approxEqual;
3066     import mir.ndslice.fuse: fuse;
3067 
3068     auto x = [
3069         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
3070         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
3071     ].fuse;
3072 
3073     assert(x.variance.approxEqual(54.76562 / 11));
3074 }
3075 
3076 /// Column variance of matrix
3077 version(mir_stat_test)
3078 @safe pure
3079 unittest
3080 {
3081     import mir.algorithm.iteration: all;
3082     import mir.math.common: approxEqual;
3083     import mir.ndslice.fuse: fuse;
3084     import mir.ndslice.topology: alongDim, byDim, map;
3085 
3086     auto x = [
3087         [0.0,  1.0, 1.5, 2.0], 
3088         [3.5, 4.25, 2.0, 7.5],
3089         [5.0,  1.0, 1.5, 0.0]
3090     ].fuse;
3091     auto result = [13.16667 / 2, 7.041667 / 2, 0.1666667 / 2, 30.16667 / 2];
3092 
3093     // Use byDim or alongDim with map to compute variance of row/column.
3094     assert(x.byDim!1.map!variance.all!approxEqual(result));
3095     assert(x.alongDim!0.map!variance.all!approxEqual(result));
3096 
3097     // FIXME
3098     // Without using map, computes the variance of the whole slice
3099     // assert(x.byDim!1.variance == x.sliced.variance);
3100     // assert(x.alongDim!0.variance == x.sliced.variance);
3101 }
3102 
3103 /// Can also set algorithm type
3104 version(mir_stat_test)
3105 @safe pure nothrow
3106 unittest
3107 {
3108     import mir.math.common: approxEqual;
3109     import mir.ndslice.slice: sliced;
3110 
3111     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
3112               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
3113 
3114     auto x = a + 1_000_000_000;
3115 
3116     auto y = x.variance;
3117     assert(y.approxEqual(54.76562 / 11));
3118 
3119     // The naive algorithm is numerically unstable in this case
3120     auto z0 = x.variance!"naive";
3121     assert(!z0.approxEqual(y));
3122     
3123     auto z1 = x.variance!"online";
3124     assert(z1.approxEqual(54.76562 / 11));
3125 
3126     // But the two-pass algorithm provides a consistent answer
3127     auto z2 = x.variance!"twoPass";
3128     assert(z2.approxEqual(y));
3129 
3130     // And the assumeZeroMean algorithm is way off
3131     auto z3 = x.variance!"assumeZeroMean";
3132     assert(z3.approxEqual(1.2e19 / 11));
3133 }
3134 
3135 /// Can also set algorithm or output type
3136 version(mir_stat_test)
3137 @safe pure nothrow
3138 unittest
3139 {
3140     import mir.math.common: approxEqual;
3141     import mir.ndslice.slice: sliced;
3142     import mir.ndslice.topology: repeat;
3143 
3144     //Set population variance, variance algorithm, sum algorithm or output type
3145 
3146     auto a = [1.0, 1e100, 1, -1e100].sliced;
3147     auto x = a * 10_000;
3148 
3149     /++
3150     Due to Floating Point precision, when centering `x`, subtracting the mean 
3151     from the second and fourth numbers has no effect. Further, after centering 
3152     and squaring `x`, the first and third numbers in the slice have precision 
3153     too low to be included in the centered sum of squares. 
3154     +/
3155     assert(x.variance(false).approxEqual(2.0e208 / 3));
3156     assert(x.variance(true).approxEqual(2.0e208 / 4));
3157 
3158     assert(x.variance!("online").approxEqual(2.0e208 / 3));
3159     assert(x.variance!("online", "kbn").approxEqual(2.0e208 / 3));
3160     assert(x.variance!("online", "kb2").approxEqual(2.0e208 / 3));
3161     assert(x.variance!("online", "precise").approxEqual(2.0e208 / 3));
3162     assert(x.variance!(double, "online", "precise").approxEqual(2.0e208 / 3));
3163     assert(x.variance!(double, "online", "precise")(true).approxEqual(2.0e208 / 4));
3164 
3165     auto y = uint.max.repeat(3);
3166     auto z = y.variance!ulong;
3167     assert(z == 0.0);
3168     static assert(is(typeof(z) == double));
3169 }
3170 
3171 /++
3172 For integral slices, pass output type as template parameter to ensure output
3173 type is correct.
3174 +/
3175 version(mir_stat_test)
3176 @safe pure nothrow
3177 unittest
3178 {
3179     import mir.math.common: approxEqual;
3180     import mir.ndslice.slice: sliced;
3181 
3182     auto x = [0, 1, 1, 2, 4, 4,
3183               2, 7, 5, 1, 2, 0].sliced;
3184 
3185     auto y = x.variance;
3186     assert(y.approxEqual(50.91667 / 11));
3187     static assert(is(typeof(y) == double));
3188 
3189     assert(x.variance!float.approxEqual(50.91667 / 11));
3190 }
3191 
3192 /++
3193 Variance works for complex numbers and other user-defined types (provided they
3194 can be converted to a floating point or complex type)
3195 +/
3196 version(mir_stat_test)
3197 @safe pure nothrow
3198 unittest
3199 {
3200     import mir.complex.math: approxEqual;
3201     import mir.ndslice.slice: sliced;
3202     import mir.complex;
3203     alias C = Complex!double;
3204 
3205     auto x = [C(1, 2), C(2, 3), C(3, 4), C(4, 5)].sliced;
3206     assert(x.variance.approxEqual((C(0, 10)) / 3));
3207 }
3208 
3209 /// Compute variance along specified dimention of tensors
3210 version(mir_stat_test)
3211 @safe pure
3212 unittest
3213 {
3214     import mir.algorithm.iteration: all;
3215     import mir.math.common: approxEqual;
3216     import mir.ndslice.fuse: fuse;
3217     import mir.ndslice.topology: as, iota, alongDim, map, repeat;
3218 
3219     auto x = [
3220         [0.0, 1, 2],
3221         [3.0, 4, 5]
3222     ].fuse;
3223 
3224     assert(x.variance.approxEqual(17.5 / 5));
3225 
3226     auto m0 = [4.5, 4.5, 4.5];
3227     assert(x.alongDim!0.map!variance.all!approxEqual(m0));
3228     assert(x.alongDim!(-2).map!variance.all!approxEqual(m0));
3229 
3230     auto m1 = [1.0, 1.0];
3231     assert(x.alongDim!1.map!variance.all!approxEqual(m1));
3232     assert(x.alongDim!(-1).map!variance.all!approxEqual(m1));
3233 
3234     assert(iota(2, 3, 4, 5).as!double.alongDim!0.map!variance.all!approxEqual(repeat(3600.0 / 2, 3, 4, 5)));
3235 }
3236 
3237 /// Arbitrary variance
3238 version(mir_stat_test)
3239 @safe pure nothrow @nogc
3240 unittest
3241 {
3242     assert(variance(1.0, 2, 3) == 1.0);
3243     assert(variance!float(1, 2, 3) == 1f);
3244 }
3245 
3246 // UCFS test
3247 version(mir_stat_test)
3248 @safe pure nothrow
3249 unittest
3250 {
3251     import mir.math.common: approxEqual;
3252 
3253     assert([1.0, 2, 3, 4].variance.approxEqual(5.0 / 3));
3254 }
3255 
3256 // testing types are right along dimension
3257 version(mir_stat_test)
3258 @safe pure nothrow
3259 unittest
3260 {
3261     import mir.algorithm.iteration: all;
3262     import mir.math.common: approxEqual;
3263     import mir.ndslice.topology: iota, alongDim, map;
3264 
3265     auto x = iota([2, 2], 1);
3266     auto y = x.alongDim!1.map!variance;
3267     assert(y.all!approxEqual([0.5, 0.5]));
3268     static assert(is(meanType!(typeof(y)) == double));
3269 }
3270 
3271 // @nogc test
3272 version(mir_stat_test)
3273 @safe pure nothrow @nogc
3274 unittest
3275 {
3276     import mir.math.common: approxEqual;
3277     import mir.ndslice.slice: sliced;
3278 
3279     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
3280                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
3281 
3282     assert(x.sliced.variance.approxEqual(54.76562 / 11));
3283     assert(x.sliced.variance!float.approxEqual(54.76562 / 11));
3284 }
3285 
3286 ///
3287 package(mir)
3288 template stdevType(T)
3289 {
3290     import mir.internal.utility: isFloatingPoint;
3291     
3292     alias U = meanType!T;
3293 
3294     static if (isFloatingPoint!U) {
3295         alias stdevType = U;
3296     } else {
3297         static assert(0, "stdevType: Can't calculate standard deviation of elements of type " ~ U.stringof);
3298     }
3299 }
3300 
3301 version(mir_stat_test)
3302 @safe pure nothrow @nogc
3303 unittest
3304 {
3305     static assert(is(stdevType!(int[]) == double));
3306     static assert(is(stdevType!(double[]) == double));
3307     static assert(is(stdevType!(float[]) == float));
3308 }
3309 
3310 version(mir_stat_test)
3311 @safe pure nothrow @nogc
3312 unittest
3313 {
3314     static struct Foo {
3315         float x;
3316         alias x this;
3317     }
3318 
3319     static assert(is(stdevType!(Foo[]) == float));
3320 }
3321 
3322 /++
3323 Calculates the standard deviation of the input
3324 
3325 By default, if `F` is not floating point type, then the result will have a
3326 `double` type if `F` is implicitly convertible to a floating point type.
3327 
3328 Params:
3329     F = controls type of output
3330     varianceAlgo = algorithm for calculating variance (default: VarianceAlgo.hybrid)
3331     summation = algorithm for calculating sums (default: Summation.appropriate)
3332 Returns:
3333     The standard deviation of the input, must be floating point type type
3334 +/
3335 template standardDeviation(
3336     F, 
3337     VarianceAlgo varianceAlgo = VarianceAlgo.hybrid, 
3338     Summation summation = Summation.appropriate)
3339 {
3340     import mir.math.common: sqrt;
3341 
3342     /++
3343     Params:
3344         r = range, must be finite iterable
3345         isPopulation = true if population standard deviation, false if sample standard deviation (default)
3346     +/
3347     @fmamath stdevType!F standardDeviation(Range)(Range r, bool isPopulation = false)
3348         if (isIterable!Range)
3349     {
3350         import core.lifetime: move;
3351         alias G = typeof(return);
3352         return r.move.variance!(G, varianceAlgo, ResolveSummationType!(summation, Range, G))(isPopulation).sqrt;
3353     }
3354 
3355     /++
3356     Params:
3357         ar = values
3358     +/
3359     @fmamath stdevType!F standardDeviation(scope const F[] ar...)
3360     {
3361         alias G = typeof(return);
3362         return ar.variance!(G, varianceAlgo, ResolveSummationType!(summation, const(G)[], G)).sqrt;
3363     }
3364 }
3365 
3366 /// ditto
3367 template standardDeviation(
3368     VarianceAlgo varianceAlgo = VarianceAlgo.hybrid, 
3369     Summation summation = Summation.appropriate)
3370 {
3371     /++
3372     Params:
3373         r = range, must be finite iterable
3374         isPopulation = true if population standard deviation, false if sample standard deviation (default)
3375     +/
3376     @fmamath stdevType!Range standardDeviation(Range)(Range r, bool isPopulation = false)
3377         if (isIterable!Range)
3378     {
3379         import core.lifetime: move;
3380 
3381         alias F = typeof(return);
3382         return .standardDeviation!(F, varianceAlgo, summation)(r.move, isPopulation);
3383     }
3384 
3385     /++
3386     Params:
3387         ar = values
3388     +/
3389     @fmamath stdevType!T standardDeviation(T)(scope const T[] ar...)
3390     {
3391         alias F = typeof(return);
3392         return .standardDeviation!(F, varianceAlgo, summation)(ar);
3393     }
3394 }
3395 
3396 /// ditto
3397 template standardDeviation(F, string varianceAlgo, string summation = "appropriate")
3398 {
3399     mixin("alias standardDeviation = .standardDeviation!(F, VarianceAlgo." ~ varianceAlgo ~ ", Summation." ~ summation ~ ");");
3400 }
3401 
3402 /// ditto
3403 template standardDeviation(string varianceAlgo, string summation = "appropriate")
3404 {
3405     mixin("alias standardDeviation = .standardDeviation!(VarianceAlgo." ~ varianceAlgo ~ ", Summation." ~ summation ~ ");");
3406 }
3407 
3408 ///
3409 version(mir_stat_test)
3410 @safe pure nothrow
3411 unittest
3412 {
3413     import mir.math.common: approxEqual, sqrt;
3414     import mir.ndslice.slice: sliced;
3415 
3416     assert(standardDeviation([1.0, 2, 3]).approxEqual(sqrt(2.0 / 2)));
3417     assert(standardDeviation([1.0, 2, 3], true).approxEqual(sqrt(2.0 / 3)));
3418     
3419     assert(standardDeviation!float([0, 1, 2, 3, 4, 5].sliced(3, 2)).approxEqual(sqrt(17.5 / 5)));
3420     
3421     static assert(is(typeof(standardDeviation!float([1, 2, 3])) == float));
3422 }
3423 
3424 /// Standard deviation of vector
3425 version(mir_stat_test)
3426 @safe pure nothrow
3427 unittest
3428 {
3429     import mir.math.common: approxEqual, sqrt;
3430     import mir.ndslice.slice: sliced;
3431 
3432     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
3433               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
3434 
3435     assert(x.standardDeviation.approxEqual(sqrt(54.76562 / 11)));
3436 }
3437 
3438 /// Standard deviation of matrix
3439 version(mir_stat_test)
3440 @safe pure
3441 unittest
3442 {
3443     import mir.math.common: approxEqual, sqrt;
3444     import mir.ndslice.fuse: fuse;
3445 
3446     auto x = [
3447         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
3448         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
3449     ].fuse;
3450 
3451     assert(x.standardDeviation.approxEqual(sqrt(54.76562 / 11)));
3452 }
3453 
3454 /// Column standard deviation of matrix
3455 version(mir_stat_test)
3456 @safe pure
3457 unittest
3458 {
3459     import mir.algorithm.iteration: all;
3460     import mir.math.common: approxEqual, sqrt;
3461     import mir.ndslice.fuse: fuse;
3462     import mir.ndslice.topology: alongDim, byDim, map;
3463 
3464     auto x = [
3465         [0.0,  1.0, 1.5, 2.0], 
3466         [3.5, 4.25, 2.0, 7.5],
3467         [5.0,  1.0, 1.5, 0.0]
3468     ].fuse;
3469     auto result = [13.16667 / 2, 7.041667 / 2, 0.1666667 / 2, 30.16667 / 2].map!sqrt;
3470 
3471     // Use byDim or alongDim with map to compute standardDeviation of row/column.
3472     assert(x.byDim!1.map!standardDeviation.all!approxEqual(result));
3473     assert(x.alongDim!0.map!standardDeviation.all!approxEqual(result));
3474 
3475     // FIXME
3476     // Without using map, computes the standardDeviation of the whole slice
3477     // assert(x.byDim!1.standardDeviation == x.sliced.standardDeviation);
3478     // assert(x.alongDim!0.standardDeviation == x.sliced.standardDeviation);
3479 }
3480 
3481 /// Can also set algorithm type
3482 version(mir_stat_test)
3483 @safe pure nothrow
3484 unittest
3485 {
3486     import mir.math.common: approxEqual, sqrt;
3487     import mir.ndslice.slice: sliced;
3488 
3489     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
3490               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
3491 
3492     auto x = a + 1_000_000_000;
3493 
3494     auto y = x.standardDeviation;
3495     assert(y.approxEqual(sqrt(54.76562 / 11)));
3496 
3497     // The naive algorithm is numerically unstable in this case
3498     auto z0 = x.standardDeviation!"naive";
3499     assert(!z0.approxEqual(y));
3500 
3501     // But the two-pass algorithm provides a consistent answer
3502     auto z1 = x.standardDeviation!"twoPass";
3503     assert(z1.approxEqual(y));
3504 }
3505 
3506 /// Can also set algorithm or output type
3507 version(mir_stat_test)
3508 @safe pure nothrow
3509 unittest
3510 {
3511     import mir.math.common: approxEqual, sqrt;
3512     import mir.ndslice.slice: sliced;
3513     import mir.ndslice.topology: repeat;
3514 
3515     //Set population standard deviation, standardDeviation algorithm, sum algorithm or output type
3516 
3517     auto a = [1.0, 1e100, 1, -1e100].sliced;
3518     auto x = a * 10_000;
3519 
3520     /++
3521     Due to Floating Point precision, when centering `x`, subtracting the mean 
3522     from the second and fourth numbers has no effect. Further, after centering 
3523     and squaring `x`, the first and third numbers in the slice have precision 
3524     too low to be included in the centered sum of squares. 
3525     +/
3526     assert(x.standardDeviation(false).approxEqual(sqrt(2.0e208 / 3)));
3527     assert(x.standardDeviation(true).approxEqual(sqrt(2.0e208 / 4)));
3528 
3529     assert(x.standardDeviation!("online").approxEqual(sqrt(2.0e208 / 3)));
3530     assert(x.standardDeviation!("online", "kbn").approxEqual(sqrt(2.0e208 / 3)));
3531     assert(x.standardDeviation!("online", "kb2").approxEqual(sqrt(2.0e208 / 3)));
3532     assert(x.standardDeviation!("online", "precise").approxEqual(sqrt(2.0e208 / 3)));
3533     assert(x.standardDeviation!(double, "online", "precise").approxEqual(sqrt(2.0e208 / 3)));
3534     assert(x.standardDeviation!(double, "online", "precise")(true).approxEqual(sqrt(2.0e208 / 4)));
3535 
3536     auto y = uint.max.repeat(3);
3537     auto z = y.standardDeviation!ulong;
3538     assert(z == 0.0);
3539     static assert(is(typeof(z) == double));
3540 }
3541 
3542 /++
3543 For integral slices, pass output type as template parameter to ensure output
3544 type is correct.
3545 +/
3546 version(mir_stat_test)
3547 @safe pure nothrow
3548 unittest
3549 {
3550     import mir.math.common: approxEqual, sqrt;
3551     import mir.ndslice.slice: sliced;
3552 
3553     auto x = [0, 1, 1, 2, 4, 4,
3554               2, 7, 5, 1, 2, 0].sliced;
3555 
3556     auto y = x.standardDeviation;
3557     assert(y.approxEqual(sqrt(50.91667 / 11)));
3558     static assert(is(typeof(y) == double));
3559 
3560     assert(x.standardDeviation!float.approxEqual(sqrt(50.91667 / 11)));
3561 }
3562 
3563 /++
3564 Variance works for other user-defined types (provided they
3565 can be converted to a floating point)
3566 +/
3567 version(mir_stat_test)
3568 @safe pure nothrow
3569 unittest
3570 {
3571     import mir.ndslice.slice: sliced;
3572     
3573     static struct Foo {
3574         float x;
3575         alias x this;
3576     }
3577     
3578     Foo[] foo = [Foo(1f), Foo(2f), Foo(3f)];
3579     assert(foo.standardDeviation == 1f);
3580 }
3581 
3582 /// Compute standard deviation along specified dimention of tensors
3583 version(mir_stat_test)
3584 @safe pure
3585 unittest
3586 {
3587     import mir.algorithm.iteration: all;
3588     import mir.math.common: approxEqual, sqrt;
3589     import mir.ndslice.fuse: fuse;
3590     import mir.ndslice.topology: as, iota, alongDim, map, repeat;
3591 
3592     auto x = [
3593         [0.0, 1, 2],
3594         [3.0, 4, 5]
3595     ].fuse;
3596 
3597     assert(x.standardDeviation.approxEqual(sqrt(17.5 / 5)));
3598 
3599     auto m0 = repeat(sqrt(4.5), 3);
3600     assert(x.alongDim!0.map!standardDeviation.all!approxEqual(m0));
3601     assert(x.alongDim!(-2).map!standardDeviation.all!approxEqual(m0));
3602 
3603     auto m1 = [1.0, 1.0];
3604     assert(x.alongDim!1.map!standardDeviation.all!approxEqual(m1));
3605     assert(x.alongDim!(-1).map!standardDeviation.all!approxEqual(m1));
3606 
3607     assert(iota(2, 3, 4, 5).as!double.alongDim!0.map!standardDeviation.all!approxEqual(repeat(sqrt(3600.0 / 2), 3, 4, 5)));
3608 }
3609 
3610 /// Arbitrary standard deviation
3611 version(mir_stat_test)
3612 @safe pure nothrow @nogc
3613 unittest
3614 {
3615     import mir.math.common: sqrt;
3616 
3617     assert(standardDeviation(1.0, 2, 3) == 1.0);
3618     assert(standardDeviation!float(1, 2, 3) == 1f);
3619 }
3620 
3621 version(mir_stat_test)
3622 @safe pure nothrow
3623 unittest
3624 {
3625     import mir.math.common: approxEqual, sqrt;
3626     assert([1.0, 2, 3, 4].standardDeviation.approxEqual(sqrt(5.0 / 3)));
3627 }
3628 
3629 version(mir_stat_test)
3630 @safe pure nothrow
3631 unittest
3632 {
3633     import mir.algorithm.iteration: all;
3634     import mir.math.common: approxEqual, sqrt;
3635     import mir.ndslice.topology: iota, alongDim, map;
3636 
3637     auto x = iota([2, 2], 1);
3638     auto y = x.alongDim!1.map!standardDeviation;
3639     assert(y.all!approxEqual([sqrt(0.5), sqrt(0.5)]));
3640     static assert(is(meanType!(typeof(y)) == double));
3641 }
3642 
3643 version(mir_stat_test)
3644 @safe pure @nogc nothrow
3645 unittest
3646 {
3647     import mir.math.common: approxEqual, sqrt;
3648     import mir.ndslice.slice: sliced;
3649 
3650     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
3651                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
3652 
3653     assert(x.sliced.standardDeviation.approxEqual(sqrt(54.76562 / 11)));
3654     assert(x.sliced.standardDeviation!float.approxEqual(sqrt(54.76562 / 11)));
3655 }
3656 
3657 /++
3658 Algorithms used to calculate the quantile of an input `x` at probability `p`.
3659 
3660 These algorithms match the same provided in R's (as of version 3.6.2) `quantile`
3661 function. In turn, these were discussed in Hyndman and Fan (1996). 
3662 
3663 All sample quantiles are defined as weighted averages of consecutive order
3664 statistics. For each `quantileAlgo`, the sample quantile is given by
3665 (using R's 1-based indexing notation):
3666 
3667     (1 - `gamma`) * `x$(SUBSCRIPT j)` + `gamma` * `x$(SUBSCRIPT j + 1)`
3668 
3669 
3670 where `x$(SUBSCRIPT j)` is the `j`th order statistic. `gamma` is a function of
3671 `j = floor(np + m)` and `g = np + m - j` where `n` is the sample size, `p` is
3672 the probability, and `m` is a constant determined by the quantile type.
3673 
3674 $(BOOKTABLE ,
3675     $(TR
3676         $(TH Type)
3677         $(TH m)
3678         $(TH gamma)
3679     )
3680     $(LEADINGROWN 3, Discontinuous sample quantile)
3681     $(T3 type1, 0, 0 if `g = 0` and 1 otherwise.)
3682     $(T3 type2, 0, 0.5 if `g = 0` and 1 otherwise.)
3683     $(T3 type3, -0.5, 0 if `g = 0` and `j` is even and 1 otherwise.)
3684     $(LEADINGROWN 3, Continuous sample quantile)
3685     $(T3 type4, 0, `gamma = g`)
3686     $(T3 type5, 0.5, `gamma = g`)
3687     $(T3 type6, `p`, `gamma = g`)
3688     $(T3 type7, `1 - p`, `gamma = g`)
3689     $(T3 type8, `(p + 1) / 3`, `gamma = g`)
3690     $(T3 type9, `p / 4 + 3 / 8`, `gamma = g`)
3691 )
3692 
3693 References:
3694     Hyndman, R. J. and Fan, Y. (1996) Sample quantiles in statistical packages, American Statistician 50, 361--365. 10.2307/2684934.
3695 
3696 See_also:
3697     $(LINK2 https://www.rdocumentation.org/packages/stats/versions/3.6.2/topics/quantile, quantile)
3698 +/
3699 enum QuantileAlgo {
3700     /++
3701     $(H4 Discontinuous sample quantile)
3702 
3703     Inverse of empirical distribution function.
3704     +/
3705     type1,
3706     /++
3707     Similar to type1, but averages at discontinuities.
3708     +/
3709     type2,
3710     /++
3711     SAS definition: nearest even order statistic.
3712     +/
3713     type3,
3714     /++
3715     $(H4 Continuous sample quantile)
3716 
3717     Linear interpolation of the empirical cdf.
3718     +/
3719     type4,
3720     /++
3721     A piece-wise linear function hwere the knots are the values midway through
3722     the steps of the empirical cdf. Popular amongst hydrologists.
3723     +/
3724     type5,
3725     /++
3726     Used by Minitab and by SPSS.
3727     +/
3728     type6,
3729     /++
3730     This is used by S and is the default for R.
3731     +/
3732     type7,
3733     /++
3734     The resulting quantile estimates are approximately median-unbiased
3735     regardless of the distribution of the input. Preferred by Hyndman and Fan
3736     (1996).
3737     +/
3738     type8,
3739     /++
3740     The resulting quantile estimates are approximately unbiased for the expected
3741     order statistics of the input is normally distributed.
3742     +/
3743     type9
3744 }
3745 
3746 /++
3747 For all $(LREF QuantileAlgo) except $(LREF QuantileAlgo.type1) and $(LREF QuantileAlgo.type3),
3748 this is an alias to the $(MATHREF stat, meanType) of `T`
3749 
3750 For $(LREF QuantileAlgo.type1) and $(LREF QuantileAlgo.type3), this is an alias to the
3751 $(MATHREF sum, elementType) of `T`.
3752 +/
3753 package(mir.stat)
3754 template quantileType(T, QuantileAlgo quantileAlgo)
3755 {
3756     static if (quantileAlgo == QuantileAlgo.type1 ||
3757                quantileAlgo == QuantileAlgo.type3)
3758     {
3759         import mir.math.sum: elementType;
3760 
3761         alias quantileType = elementType!T;
3762     }
3763     else
3764     {
3765         alias quantileType = meanType!T;
3766     }
3767 }
3768 
3769 version(mir_stat_test)
3770 @safe pure nothrow @nogc
3771 unittest
3772 {
3773     static assert(is(quantileType!(int[], QuantileAlgo.type1) == int));
3774     static assert(is(quantileType!(double[], QuantileAlgo.type1) == double));
3775     static assert(is(quantileType!(float[], QuantileAlgo.type1) == float));
3776 
3777     static assert(is(quantileType!(int[], QuantileAlgo.type2) == double));
3778     static assert(is(quantileType!(double[], QuantileAlgo.type2) == double));
3779     static assert(is(quantileType!(float[], QuantileAlgo.type2) == float));
3780 
3781     static assert(is(quantileType!(int[], QuantileAlgo.type3) == int));
3782     static assert(is(quantileType!(double[], QuantileAlgo.type3) == double));
3783     static assert(is(quantileType!(float[], QuantileAlgo.type3) == float));
3784 
3785     static assert(is(quantileType!(int[], QuantileAlgo.type4) == double));
3786     static assert(is(quantileType!(double[], QuantileAlgo.type4) == double));
3787     static assert(is(quantileType!(float[], QuantileAlgo.type4) == float));
3788 
3789     static assert(is(quantileType!(int[], QuantileAlgo.type5) == double));
3790     static assert(is(quantileType!(double[], QuantileAlgo.type5) == double));
3791     static assert(is(quantileType!(float[], QuantileAlgo.type5) == float));
3792 
3793     static assert(is(quantileType!(int[], QuantileAlgo.type6) == double));
3794     static assert(is(quantileType!(double[], QuantileAlgo.type6) == double));
3795     static assert(is(quantileType!(float[], QuantileAlgo.type6) == float));
3796 
3797     static assert(is(quantileType!(int[], QuantileAlgo.type7) == double));
3798     static assert(is(quantileType!(double[], QuantileAlgo.type7) == double));
3799     static assert(is(quantileType!(float[], QuantileAlgo.type7) == float));
3800 
3801     static assert(is(quantileType!(int[], QuantileAlgo.type8) == double));
3802     static assert(is(quantileType!(double[], QuantileAlgo.type8) == double));
3803     static assert(is(quantileType!(float[], QuantileAlgo.type8) == float));
3804 
3805     static assert(is(quantileType!(int[], QuantileAlgo.type9) == double));
3806     static assert(is(quantileType!(double[], QuantileAlgo.type9) == double));
3807     static assert(is(quantileType!(float[], QuantileAlgo.type9) == float));
3808 }
3809 
3810 version(mir_stat_test)
3811 @safe pure nothrow @nogc
3812 unittest
3813 {
3814     import mir.complex: Complex;
3815 
3816     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type1) == Complex!float));
3817     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type2) == Complex!float));
3818     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type3) == Complex!float));
3819     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type4) == Complex!float));
3820     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type5) == Complex!float));
3821     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type6) == Complex!float));
3822     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type7) == Complex!float));
3823     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type8) == Complex!float));
3824     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type9) == Complex!float));
3825 }
3826 
3827 version(mir_stat_test)
3828 @safe pure nothrow @nogc
3829 unittest
3830 {
3831     import std.complex: Complex;
3832 
3833     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type1) == Complex!float));
3834     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type2) == Complex!float));
3835     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type3) == Complex!float));
3836     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type4) == Complex!float));
3837     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type5) == Complex!float));
3838     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type6) == Complex!float));
3839     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type7) == Complex!float));
3840     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type8) == Complex!float));
3841     static assert(is(quantileType!(Complex!(float)[], QuantileAlgo.type9) == Complex!float));
3842 }
3843 
3844 version(mir_stat_test)
3845 @safe pure nothrow @nogc
3846 unittest
3847 {
3848     static struct Foo {
3849         float x;
3850         alias x this;
3851     }
3852 
3853     static assert(is(quantileType!(Foo[], QuantileAlgo.type7) == float));
3854 
3855     static assert(is(quantileType!(Foo[], QuantileAlgo.type1) == Foo));
3856     static assert(is(quantileType!(Foo[], QuantileAlgo.type3) == Foo));
3857 }
3858 
3859 version(mir_stat_test)
3860 @safe pure nothrow @nogc
3861 unittest
3862 {
3863     import mir.complex: Complex;
3864     static struct Foo {
3865         Complex!float x;
3866         alias x this;
3867     }
3868 
3869     static assert(is(quantileType!(Foo[], QuantileAlgo.type7) == Complex!float));
3870 }
3871 
3872 version(mir_stat_test)
3873 @safe pure nothrow @nogc
3874 unittest
3875 {
3876     import std.complex: Complex;
3877     static struct Foo {
3878         Complex!float x;
3879         alias x this;
3880     }
3881 
3882     static assert(is(quantileType!(Foo[], QuantileAlgo.type7) == Complex!float));
3883 }
3884 
3885 @fmamath private @safe pure nothrow @nogc
3886 auto quantileImpl(F, QuantileAlgo quantileAlgo, Iterator, G)(Slice!Iterator slice, G p)
3887     if ((isFloatingPoint!F || (quantileAlgo == QuantileAlgo.type1 || 
3888                                quantileAlgo == QuantileAlgo.type3)) &&
3889         isFloatingPoint!G)
3890 {
3891     assert(p >= 0 && p <= 1, "quantileImpl: p must be between 0 and 1");
3892     size_t n = slice.elementCount;
3893     assert(n > 1, "quantileImpl: slice.elementCount must be greater than 1");
3894 
3895     import mir.math.common: floor;
3896     import mir.ndslice.sorting: partitionAt;
3897     import std.traits: Unqual;
3898 
3899     alias GG = Unqual!G;
3900 
3901     GG m;
3902 
3903     static if (quantileAlgo == QuantileAlgo.type1) {
3904         m = 0;
3905     } else static if (quantileAlgo == QuantileAlgo.type2) {
3906         m = 0;
3907     } else static if (quantileAlgo == QuantileAlgo.type3) {
3908         m = -0.5;
3909     } else static if (quantileAlgo == QuantileAlgo.type4) {
3910         m = 0;
3911     } else static if (quantileAlgo == QuantileAlgo.type5) {
3912         m = 0.5;
3913     } else static if (quantileAlgo == QuantileAlgo.type6) {
3914         m = p;
3915     } else static if (quantileAlgo == QuantileAlgo.type7) {
3916         m = 1 - p;
3917     } else static if (quantileAlgo == QuantileAlgo.type8) {
3918         m = (p + 1) / 3;
3919     } else static if (quantileAlgo == QuantileAlgo.type9) {
3920         m = p / 4 + cast(GG) 3 / 8;
3921     }
3922 
3923     GG g = n * p + m - 1; //note: 0-based, not 1-based indexing
3924 
3925     GG pre_j = floor(g);
3926     GG pre_j_1 = pre_j + 1;
3927     size_t j;
3928     if (pre_j >= (n - 1)) { //note: 0-based, not 1-based indexing
3929         j = n - 1;
3930     } else if (pre_j < 0) {
3931         j = 0;
3932     } else {
3933         j = cast(size_t) pre_j;
3934     }
3935 
3936     size_t j_1;
3937     if (pre_j_1 >= (n - 1)) { //note: 0-based, not 1-based indexing
3938         j_1 = n - 1;
3939     } else if (pre_j_1 < 0) {
3940         j_1 = 0;
3941     } else {
3942         j_1 = cast(size_t) pre_j_1;
3943     }
3944 
3945     g -= j;
3946     GG gamma;
3947 
3948     static if (quantileAlgo == QuantileAlgo.type1) {
3949         if (g == 0) {
3950             gamma = 0;
3951         } else {
3952             gamma = 1;
3953         }
3954     } else static if (quantileAlgo == QuantileAlgo.type2) {
3955         if (g == 0) {
3956             gamma = 0.5;
3957         } else {
3958             gamma = 1;
3959         }
3960     } else static if (quantileAlgo == QuantileAlgo.type3) {
3961         if (g == 0 && (j + 1) % 2 == 0) { //need to adjust because 0-based indexing
3962             gamma = 0;
3963         } else {
3964             gamma = 1;
3965         }
3966     } else {
3967         gamma = g;
3968     }
3969 
3970     if (gamma == 0) {
3971         partitionAt(slice, j);
3972         return cast(F) slice[j];
3973     } else if (gamma == 1) {
3974         partitionAt(slice, j_1);
3975         return cast(F) slice[j_1];
3976     } else if (j != j_1) {
3977         partitionAt(slice, j_1);
3978         partitionAt(slice[0 .. j_1], j);
3979         return cast(F) ((1 - gamma) * slice[j] + gamma * slice[j_1]);
3980     } else {
3981         partitionAt(slice, j);
3982         return cast(F) slice[j];
3983     }
3984 }
3985 
3986 /++
3987 Computes the quantile(s) of the input, given one or more probabilities `p`.
3988 
3989 By default, if `p` is a $(NDSLICEREF slice, Slice), built-in dynamic array, or type
3990 with `asSlice`, then the output type is a reference-counted copy of the input. A
3991 compile-time parameter is provided to instead overwrite the input in-place.
3992 
3993 For all $(LREF QuantileAlgo) except $(LREF QuantileAlgo.type1) and $(LREF QuantileAlgo.type3),
3994 by default, if `F` is not floating point type or complex type, then the result
3995 will have a `double` type if `F` is implicitly convertible to a floating point 
3996 type or a type for which `isComplex!F` is true.
3997 
3998 For $(LREF QuantileAlgo.type1) and $(LREF QuantileAlgo.type3), the return type is the
3999 $(MATHREF sum, elementType) of the input.
4000 
4001 Params:
4002     F = controls type of output
4003     quantileAlgo = algorithm for calculating quantile (default: $(LREF QuantileAlgo.type7))
4004     allowModifySlice = controls whether the input is modified in place, default is false
4005 
4006 Returns:
4007     The quantile of all the elements in the input at probability `p`.
4008 
4009 See_also:
4010     $(LREF median),
4011     $(MATHREF sum, partitionAt),
4012     $(MATHREF sum, elementType)
4013 +/
4014 template quantile(F, 
4015                   QuantileAlgo quantileAlgo = QuantileAlgo.type7, 
4016                   bool allowModifySlice = false,
4017                   bool allowModifyProbability = false)
4018     if (isFloatingPoint!F || (quantileAlgo == QuantileAlgo.type1 || 
4019                               quantileAlgo == QuantileAlgo.type3))
4020 {
4021     import mir.math.sum: elementType;
4022     import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind, sliced;
4023     import mir.ndslice.topology: flattened;
4024     import std.traits: Unqual;
4025 
4026     /++
4027     Params:
4028         slice = slice
4029         p = probability
4030     +/
4031     quantileType!(F, quantileAlgo) quantile(Iterator, size_t N, SliceKind kind, G)
4032             (Slice!(Iterator, N, kind) slice, G p)
4033         if (isFloatingPoint!(Unqual!G))
4034     {
4035         import mir.ndslice.slice: IteratorOf;
4036         import std.traits: Unqual;
4037 
4038         alias FF = typeof(return);
4039         static if (!allowModifySlice) {
4040             import mir.ndslice.allocation: rcslice;
4041             import mir.ndslice.topology: as;
4042 
4043             auto view = slice.lightScope;
4044             auto val = view.as!(Unqual!(slice.DeepElement)).rcslice;
4045             auto temp = val.lightScope.flattened;
4046         } else {
4047             auto temp = slice.flattened;
4048         }
4049         return quantileImpl!(FF, quantileAlgo, IteratorOf!(typeof(temp)), Unqual!G)(temp, p);
4050     }
4051 
4052     /++
4053     Params:
4054         slice = slice
4055         p = probability slice
4056     +/
4057     auto quantile(IteratorA, size_t N, SliceKind kindA, IteratorB, SliceKind kindB)
4058             (Slice!(IteratorA, N, kindA) slice, 
4059              Slice!(IteratorB, 1, kindB) p)
4060         if (isFloatingPoint!(elementType!(Slice!(IteratorB))))
4061     {
4062         import mir.ndslice.allocation: rcslice;
4063         import mir.ndslice.slice: IteratorOf;
4064         import mir.ndslice.topology: as;
4065 
4066         alias G = elementType!(Slice!(IteratorB));
4067         alias FF = quantileType!(F, quantileAlgo);
4068 
4069         static if (!allowModifySlice) {
4070 
4071             auto view = slice.lightScope;
4072             auto val = view.as!(Unqual!(slice.DeepElement)).rcslice;
4073             auto temp = val.lightScope.flattened;
4074         } else {
4075             auto temp = slice.flattened;
4076         }
4077 
4078         static if (allowModifyProbability) {
4079             foreach(ref e; p) {
4080                 e = quantileImpl!(FF, quantileAlgo, IteratorOf!(typeof(temp)), G)(temp, e);
4081             }
4082             return p;
4083         } else {
4084             auto view_p = p.lightScope;
4085             auto val_p = view_p.as!G.rcslice;
4086             auto temp_p = val_p.lightScope.flattened;
4087             foreach(ref e; temp_p) {
4088                 e = quantileImpl!(FF, quantileAlgo, IteratorOf!(typeof(temp)), G)(temp, e);
4089             }
4090             return temp_p;
4091         }
4092     }
4093 
4094     /// ditto
4095     auto quantile(Iterator, size_t N, SliceKind kind)(
4096         Slice!(Iterator, N, kind) slice, scope const F[] p...)
4097         if (isFloatingPoint!(elementType!(F[])))
4098     {
4099         import mir.ndslice.allocation: rcslice;
4100         import mir.ndslice.slice: IteratorOf;
4101 
4102         alias G = elementType!(F[]);
4103         alias FF = quantileType!(F, quantileAlgo);
4104 
4105         static if (!allowModifySlice) {
4106             import mir.ndslice.allocation: rcslice;
4107             import mir.ndslice.topology: as;
4108 
4109             auto view = slice.lightScope;
4110             auto val = view.as!(Unqual!(slice.DeepElement)).rcslice;
4111             auto temp = val.lightScope.flattened;
4112         } else {
4113             auto temp = slice.flattened;
4114         }
4115 
4116         auto val_p = p.rcslice!G;
4117         auto temp_p = val_p.lightScope.flattened;
4118         foreach(ref e; temp_p) {
4119             e = quantileImpl!(FF, quantileAlgo, IteratorOf!(typeof(temp)), G)(temp, e);
4120         }
4121         return temp_p;
4122     }
4123 
4124     /// ditto
4125     auto quantile(SliceLike, G)(SliceLike x, G p)
4126         if (isConvertibleToSlice!SliceLike && !isSlice!SliceLike &&
4127             isFloatingPoint!(Unqual!G))
4128     {
4129         import mir.ndslice.slice: toSlice;
4130         return quantile(x.toSlice, p);
4131     }
4132 
4133     /// ditto
4134     auto quantile(SliceLikeX, SliceLikeP)(SliceLikeX x, SliceLikeP p)
4135         if (isConvertibleToSlice!SliceLikeX && !isSlice!SliceLikeX &&
4136             isConvertibleToSlice!SliceLikeP && !isSlice!SliceLikeP)
4137     {
4138         import mir.ndslice.slice: toSlice;
4139         return quantile(x.toSlice, p.toSlice);
4140     }
4141 }
4142 
4143 ///
4144 template quantile(QuantileAlgo quantileAlgo = QuantileAlgo.type7, 
4145                   bool allowModifySlice = false,
4146                   bool allowModifyProbability = false)
4147 {
4148     import mir.math.sum: elementType;
4149     import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind;
4150     import std.traits: Unqual;
4151 
4152     /++
4153     Params:
4154         slice = slice
4155         p = probability
4156     +/
4157     quantileType!(Slice!(Iterator), quantileAlgo) quantile(Iterator, size_t N, SliceKind kind, G)
4158             (Slice!(Iterator, N, kind) slice, G p)
4159         if (isFloatingPoint!(Unqual!G))
4160     {
4161         alias F = typeof(return);
4162 
4163         return .quantile!(F, quantileAlgo, allowModifySlice, allowModifyProbability)(slice, p);
4164     }
4165 
4166     /// ditto
4167     auto quantile(IteratorA, size_t N, SliceKind kindA, IteratorB, SliceKind kindB)
4168             (Slice!(IteratorA, N, kindA) slice, 
4169              Slice!(IteratorB, 1, kindB) p)
4170         if (isFloatingPoint!(elementType!(Slice!(IteratorB))))
4171     {
4172         alias F = quantileType!(Slice!(IteratorA), quantileAlgo);
4173         return .quantile!(F, quantileAlgo, allowModifySlice, allowModifyProbability)(slice, p);
4174     }
4175 
4176     /// ditto
4177     auto quantile(Iterator, size_t N, SliceKind kind, G)(
4178         Slice!(Iterator, N, kind) slice, scope G[] p...)
4179         if (isFloatingPoint!(elementType!(G[])))
4180     {
4181         alias F = quantileType!(Slice!(Iterator), quantileAlgo);
4182         return .quantile!(F, quantileAlgo, allowModifySlice, allowModifyProbability)(slice, p);
4183     }
4184 
4185     /// ditto
4186     auto quantile(SliceLike, G)(SliceLike x, G p)
4187         if (isConvertibleToSlice!SliceLike && !isSlice!SliceLike &&
4188             isFloatingPoint!(Unqual!G))
4189     {
4190         import mir.ndslice.slice: toSlice;
4191         alias F = quantileType!(typeof(x.toSlice), quantileAlgo);
4192         return .quantile!(F, quantileAlgo, allowModifySlice, allowModifyProbability)(x, p);
4193     }
4194 
4195     /// ditto
4196     auto quantile(SliceLikeX, SliceLikeP)(SliceLikeX x, SliceLikeP p)
4197         if (isConvertibleToSlice!SliceLikeX && !isSlice!SliceLikeX &&
4198             isConvertibleToSlice!SliceLikeP && !isSlice!SliceLikeP)
4199     {
4200         import mir.ndslice.slice: toSlice;
4201         alias F = quantileType!(typeof(x.toSlice), quantileAlgo);
4202         return .quantile!(F, quantileAlgo, allowModifySlice, allowModifyProbability)(x, p);
4203     }
4204 }
4205 
4206 /// ditto
4207 template quantile(F, string quantileAlgo,
4208                   bool allowModifySlice = false,
4209                   bool allowModifyProbability = false)
4210 {
4211     mixin("alias quantile = .quantile!(F, QuantileAlgo." ~ quantileAlgo ~ ", allowModifySlice, allowModifyProbability);");
4212 }
4213 
4214 /// ditto
4215 template quantile(string quantileAlgo,
4216                   bool allowModifySlice = false,
4217                   bool allowModifyProbability = false)
4218 {
4219     mixin("alias quantile = .quantile!(QuantileAlgo." ~ quantileAlgo ~ ", allowModifySlice, allowModifyProbability);");
4220 }
4221 
4222 /// Simple example
4223 version(mir_stat_test)
4224 @safe pure nothrow
4225 unittest 
4226 {
4227     import mir.algorithm.iteration: all;
4228     import mir.math.common: approxEqual;
4229     import mir.ndslice.slice: sliced;
4230 
4231     auto x = [3.0, 1.0, 4.0, 2.0, 0.0].sliced;
4232               
4233     assert(x.quantile(0.5).approxEqual(2.0));
4234 
4235     auto qtile = [0.25, 0.75].sliced;
4236 
4237     assert(x.quantile(qtile).all!approxEqual([1.0, 3.0]));
4238     assert(x.quantile(0.25, 0.75).all!approxEqual([1.0, 3.0]));
4239 }
4240 
4241 //no change in x by default
4242 version(mir_stat_test)
4243 @safe pure nothrow
4244 unittest 
4245 {
4246     import mir.algorithm.iteration: all;
4247     import mir.math.common: approxEqual;
4248     import mir.ndslice.slice: sliced;
4249 
4250     auto x = [3.0, 1.0, 4.0, 2.0, 0.0].sliced;
4251     auto x_copy = x.dup;
4252     auto result = x.quantile(0.5);
4253 
4254     assert(result.approxEqual(2.0));
4255     assert(x.all!approxEqual(x_copy));
4256 }
4257 
4258 /// Modify probability in place
4259 version(mir_stat_test)
4260 @safe pure nothrow
4261 unittest 
4262 {
4263     import mir.algorithm.iteration: all;
4264     import mir.math.common: approxEqual;
4265     import mir.ndslice.slice: sliced;
4266 
4267     auto x = [3.0, 1.0, 4.0, 2.0, 0.0].sliced;
4268 
4269     auto qtile = [0.25, 0.75].sliced;
4270     auto qtile_copy = qtile.dup;
4271 
4272     x.quantile!("type7", false, true)(qtile);
4273     assert(qtile.all!approxEqual([1.0, 3.0]));
4274     assert(!qtile.all!approxEqual(qtile_copy));
4275 }
4276 
4277 /// Quantile of vector
4278 version(mir_stat_test)
4279 @safe pure nothrow
4280 unittest 
4281 {
4282     import mir.algorithm.iteration: all;
4283     import mir.math.common: approxEqual;
4284     import mir.ndslice.slice: sliced;
4285 
4286     auto x = [1.0, 9.8, 0.2, 8.5, 5.8, 3.5, 4.5, 8.2, 5.2, 5.2,
4287               2.5, 1.8, 2.2, 3.8, 5.2, 9.2, 6.2, 9.2, 9.2, 8.5].sliced;
4288 
4289     assert(x.quantile(0.5).approxEqual(5.20));
4290 
4291     auto qtile = [0.25, 0.75].sliced;
4292 
4293     assert(x.quantile(qtile).all!approxEqual([3.250, 8.500]));
4294 }
4295 
4296 /// Quantile of matrix
4297 version(mir_stat_test)
4298 @safe pure
4299 unittest 
4300 {
4301     import mir.algorithm.iteration: all;
4302     import mir.math.common: approxEqual;
4303     import mir.ndslice.fuse: fuse;
4304     import mir.ndslice.slice: sliced;
4305 
4306     auto x = [
4307         [1.0, 9.8, 0.2, 8.5, 5.8, 3.5, 4.5, 8.2, 5.2, 5.2],
4308         [2.5, 1.8, 2.2, 3.8, 5.2, 9.2, 6.2, 9.2, 9.2, 8.5]
4309     ].fuse;
4310 
4311     assert(x.quantile(0.5).approxEqual(5.20));
4312 
4313     auto qtile = [0.25, 0.75].sliced;
4314 
4315     assert(x.quantile(qtile).all!approxEqual([3.250, 8.500]));
4316 }
4317 
4318 /// Row quantile of matrix
4319 version(mir_stat_test)
4320 @safe pure
4321 unittest
4322 {
4323     import mir.algorithm.iteration: all;
4324     import mir.math.common: approxEqual;
4325     import mir.ndslice.fuse: fuse;
4326     import mir.ndslice.slice: sliced;
4327     import mir.ndslice.topology: alongDim, byDim, map, flattened;
4328 
4329     auto x = [
4330         [1.0, 9.8, 0.2, 8.5, 5.8, 3.5, 4.5, 8.2, 5.2, 5.2],
4331         [2.5, 1.8, 2.2, 3.8, 5.2, 9.2, 6.2, 9.2, 9.2, 8.5]
4332     ].fuse;
4333 
4334     auto result0 = [5.200, 5.700];
4335 
4336     // Use byDim or alongDim with map to compute median of row/column.
4337     assert(x.byDim!0.map!(a => a.quantile(0.5)).all!approxEqual(result0));
4338     assert(x.alongDim!1.map!(a => a.quantile(0.5)).all!approxEqual(result0));
4339 
4340     auto qtile = [0.25, 0.75].sliced;
4341     auto result1 = [[3.750, 7.600], [2.825, 9.025]];
4342 
4343     assert(x.byDim!0.map!(a => a.quantile(qtile)).all!(all!approxEqual)(result1));
4344 }
4345 
4346 /// Allow modification of input
4347 version(mir_stat_test)
4348 @safe pure nothrow
4349 unittest 
4350 {
4351     import mir.algorithm.iteration: all;
4352     import mir.math.common: approxEqual;
4353     import mir.ndslice.slice: sliced;
4354 
4355     auto x = [3.0, 1.0, 4.0, 2.0, 0.0].sliced;
4356     auto x_copy = x.dup;
4357 
4358     auto result = x.quantile!(QuantileAlgo.type7, true)(0.5);
4359     assert(!x.all!approxEqual(x_copy));
4360 }
4361 
4362 /// Double-check probability is not modified
4363 version(mir_stat_test)
4364 @safe pure nothrow
4365 unittest 
4366 {
4367     import mir.algorithm.iteration: all;
4368     import mir.math.common: approxEqual;
4369     import mir.ndslice.slice: sliced;
4370 
4371     auto x = [3.0, 1.0, 4.0, 2.0, 0.0].sliced;
4372 
4373     auto qtile = [0.25, 0.75].sliced;
4374     auto qtile_copy = qtile.dup;
4375 
4376     auto result = x.quantile!("type7", false, false)(qtile);
4377     assert(result.all!approxEqual([1.0, 3.0]));
4378     assert(qtile.all!approxEqual(qtile_copy));
4379 }
4380 
4381 /// Can also set algorithm type
4382 version(mir_stat_test)
4383 @safe pure nothrow
4384 unittest
4385 {
4386     import mir.math.common: approxEqual;
4387     import mir.ndslice.slice: sliced;
4388 
4389     auto x = [1.0, 9.8, 0.2, 8.5, 5.8, 3.5, 4.5, 8.2, 5.2, 5.2,
4390               2.5, 1.8, 2.2, 3.8, 5.2, 9.2, 6.2, 9.2, 9.2, 8.5].sliced;
4391 
4392     assert(x.quantile!"type1"(0.5).approxEqual(5.20));
4393     assert(x.quantile!"type2"(0.5).approxEqual(5.20));
4394     assert(x.quantile!"type3"(0.5).approxEqual(5.20));
4395     assert(x.quantile!"type4"(0.5).approxEqual(5.20));
4396     assert(x.quantile!"type5"(0.5).approxEqual(5.20));
4397     assert(x.quantile!"type6"(0.5).approxEqual(5.20));
4398     assert(x.quantile!"type7"(0.5).approxEqual(5.20));
4399     assert(x.quantile!"type8"(0.5).approxEqual(5.20));
4400     assert(x.quantile!"type9"(0.5).approxEqual(5.20));
4401 }
4402 
4403 /// Can also set algorithm or output type
4404 version(mir_stat_test)
4405 @safe pure nothrow
4406 unittest
4407 {
4408     import mir.ndslice.slice: sliced;
4409 
4410     auto a = [1, 1e100, 1, -1e100].sliced;
4411 
4412     auto x = a * 10_000;
4413 
4414     auto result0 = x.quantile!float(0.5);
4415     assert(result0 == 10_000f);
4416     static assert(is(typeof(result0) == float));
4417 
4418     auto result1 = x.quantile!(float, "type8")(0.5);
4419     assert(result1 == 10_000f);
4420     static assert(is(typeof(result1) == float));
4421 }
4422 
4423 /// Support for integral and user-defined types for type 1 & 3
4424 version(mir_stat_test)
4425 @safe pure nothrow
4426 unittest
4427 {
4428     import mir.ndslice.topology: repeat;
4429 
4430     auto x = uint.max.repeat(3);
4431     assert(x.quantile!(uint, "type1")(0.5) == uint.max);
4432     assert(x.quantile!(uint, "type3")(0.5) == uint.max);
4433 
4434     static struct Foo {
4435         float x;
4436         alias x this;
4437     }
4438 
4439     Foo[] foo = [Foo(1f), Foo(2f), Foo(3f)];
4440     assert(foo.quantile!"type1"(0.5) == 2f);
4441     assert(foo.quantile!"type3"(0.5) == 2f);
4442 }
4443 
4444 /// Compute quantile along specified dimention of tensors
4445 version(mir_stat_test)
4446 @safe pure
4447 unittest
4448 {
4449     import mir.algorithm.iteration: all;
4450     import mir.math.common: approxEqual;
4451     import mir.ndslice.fuse: fuse;
4452     import mir.ndslice.topology: as, iota, alongDim, map, repeat;
4453 
4454     auto x = [
4455         [0.0, 1, 3],
4456         [4.0, 5, 7]
4457     ].fuse;
4458 
4459     assert(x.quantile(0.5).approxEqual(3.5));
4460 
4461     auto m0 = [2.0, 3.0, 5.0];
4462     assert(x.alongDim!0.map!(a => a.quantile(0.5)).all!approxEqual(m0));
4463     assert(x.alongDim!(-2).map!(a => a.quantile(0.5)).all!approxEqual(m0));
4464 
4465     auto m1 = [1.0, 5.0];
4466     assert(x.alongDim!1.map!(a => a.quantile(0.5)).all!approxEqual(m1));
4467     assert(x.alongDim!(-1).map!(a => a.quantile(0.5)).all!approxEqual(m1));
4468 
4469     assert(iota(2, 3, 4, 5).as!double.alongDim!0.map!(a => a.quantile(0.5)).all!approxEqual(iota([3, 4, 5], 3 * 4 * 5 / 2)));
4470 }
4471 
4472 /// Support for array
4473 version(mir_stat_test)
4474 @safe pure nothrow
4475 unittest 
4476 {
4477     import mir.algorithm.iteration: all;
4478     import mir.math.common: approxEqual;
4479 
4480     double[] x = [3.0, 1.0, 4.0, 2.0, 0.0];
4481               
4482     assert(x.quantile(0.5).approxEqual(2.0));
4483 
4484     double[] qtile = [0.25, 0.75];
4485 
4486     assert(x.quantile(qtile).all!approxEqual([1.0, 3.0]));
4487 }
4488 
4489 //@nogc test
4490 version(mir_stat_test)
4491 @safe pure nothrow @nogc
4492 unittest 
4493 {
4494     import mir.algorithm.iteration: all;
4495     import mir.math.common: approxEqual;
4496     import mir.ndslice.slice: sliced;
4497 
4498     static immutable x = [1.0, 9.8, 0.2, 8.5, 5.8, 3.5, 4.5, 8.2, 5.2, 5.2,
4499                           2.5, 1.8, 2.2, 3.8, 5.2, 9.2, 6.2, 9.2, 9.2, 8.5];
4500 
4501     assert(x.sliced.quantile(0.5).approxEqual(5.20));
4502 
4503     static immutable qtile = [0.25, 0.75];
4504     static immutable result = [3.250, 8.500];
4505 
4506     assert(x.sliced.quantile(qtile).all!approxEqual(result));
4507 }
4508 
4509 // withAsSlice test
4510 version(mir_stat_test)
4511 @safe pure nothrow @nogc
4512 unittest
4513 {
4514     import mir.algorithm.iteration: all;
4515     import mir.math.common: approxEqual;
4516     import mir.rc.array: RCArray;
4517 
4518     static immutable a = [1.0, 9.8, 0.2, 8.5, 5.8, 3.5, 4.5, 8.2, 5.2, 5.2,
4519                           2.5, 1.8, 2.2, 3.8, 5.2, 9.2, 6.2, 9.2, 9.2, 8.5];
4520 
4521     auto x = RCArray!double(20);
4522     foreach(i, ref e; x)
4523         e = a[i];
4524 
4525     assert(x.quantile(0.5).approxEqual(5.20));
4526 
4527     auto qtile = RCArray!double(2);
4528     qtile[0] = 0.25;
4529     qtile[1] = 0.75;
4530     static immutable result = [3.250, 8.500];
4531 
4532     assert(x.quantile(qtile).all!approxEqual(result));
4533 }
4534 
4535 //x.length = 20, qtile at tenths
4536 version(mir_stat_test)
4537 @safe pure nothrow
4538 unittest 
4539 {
4540     import mir.algorithm.iteration: all;
4541     import mir.math.common: approxEqual;
4542     import mir.ndslice.slice: sliced;
4543 
4544     auto x = [1.0, 9.8, 0.2, 8.5, 5.8, 3.5, 4.5, 8.2, 5.2, 5.2,
4545               2.5, 1.8, 2.2, 3.8, 5.2, 9.2, 6.2, 9.2, 9.2, 8.5].sliced;
4546     auto qtile = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0].sliced;
4547 
4548     assert(x.quantile!"type1"(qtile.dup).all!approxEqual([0.2, 1.0, 2.2, 3.5, 4.5, 5.2, 5.8, 8.2, 8.5, 9.2, 9.8]));
4549     assert(x.quantile!"type2"(qtile.dup).all!approxEqual([0.2, 1.4, 2.35, 3.65, 4.85, 5.2, 6.0, 8.35, 8.85, 9.2, 9.8]));   
4550     assert(x.quantile!"type3"(qtile.dup).all!approxEqual([0.2, 1.0, 2.2, 3.5, 4.5, 5.2, 5.8, 8.2, 8.5, 9.2, 9.8]));
4551     assert(x.quantile!"type4"(qtile.dup).all!approxEqual([0.2, 1.0, 2.2, 3.5, 4.5, 5.2, 5.8, 8.2, 8.5, 9.2, 9.8]));
4552     assert(x.quantile!"type5"(qtile.dup).all!approxEqual([0.20, 1.40, 2.35, 3.65, 4.85, 5.20, 6.00, 8.35, 8.85, 9.20, 9.80]));
4553     assert(x.quantile!"type6"(qtile.dup).all!approxEqual([0.20, 1.08, 2.26, 3.59, 4.78, 5.20, 6.04, 8.41, 9.06, 9.20, 9.80]));
4554     assert(x.quantile!"type7"(qtile.dup).all!approxEqual([0.20, 1.72, 2.44, 3.71, 4.92, 5.20, 5.96, 8.29, 8.64, 9.20, 9.80]));
4555     assert(x.quantile!"type8"(qtile.dup).all!approxEqual([0.200000, 1.293333, 2.320000, 3.630000, 4.826667, 5.200000, 6.013333, 8.370000, 8.920000, 9.200000, 9.800000]));
4556     assert(x.quantile!"type9"(qtile.dup).all!approxEqual([0.2000, 1.3200, 2.3275, 3.6350, 4.8325, 5.2000, 6.0100, 8.3650, 8.9025, 9.200, 9.800]));
4557 }
4558 
4559 //x.length = 20, qtile at 5s
4560 version(mir_stat_test)
4561 @safe pure nothrow
4562 unittest 
4563 {
4564     import mir.algorithm.iteration: all;
4565     import mir.math.common: approxEqual;
4566     import mir.ndslice.slice: sliced;
4567 
4568     auto x = [1.0, 9.8, 0.2, 8.5, 5.8, 3.5, 4.5, 8.2, 5.2, 5.2,
4569               2.5, 1.8, 2.2, 3.8, 5.2, 9.2, 6.2, 9.2, 9.2, 8.5].sliced;
4570     auto qtile = [0.05, 0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95].sliced;
4571 
4572     assert(x.quantile!"type1"(qtile.dup).all!approxEqual([0.2, 1.8, 2.5, 3.8, 5.2, 5.2, 6.2, 8.5, 9.2, 9.2]));
4573     assert(x.quantile!"type2"(qtile.dup).all!approxEqual([0.60, 2.00, 3.00, 4.15, 5.20, 5.50, 7.20, 8.50, 9.20, 9.50]));   
4574     assert(x.quantile!"type3"(qtile.dup).all!approxEqual([0.2, 1.8, 2.5, 3.8, 5.2, 5.2, 6.2, 8.5, 9.2, 9.2]));
4575     assert(x.quantile!"type4"(qtile.dup).all!approxEqual([0.2, 1.8, 2.5, 3.8, 5.2, 5.2, 6.2, 8.5, 9.2, 9.2]));
4576     assert(x.quantile!"type5"(qtile.dup).all!approxEqual([0.60, 2.00, 3.00, 4.15, 5.20, 5.50, 7.20, 8.50, 9.20, 9.50]));
4577     assert(x.quantile!"type6"(qtile.dup).all!approxEqual([0.240, 1.860, 2.750, 4.045, 5.200, 5.530, 7.500, 8.500, 9.200, 9.770]));
4578     assert(x.quantile!"type7"(qtile.dup).all!approxEqual([0.960, 2.140, 3.250, 4.255, 5.200, 5.470, 6.900, 8.500, 9.200, 9.230]));
4579     assert(x.quantile!"type8"(qtile.dup).all!approxEqual([0.480000, 1.953333, 2.916667, 4.115000, 5.200000, 5.510000, 7.300000, 8.500000, 9.200000, 9.590000]));
4580     assert(x.quantile!"type9"(qtile.dup).all!approxEqual([0.51000, 1.96500, 2.93750, 4.12375, 5.20000, 5.50750, 7.27500, 8.50000, 9.20000, 9.56750]));
4581 }
4582 
4583 //x.length = 21, qtile at tenths
4584 version(mir_stat_test)
4585 @safe pure nothrow
4586 unittest 
4587 {
4588     import mir.algorithm.iteration: all;
4589     import mir.math.common: approxEqual;
4590     import mir.ndslice.slice: sliced;
4591 
4592     auto x = [ 1.0, 9.3, 0.2, 8.1, 5.5, 3.3, 4.3, 7.9, 5.0, 5.0, 
4593               10.0, 2.4, 1.7, 2.1, 3.6, 5.0, 8.8, 9.8, 6.0, 8.8, 
4594                8.8].sliced;
4595     auto qtile = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0].sliced;
4596 
4597     assert(x.quantile!"type1"(qtile.dup).all!approxEqual([0.2, 1.7, 2.4, 3.6, 5.0, 5.0, 6.0, 8.1, 8.8, 9.3, 10.0]));
4598     assert(x.quantile!"type2"(qtile.dup).all!approxEqual([0.2, 1.7, 2.4, 3.6, 5.0, 5.0, 6.0, 8.1, 8.8, 9.3, 10.0]));   
4599     assert(x.quantile!"type3"(qtile.dup).all!approxEqual([0.2, 1.0, 2.1, 3.3, 4.3, 5.0, 6.0, 8.1, 8.8, 9.3, 10.0]));
4600     assert(x.quantile!"type4"(qtile.dup).all!approxEqual([0.20, 1.07, 2.16, 3.39, 4.58, 5.00, 5.80, 8.04, 8.80, 9.25, 10.00]));
4601     assert(x.quantile!"type5"(qtile.dup).all!approxEqual([0.20, 1.42, 2.31, 3.54, 4.93, 5.00, 6.19, 8.24, 8.80, 9.50, 10.00]));
4602     assert(x.quantile!"type6"(qtile.dup).all!approxEqual([0.20, 1.14, 2.22, 3.48, 4.86, 5.00, 6.38, 8.38, 8.80, 9.70, 10.00]));
4603     assert(x.quantile!"type7"(qtile.dup).all!approxEqual([0.2, 1.7, 2.4, 3.6, 5.0, 5.0, 6.0, 8.1, 8.8, 9.3, 10.0]));
4604     assert(x.quantile!"type8"(qtile.dup).all!approxEqual([0.200000, 1.326667, 2.280000, 3.520000, 4.906667, 5.000000, 6.253333, 8.286667, 8.800000, 9.566667, 10.000000]));
4605     assert(x.quantile!"type9"(qtile.dup).all!approxEqual([0.2000, 1.3500, 2.2875, 3.5250, 4.9125, 5.0000, 6.2375, 8.2750, 8.8000, 9.5500, 10.0000]));
4606 }
4607 
4608 //x.length = 21, qtile at 5s
4609 version(mir_stat_test)
4610 @safe pure nothrow
4611 unittest 
4612 {
4613     import mir.algorithm.iteration: all;
4614     import mir.math.common: approxEqual;
4615     import mir.ndslice.slice: sliced;
4616 
4617     auto x = [ 1.0, 9.3, 0.2, 8.1, 5.5, 3.3, 4.3, 7.9, 5.0, 5.0, 
4618               10.0, 2.4, 1.7, 2.1, 3.6, 5.0, 8.8, 9.8, 6.0, 8.8, 
4619                8.8].sliced;
4620     auto qtile = [0.05, 0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95].sliced;
4621 
4622     assert(x.quantile!"type1"(qtile.dup).all!approxEqual([1.0, 2.1, 3.3, 4.3, 5.0, 5.5, 7.9, 8.8, 8.8, 9.8]));
4623     assert(x.quantile!"type2"(qtile.dup).all!approxEqual([1.0, 2.1, 3.3, 4.3, 5.0, 5.5, 7.9, 8.8, 8.8, 9.8]));   
4624     assert(x.quantile!"type3"(qtile.dup).all!approxEqual([0.2, 1.7, 2.4, 3.6, 5.0, 5.5, 7.9, 8.8, 8.8, 9.8]));
4625     assert(x.quantile!"type4"(qtile.dup).all!approxEqual([0.240, 1.760, 2.625, 3.845, 5.000, 5.275, 7.235, 8.625, 8.800, 9.775]));
4626     assert(x.quantile!"type5"(qtile.dup).all!approxEqual([0.640, 1.960, 3.075, 4.195, 5.000, 5.525, 7.930, 8.800, 8.975, 9.890]));
4627     assert(x.quantile!"type6"(qtile.dup).all!approxEqual([0.28, 1.82, 2.85, 4.09, 5.00, 5.55, 7.96, 8.80, 9.15, 9.98]));
4628     assert(x.quantile!"type7"(qtile.dup).all!approxEqual([1.0, 2.1, 3.3, 4.3, 5.0, 5.5, 7.9, 8.8, 8.8, 9.8]));
4629     assert(x.quantile!"type8"(qtile.dup).all!approxEqual([0.520000, 1.913333, 3.000000, 4.160000, 5.000000, 5.533333, 7.940000, 8.800000, 9.033333, 9.920000]));
4630     assert(x.quantile!"type9"(qtile.dup).all!approxEqual([0.55000, 1.92500, 3.01875, 4.16875, 5.00000, 5.53125, 7.93750, 8.80000, 9.01875, 9.91250]));
4631 }
4632 
4633 /++
4634 Computes the interquartile range of the input.
4635 
4636 By default, this function computes the result using $(LREF quantile), i.e.
4637 `result = quantile(x, 0.75) - quantile(x, 0.25)`. There are also overloads for
4638 providing a low value, as in `result = quantile(x, 1 - low) - quantile(x, low)`
4639 and both a low and high value, as in `result = quantile(x, high) - quantile(x, low)`.
4640 
4641 For all $(LREF QuantileAlgo) except $(LREF QuantileAlgo.type1) and $(LREF QuantileAlgo.type3),
4642 by default, if `F` is not floating point type or complex type, then the result
4643 will have a `double` type if `F` is implicitly convertible to a floating point 
4644 type or a type for which `isComplex!F` is true.
4645 
4646 For $(LREF QuantileAlgo.type1) and $(LREF QuantileAlgo.type3), the return type is the
4647 $(MATHREF sum, elementType) of the input.
4648 
4649 Params:
4650     F = controls type of output
4651     quantileAlgo = algorithm for calculating quantile (default: $(LREF QuantileAlgo.type7))
4652     allowModifySlice = controls whether the input is modified in place, default is false
4653 
4654 Returns:
4655     The interquartile range of the input. 
4656 
4657 See_also:
4658     $(LREF quantile)
4659 +/
4660 template interquartileRange(F, QuantileAlgo quantileAlgo = QuantileAlgo.type7,
4661                             bool allowModifySlice = false)
4662 {
4663     import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind;
4664 
4665     /++
4666     Params:
4667         slice = slice
4668     +/
4669     @fmamath quantileType!(F, quantileAlgo) interquartileRange(
4670         Iterator, size_t N, SliceKind kind)(
4671             Slice!(Iterator, N, kind) slice)
4672     {
4673         import core.lifetime: move;
4674 
4675         alias FF = typeof(return);
4676         auto lo_hi = quantile!(FF, quantileAlgo, allowModifySlice, false)(slice.move, cast(FF) 0.25, cast(FF) 0.75);
4677         return lo_hi[1] - lo_hi[0];
4678     }
4679 
4680     /++
4681     Params:
4682         slice = slice
4683         lo = low value
4684     +/
4685     @fmamath quantileType!(F, quantileAlgo) interquartileRange(
4686         Iterator, size_t N, SliceKind kind)(
4687             Slice!(Iterator, N, kind) slice,
4688             F lo = 0.25)
4689     {
4690         import core.lifetime: move;
4691 
4692         alias FF = typeof(return);
4693         auto lo_hi = quantile!(FF, quantileAlgo, allowModifySlice, false)(slice.move, cast(FF) lo, cast(FF) (1 - lo));
4694         return lo_hi[1] - lo_hi[0];
4695     }
4696 
4697     /++
4698     Params:
4699         slice = slice
4700         lo = low value
4701         hi = high value
4702     +/
4703     @fmamath quantileType!(F, quantileAlgo) interquartileRange(
4704         Iterator, size_t N, SliceKind kind)(
4705             Slice!(Iterator, N, kind) slice,
4706             F lo,
4707             F hi)
4708     {
4709         import core.lifetime: move;
4710 
4711         alias FF = typeof(return);
4712         auto lo_hi = quantile!(FF, quantileAlgo, allowModifySlice, false)(slice.move, cast(FF) lo, cast(FF) hi);
4713         return lo_hi[1] - lo_hi[0];
4714     }
4715 
4716     /++
4717     Params:
4718         array = array
4719     +/
4720     @fmamath quantileType!(F[], quantileAlgo) interquartileRange(scope F[] array...)
4721     {
4722         import mir.ndslice.slice: sliced;
4723 
4724         alias FF = typeof(return);
4725         return .interquartileRange!(FF, quantileAlgo, allowModifySlice)(array.sliced);
4726     }
4727 
4728     /// ditto
4729     @fmamath auto interquartileRange(SliceLike)(SliceLike x)
4730         if (isConvertibleToSlice!SliceLike && !isSlice!SliceLike)
4731     {
4732         import mir.ndslice.slice: toSlice;
4733         return interquartileRange(x.toSlice);
4734     }
4735 }
4736 
4737 /// ditto
4738 template interquartileRange(QuantileAlgo quantileAlgo = QuantileAlgo.type7,
4739                             bool allowModifySlice = false)
4740 {
4741     import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind;
4742 
4743     /// ditto
4744     @fmamath quantileType!(Slice!(Iterator), quantileAlgo)
4745         interquartileRange(Iterator, size_t N, SliceKind kind)(
4746             Slice!(Iterator, N, kind) slice)
4747     {
4748         import core.lifetime: move;
4749 
4750         alias F = typeof(return);
4751         return .interquartileRange!(F, quantileAlgo, allowModifySlice)(slice.move);
4752     }
4753 
4754     /// ditto
4755     @fmamath quantileType!(Slice!(Iterator), quantileAlgo)
4756         interquartileRange(Iterator, size_t N, SliceKind kind, F)(
4757             Slice!(Iterator, N, kind) slice,
4758             F lo)
4759     {
4760         import core.lifetime: move;
4761 
4762         alias FF = typeof(return);
4763         return .interquartileRange!(FF, quantileAlgo, allowModifySlice)(slice.move, cast(FF) lo);
4764     }
4765 
4766     /// ditto
4767     @fmamath quantileType!(Slice!(Iterator), quantileAlgo)
4768         interquartileRange(Iterator, size_t N, SliceKind kind, F)(
4769             Slice!(Iterator, N, kind) slice,
4770             F lo,
4771             F hi)
4772     {
4773         import core.lifetime: move;
4774 
4775         alias FF = typeof(return);
4776         return .interquartileRange!(F, quantileAlgo, allowModifySlice)(slice.move, cast(FF) lo, cast(FF) hi);
4777     }
4778 
4779     /// ditto
4780     @fmamath quantileType!(T[], quantileAlgo)
4781         interquartileRange(T)(scope T[] array...)
4782     {
4783         import core.lifetime: move;
4784 
4785         alias F = typeof(return);
4786         return .interquartileRange!(F, quantileAlgo, allowModifySlice)(array);
4787     }
4788 
4789     /// ditto
4790     @fmamath auto interquartileRange(SliceLike)(SliceLike x)
4791         if (isConvertibleToSlice!SliceLike && !isSlice!SliceLike)
4792     {
4793         import mir.ndslice.slice: toSlice;
4794         alias F = quantileType!(typeof(x.toSlice), quantileAlgo);
4795         return .interquartileRange!(F, quantileAlgo, allowModifySlice)(x);
4796     }
4797 }
4798 
4799 /// ditto
4800 template interquartileRange(F, string quantileAlgo, bool allowModifySlice = false)
4801 {
4802     mixin("alias interquartileRange = .interquartileRange!(F, QuantileAlgo." ~ quantileAlgo ~ ", allowModifySlice);");
4803 }
4804 
4805 /// ditto
4806 template interquartileRange(string quantileAlgo, bool allowModifySlice = false)
4807 {
4808     mixin("alias interquartileRange = .interquartileRange!(QuantileAlgo." ~ quantileAlgo ~ ", allowModifySlice);");
4809 }
4810 
4811 /// Simple example
4812 version(mir_stat_test)
4813 @safe pure nothrow
4814 unittest 
4815 {
4816     import mir.math.common: approxEqual;
4817     import mir.ndslice.slice: sliced;
4818 
4819     auto x = [3.0, 1.0, 4.0, 2.0, 0.0].sliced;
4820 
4821     assert(x.interquartileRange.approxEqual(2.0));
4822     assert(x.interquartileRange(0.25).approxEqual(2.0));
4823     assert(x.interquartileRange(0.25, 0.75).approxEqual(2.0));
4824 }
4825 
4826 //no change in x by default
4827 version(mir_stat_test)
4828 @safe pure nothrow
4829 unittest 
4830 {
4831     import mir.algorithm.iteration: all;
4832     import mir.math.common: approxEqual;
4833     import mir.ndslice.slice: sliced;
4834 
4835     auto x = [3.0, 1.0, 4.0, 2.0, 0.0].sliced;
4836     auto x_copy = x.dup;
4837     auto result = x.interquartileRange;
4838 
4839     assert(x.all!approxEqual(x_copy));
4840 }
4841 
4842 /// Interquartile Range of vector
4843 version(mir_stat_test)
4844 @safe pure nothrow
4845 unittest 
4846 {
4847     import mir.math.common: approxEqual;
4848     import mir.ndslice.slice: sliced;
4849 
4850     auto x = [1.0, 9.8, 0.2, 8.5, 5.8, 3.5, 4.5, 8.2, 5.2, 5.2,
4851               2.5, 1.8, 2.2, 3.8, 5.2, 9.2, 6.2, 9.2, 9.2, 8.5].sliced;
4852 
4853     assert(x.interquartileRange.approxEqual(5.25));
4854 }
4855 
4856 /// Interquartile Range of matrix
4857 version(mir_stat_test)
4858 @safe pure
4859 unittest 
4860 {
4861     import mir.math.common: approxEqual;
4862     import mir.ndslice.fuse: fuse;
4863     import mir.ndslice.slice: sliced;
4864 
4865     auto x = [
4866         [1.0, 9.8, 0.2, 8.5, 5.8, 3.5, 4.5, 8.2, 5.2, 5.2],
4867         [2.5, 1.8, 2.2, 3.8, 5.2, 9.2, 6.2, 9.2, 9.2, 8.5]
4868     ].fuse;
4869 
4870     assert(x.interquartileRange.approxEqual(5.25));
4871 }
4872 
4873 /// Allow modification of input
4874 version(mir_stat_test)
4875 @safe pure nothrow
4876 unittest 
4877 {
4878     import mir.algorithm.iteration: all;
4879     import mir.math.common: approxEqual;
4880     import mir.ndslice.slice: sliced;
4881 
4882     auto x = [3.0, 1.0, 4.0, 2.0, 0.0].sliced;
4883     auto x_copy = x.dup;
4884 
4885     auto result = x.interquartileRange!(QuantileAlgo.type7, true);
4886     assert(!x.all!approxEqual(x_copy));
4887 }
4888 
4889 /// Can also set algorithm type
4890 version(mir_stat_test)
4891 @safe pure nothrow
4892 unittest
4893 {
4894     import mir.math.common: approxEqual;
4895     import mir.ndslice.slice: sliced;
4896 
4897     auto x = [1.0, 9.8, 0.2, 8.5, 5.8, 3.5, 4.5, 8.2, 5.2, 5.2,
4898               2.5, 1.8, 2.2, 3.8, 5.2, 9.2, 6.2, 9.2, 9.2, 8.5].sliced;
4899 
4900     assert(x.interquartileRange!"type1".approxEqual(6.0));
4901     assert(x.interquartileRange!"type2".approxEqual(5.5));
4902     assert(x.interquartileRange!"type3".approxEqual(6.0));
4903     assert(x.interquartileRange!"type4".approxEqual(6.0));
4904     assert(x.interquartileRange!"type5".approxEqual(5.5));
4905     assert(x.interquartileRange!"type6".approxEqual(5.75));
4906     assert(x.interquartileRange!"type7".approxEqual(5.25));
4907     assert(x.interquartileRange!"type8".approxEqual(5.583333));
4908     assert(x.interquartileRange!"type9".approxEqual(5.5625));
4909 }
4910 
4911 /// Can also set algorithm or output type
4912 version(mir_stat_test)
4913 @safe pure nothrow
4914 unittest
4915 {
4916     import mir.math.common: approxEqual;
4917     import mir.ndslice.slice: sliced;
4918 
4919     auto a = [1, 1e34, 1, -1e34, 0].sliced;
4920 
4921     auto x = a * 10_000;
4922 
4923     auto result0 = x.interquartileRange!float;
4924     assert(result0.approxEqual(10_000));
4925     static assert(is(typeof(result0) == float));
4926 
4927     auto result1 = x.interquartileRange!(float, "type8");
4928     assert(result1.approxEqual(6.666667e37));
4929     static assert(is(typeof(result1) == float));
4930 }
4931 
4932 /// Support for array
4933 version(mir_stat_test)
4934 @safe pure nothrow
4935 unittest 
4936 {
4937     import mir.math.common: approxEqual;
4938 
4939     double[] x = [3.0, 1.0, 4.0, 2.0, 0.0];
4940               
4941     assert(x.interquartileRange.approxEqual(2.0));
4942 }
4943 
4944 // withAsSlice test
4945 version(mir_stat_test)
4946 @safe pure nothrow @nogc
4947 unittest
4948 {
4949     import mir.algorithm.iteration: all;
4950     import mir.math.common: approxEqual;
4951     import mir.rc.array: RCArray;
4952 
4953     static immutable a = [1.0, 9.8, 0.2, 8.5, 5.8, 3.5, 4.5, 8.2, 5.2, 5.2,
4954                           2.5, 1.8, 2.2, 3.8, 5.2, 9.2, 6.2, 9.2, 9.2, 8.5];
4955 
4956     auto x = RCArray!double(20);
4957     foreach(i, ref e; x)
4958         e = a[i];
4959 
4960     assert(x.interquartileRange.approxEqual(5.25));
4961     assert(x.interquartileRange!double.approxEqual(5.25));
4962 }
4963 
4964 // Arbitrary test
4965 version(mir_stat_test)
4966 @safe pure nothrow
4967 unittest 
4968 {
4969     import mir.math.common: approxEqual;
4970 
4971     assert(interquartileRange(3.0, 1.0, 4.0, 2.0, 0.0).approxEqual(2.0));
4972 }
4973 
4974 // @nogc test
4975 version(mir_stat_test)
4976 @safe pure nothrow @nogc
4977 unittest 
4978 {
4979     import mir.math.common: approxEqual;
4980     import mir.ndslice.slice: sliced;
4981 
4982     static immutable x = [1.0, 9.8, 0.2, 8.5, 5.8, 3.5, 4.5, 8.2, 5.2, 5.2,
4983                           2.5, 1.8, 2.2, 3.8, 5.2, 9.2, 6.2, 9.2, 9.2, 8.5];
4984 
4985     assert(x.sliced.interquartileRange.approxEqual(5.25));
4986 }
4987 
4988 /++
4989 Calculates the median absolute deviation about the median of the input.
4990 
4991 By default, if `F` is not floating point type, then the result will have a
4992 `double` type if `F` is implicitly convertible to a floating point type.
4993 
4994 Params:
4995     F = output type
4996 
4997 Returns:
4998     The median absolute deviation of the input
4999 +/
5000 template medianAbsoluteDeviation(F)
5001 {
5002     /++
5003     Params:
5004         slice = slice
5005     +/
5006     @fmamath meanType!F medianAbsoluteDeviation(Iterator, size_t N, SliceKind kind)(
5007             Slice!(Iterator, N, kind) slice)
5008     {
5009         import core.lifetime: move;
5010         import mir.math.common: fabs;
5011         import mir.ndslice.topology: map;
5012         import mir.stat.transform: center;
5013 
5014         alias G = typeof(return);
5015         static assert(isFloatingPoint!G, "medianAbsoluteDeviation: output type must be floating point");
5016         return slice.move.center!(median!(G, false)).map!fabs.median!(G, false);
5017     }
5018 }
5019 
5020 /// ditto
5021 @fmamath meanType!(Slice!(Iterator, N, kind))
5022     medianAbsoluteDeviation(Iterator, size_t N, SliceKind kind)(
5023         Slice!(Iterator, N, kind) slice)
5024 {
5025     import core.lifetime: move;
5026 
5027     alias F = typeof(return);
5028     return medianAbsoluteDeviation!F(slice.move);
5029 }
5030 
5031 /// ditto
5032 @fmamath meanType!(T[]) medianAbsoluteDeviation(T)(scope const T[] ar...)
5033 {
5034     import mir.ndslice.slice: sliced;
5035 
5036     alias G = typeof(return);
5037     return medianAbsoluteDeviation!G(ar.sliced);
5038 }
5039 
5040 /// ditto
5041 @fmamath auto medianAbsoluteDeviation(SliceLike)(SliceLike x)
5042     if (isConvertibleToSlice!SliceLike && !isSlice!SliceLike)
5043 {
5044     import mir.ndslice.slice: toSlice;
5045     return medianAbsoluteDeviation(x.toSlice);
5046 }
5047 
5048 /// medianAbsoluteDeviation of vector
5049 version(mir_stat_test)
5050 @safe pure nothrow
5051 unittest
5052 {
5053     import mir.math.common: approxEqual;
5054     import mir.ndslice.slice: sliced;
5055 
5056     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
5057               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
5058 
5059     assert(x.medianAbsoluteDeviation.approxEqual(1.25));
5060 }
5061 
5062 // dynamic array test
5063 version(mir_stat_test)
5064 @safe pure nothrow
5065 unittest
5066 {
5067     import mir.math.common: approxEqual;
5068 
5069     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
5070               2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
5071 
5072     assert(x.medianAbsoluteDeviation.approxEqual(1.25));
5073 }
5074 
5075 /// Median Absolute Deviation of matrix
5076 version(mir_stat_test)
5077 @safe pure
5078 unittest
5079 {
5080     import mir.math.common: approxEqual;
5081     import mir.ndslice.fuse: fuse;
5082 
5083     auto x = [
5084         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
5085         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
5086     ].fuse;
5087 
5088     assert(x.medianAbsoluteDeviation.approxEqual(1.25));
5089 }
5090 
5091 /// Median Absolute Deviation of dynamic array
5092 version(mir_stat_test)
5093 @safe pure nothrow
5094 unittest
5095 {
5096     import mir.math.common: approxEqual;
5097 
5098     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
5099               2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
5100 
5101     assert(x.medianAbsoluteDeviation.approxEqual(1.25));
5102 }
5103 
5104 // @nogc test
5105 version(mir_stat_test)
5106 @safe pure nothrow @nogc
5107 unittest
5108 {
5109     import mir.math.common: approxEqual;
5110     import mir.ndslice.slice: sliced;
5111 
5112     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
5113                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
5114 
5115     assert(x.sliced.medianAbsoluteDeviation.approxEqual(1.25));
5116 }
5117 
5118 // withAsSlice test
5119 version(mir_stat_test)
5120 @safe pure nothrow @nogc
5121 unittest
5122 {
5123     import mir.math.common: approxEqual;
5124     import mir.rc.array: RCArray;
5125 
5126     static immutable a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
5127                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
5128 
5129     auto x = RCArray!double(12);
5130     foreach(i, ref e; x)
5131         e = a[i];
5132 
5133     assert(a.medianAbsoluteDeviation.approxEqual(1.25));
5134 }
5135 
5136 /++
5137 Calculates the dispersion of the input.
5138 
5139 For an input `x`, this function first centers `x` by subtracting each `e` in `x`
5140 by the result of `centralTendency`, then it transforms the centered values using
5141 the function `transform`, and then finally summarizes that information using
5142 the `summarize` funcion. 
5143 
5144 The default functions provided are equivalent to calculating the population
5145 variance. The `centralTendency` default is the `mean` function, which results
5146 in the input being centered about the mean. The default `transform` function
5147 will square the centered values. The default `summarize` function is `mean`,
5148 which will return the mean of the squared centered values.
5149 
5150 Params:
5151     centralTendency = function that will produce the value that the input is centered about, default is `mean`
5152     transform = function to transform centered values, default squares the centered values
5153     summarize = function to summarize the transformed centered values, default is `mean`
5154 
5155 Returns:
5156     The dispersion of the input
5157 +/
5158 template dispersion(
5159     alias centralTendency = mean,
5160     alias transform = "a * a",
5161     alias summarize = mean)
5162 {
5163     import mir.functional: naryFun;
5164     import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind, sliced;
5165 
5166     static if (__traits(isSame, naryFun!transform, transform))
5167     {
5168         /++
5169         Params:
5170             slice = slice
5171         +/
5172         @fmamath auto dispersion(Iterator, size_t N, SliceKind kind)(
5173             Slice!(Iterator, N, kind) slice)
5174         {
5175             import mir.ndslice.topology: map;
5176             import mir.stat.transform: center;
5177 
5178             return summarize(slice.center!centralTendency.map!transform);
5179         }
5180 
5181         /// ditto
5182         @fmamath auto dispersion(T)(scope const T[] ar...)
5183         {
5184             return dispersion(ar.sliced);
5185         }
5186 
5187         /// ditto
5188         @fmamath auto dispersion(SliceLike)(SliceLike x)
5189             if (isConvertibleToSlice!SliceLike && !isSlice!SliceLike)
5190         {
5191             import mir.ndslice.slice: toSlice;
5192             return dispersion(x.toSlice);
5193         }
5194     }
5195     else
5196         alias dispersion = .dispersion!(centralTendency, naryFun!transform, summarize);
5197 }
5198 
5199 /// Simple examples
5200 version(mir_stat_test)
5201 @safe pure nothrow
5202 unittest
5203 {
5204     import mir.complex: Complex;
5205     import mir.complex.math: capproxEqual = approxEqual;
5206     import mir.functional: naryFun;
5207     import mir.math.common: approxEqual;
5208     import mir.ndslice.slice: sliced;
5209 
5210     alias C = Complex!double;
5211 
5212     assert(dispersion([1.0, 2, 3]).approxEqual(2.0 / 3));
5213 
5214     assert(dispersion([C(1.0, 3), C(2), C(3)]).capproxEqual(C(-4, -6) / 3));
5215 
5216     assert(dispersion!(mean!float, "a * a", mean!float)([0, 1, 2, 3, 4, 5].sliced(3, 2)).approxEqual(17.5 / 6));
5217 
5218     static assert(is(typeof(dispersion!(mean!float, "a ^^ 2", mean!float)([1, 2, 3])) == float));
5219 }
5220 
5221 /// Dispersion of vector
5222 version(mir_stat_test)
5223 @safe pure nothrow
5224 unittest
5225 {
5226     import mir.math.common: approxEqual;
5227 
5228     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
5229               2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
5230 
5231     assert(x.dispersion.approxEqual(54.76562 / 12));
5232 }
5233 
5234 /// Dispersion of matrix
5235 version(mir_stat_test)
5236 @safe pure
5237 unittest
5238 {
5239     import mir.math.common: approxEqual;
5240     import mir.ndslice.fuse: fuse;
5241 
5242     auto x = [
5243         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
5244         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
5245     ].fuse;
5246 
5247     assert(x.dispersion.approxEqual(54.76562 / 12));
5248 }
5249 
5250 /// Column dispersion of matrix
5251 version(mir_stat_test)
5252 @safe pure
5253 unittest
5254 {
5255     import mir.algorithm.iteration: all;
5256     import mir.math.common: approxEqual;
5257     import mir.ndslice.fuse: fuse;
5258     import mir.ndslice.topology: alongDim, byDim, map;
5259 
5260     auto x = [
5261         [0.0,  1.0, 1.5, 2.0], 
5262         [3.5, 4.25, 2.0, 7.5],
5263         [5.0,  1.0, 1.5, 0.0]
5264     ].fuse;
5265     auto result = [13.16667 / 3, 7.041667 / 3, 0.1666667 / 3, 30.16667 / 3];
5266 
5267     // Use byDim or alongDim with map to compute dispersion of row/column.
5268     assert(x.byDim!1.map!dispersion.all!approxEqual(result));
5269     assert(x.alongDim!0.map!dispersion.all!approxEqual(result));
5270 
5271     // FIXME
5272     // Without using map, computes the dispersion of the whole slice
5273     // assert(x.byDim!1.dispersion == x.sliced.dispersion);
5274     // assert(x.alongDim!0.dispersion == x.sliced.dispersion);
5275 }
5276 
5277 /// Can also set functions to change type of dispersion that is used
5278 version(mir_stat_test)
5279 @safe
5280 unittest
5281 {
5282     import mir.functional: naryFun;
5283     import mir.math.common: approxEqual, fabs, sqrt;
5284 
5285     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
5286               2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
5287               
5288     alias square = naryFun!"a * a";
5289 
5290     // Other population variance examples
5291     assert(x.dispersion.approxEqual(54.76562 / 12));
5292     assert(x.dispersion!mean.approxEqual(54.76562 / 12));
5293     assert(x.dispersion!(mean, square).approxEqual(54.76562 / 12));
5294     assert(x.dispersion!(mean, square, mean).approxEqual(54.76562 / 12));
5295 
5296     // Population standard deviation
5297     assert(x.dispersion!(mean, square, mean).sqrt.approxEqual(sqrt(54.76562 / 12)));
5298 
5299     // Mean absolute deviation about the mean
5300     assert(x.dispersion!(mean, fabs, mean).approxEqual(21.0 / 12));
5301     //Mean absolute deviation about the median
5302     assert(x.dispersion!(median, fabs, mean).approxEqual(19.25000 / 12));
5303     //Median absolute deviation about the mean
5304     assert(x.dispersion!(mean, fabs, median).approxEqual(1.43750));
5305     //Median absolute deviation about the median
5306     assert(x.dispersion!(median, fabs, median).approxEqual(1.25000));
5307 }
5308 
5309 /++
5310 For integral slices, pass output type to `centralTendency`, `transform`, and 
5311 `summary` functions as template parameter to ensure output type is correct.
5312 +/
5313 version(mir_stat_test)
5314 @safe pure nothrow
5315 unittest
5316 {
5317     import mir.functional: naryFun;
5318     import mir.math.common: approxEqual;
5319     import mir.ndslice.slice: sliced;
5320 
5321     auto x = [0, 1, 1, 2, 4, 4,
5322               2, 7, 5, 1, 2, 0].sliced;
5323 
5324     alias square = naryFun!"a * a";
5325 
5326     auto y = x.dispersion;
5327     assert(y.approxEqual(50.91667 / 12));
5328     static assert(is(typeof(y) == double));
5329 
5330     assert(x.dispersion!(mean!float, square, mean!float).approxEqual(50.91667 / 12));
5331 }
5332 
5333 // mir.complex test
5334 version(mir_stat_test)
5335 @safe pure nothrow
5336 unittest
5337 {
5338     import mir.complex: Complex;
5339     import mir.complex.math: approxEqual;
5340     import mir.ndslice.slice: sliced;
5341 
5342     alias C = Complex!double;
5343 
5344     auto x = [C(1.0, 2), C(2.0, 3), C(3.0, 4), C(4.0, 5)].sliced;
5345     assert(x.dispersion.approxEqual(C(0.0, 10.0) / 4));
5346 }
5347 
5348 // std.complex test
5349 version(mir_stat_test)
5350 @safe pure nothrow
5351 unittest
5352 {
5353     import mir.ndslice.slice: sliced;
5354     import std.complex: complex;
5355     import std.math.operations: isClose;
5356 
5357     auto x = [complex(1.0, 2), complex(2, 3), complex(3, 4), complex(4, 5)].sliced;
5358     assert(x.dispersion.isClose(complex(0.0, 10.0) / 4));
5359 }
5360 
5361 /++
5362 Dispersion works for complex numbers and other user-defined types (provided that
5363 the `centralTendency`, `transform`, and `summary` functions are defined for those
5364 types)
5365 +/
5366 version(mir_stat_test)
5367 @safe pure nothrow
5368 unittest
5369 {
5370     import mir.ndslice.slice: sliced;
5371     import std.complex: Complex;
5372     import std.math.operations: isClose;
5373 
5374     auto x = [Complex!double(1, 2), Complex!double(2, 3), Complex!double(3, 4), Complex!double(4, 5)].sliced;
5375     assert(x.dispersion.isClose(Complex!double(0, 10) / 4));
5376 }
5377 
5378 /// Compute mean tensors along specified dimention of tensors
5379 version(mir_stat_test)
5380 @safe pure
5381 unittest
5382 {
5383     import mir.algorithm.iteration: all;
5384     import mir.math.common: approxEqual;
5385     import mir.ndslice.fuse: fuse;
5386     import mir.ndslice.topology: as, iota, alongDim, map, repeat;
5387 
5388     auto x = [
5389         [0.0, 1, 2],
5390         [3.0, 4, 5]
5391     ].fuse;
5392 
5393     assert(x.dispersion.approxEqual(17.5 / 6));
5394 
5395     auto m0 = [2.25, 2.25, 2.25];
5396     assert(x.alongDim!0.map!dispersion.all!approxEqual(m0));
5397     assert(x.alongDim!(-2).map!dispersion.all!approxEqual(m0));
5398 
5399     auto m1 = [2.0 / 3, 2.0 / 3];
5400     assert(x.alongDim!1.map!dispersion.all!approxEqual(m1));
5401     assert(x.alongDim!(-1).map!dispersion.all!approxEqual(m1));
5402 
5403     assert(iota(2, 3, 4, 5).as!double.alongDim!0.map!dispersion.all!approxEqual(repeat(1800.0 / 2, 3, 4, 5)));
5404 }
5405 
5406 /// Arbitrary dispersion
5407 version(mir_stat_test)
5408 @safe pure nothrow @nogc
5409 unittest
5410 {
5411     import mir.functional: naryFun;
5412     import mir.math.common: approxEqual;
5413 
5414     alias square = naryFun!"a * a";
5415 
5416     assert(dispersion(1.0, 2, 3).approxEqual(2.0 / 3));
5417     assert(dispersion!(mean!float, square, mean!float)(1, 2, 3).approxEqual(2f / 3));
5418 }
5419 
5420 // UFCS UT
5421 version(mir_stat_test)
5422 @safe pure nothrow
5423 unittest
5424 {
5425     import mir.math.common: approxEqual;
5426     assert([1.0, 2, 3, 4].dispersion.approxEqual(5.0 / 4));
5427 }
5428 
5429 // Confirm type output is correct
5430 version(mir_stat_test)
5431 @safe pure nothrow
5432 unittest
5433 {
5434     import mir.algorithm.iteration: all;
5435     import mir.math.common: approxEqual;
5436     import mir.ndslice.topology: iota, alongDim, map;
5437 
5438     auto x = iota([2, 2], 1);
5439     auto y = x.alongDim!1.map!dispersion;
5440     assert(y.all!approxEqual([0.25, 0.25]));
5441     static assert(is(meanType!(typeof(y)) == double));
5442 }
5443 
5444 // @nogc UT
5445 version(mir_stat_test)
5446 @safe pure @nogc nothrow
5447 unittest
5448 {
5449     import mir.math.common: approxEqual;
5450     import mir.ndslice.slice: sliced;
5451 
5452     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
5453                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
5454 
5455     assert(x.sliced.dispersion.approxEqual(54.76562 / 12));
5456 }
5457 
5458 // withAsSlice test
5459 version(mir_stat_test)
5460 @safe pure nothrow @nogc
5461 unittest
5462 {
5463     import mir.math.common: approxEqual;
5464     import mir.rc.array: RCArray;
5465 
5466     static immutable a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
5467                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
5468 
5469     auto x = RCArray!double(12);
5470     foreach(i, ref e; x)
5471         e = a[i];
5472 
5473     assert(x.dispersion.approxEqual(54.76562 / 12));
5474 }
5475 
5476 /++
5477 Skewness algorithms.
5478 
5479 See_also:
5480     $(WEB en.wikipedia.org/wiki/Skewness, Skewness),
5481     $(WEB en.wikipedia.org/wiki/Algorithms_for_calculating_variance, Algorithms for calculating variance)
5482 +/
5483 enum SkewnessAlgo
5484 {
5485     /++
5486     Similar to Welford's algorithm for updating variance, but adjusted for skewness.
5487     Can also `put` another SkewnessAccumulator of the same type, which
5488     uses the parallel algorithm from Terriberry that extends the work of Chan et
5489     al. 
5490     +/
5491     online,
5492 
5493     /++
5494     Calculates skewness using
5495     (E(x^^3) - 3 * mu * sigma ^^ 2 + mu ^^ 3) / (sigma ^^ 3)
5496 
5497     This algorithm can be numerically unstable.
5498     +/
5499     naive,
5500 
5501     /++
5502     Calculates skewness by first calculating the mean, then calculating
5503     E((x - E(x)) ^^ 3) / (E((x - E(x)) ^^ 2) ^^ 1.5)
5504     +/
5505     twoPass,
5506 
5507     /++
5508     Calculates skewness by first calculating the mean, then the standard deviation, then calculating
5509     E(((x - E(x)) / (E((x - E(x)) ^^ 2) ^^ 0.5)) ^^ 3)
5510     +/
5511     threePass,
5512 
5513     /++
5514     Calculates skewness assuming the mean of the input is zero. 
5515     +/
5516     assumeZeroMean,
5517 
5518     /++
5519     When slices, slice-like objects, or ranges are the inputs, uses the two-pass
5520     algorithm. When an individual data-point is added, uses the online algorithm.
5521     +/
5522     hybrid
5523 }
5524 
5525 ///
5526 struct SkewnessAccumulator(T, SkewnessAlgo skewnessAlgo, Summation summation)
5527     if (isMutable!T && skewnessAlgo == SkewnessAlgo.naive)
5528 {
5529     import mir.functional: naryFun;
5530     import mir.math.sum: Summator;
5531     import std.traits: isIterable;
5532 
5533     ///
5534     private MeanAccumulator!(T, summation) meanAccumulator;
5535     ///
5536     alias S = Summator!(T, summation);
5537     ///
5538     private S summatorOfSquares;
5539     ///
5540     private S summatorOfCubes;
5541 
5542     ///
5543     this(Range)(Range r)
5544         if (isIterable!Range)
5545     {
5546         import core.lifetime: move;
5547         this.put(r.move);
5548     }
5549 
5550     ///
5551     this()(T x)
5552     {
5553         this.put(x);
5554     }
5555 
5556     ///
5557     void put(Range)(Range r)
5558         if (isIterable!Range)
5559     {
5560         foreach(x; r)
5561         {
5562             this.put(x);
5563         }
5564     }
5565 
5566     ///
5567     void put()(T x)
5568     {
5569         meanAccumulator.put(x);
5570         T x2 = x * x;
5571         summatorOfSquares.put(x2);
5572         summatorOfCubes.put(x2 * x);
5573     }
5574 
5575     void put(U, Summation sumAlgo)(SkewnessAccumulator!(U, skewnessAlgo, sumAlgo) v)
5576     {
5577         meanAccumulator.put(v.meanAccumulator);
5578         summatorOfSquares.put(v.sumOfSquares!T);
5579         summatorOfCubes.put(v.sumOfCubes!T);
5580     }
5581 
5582 const:
5583 
5584     ///
5585     size_t count() @property
5586     {
5587         return meanAccumulator.count;
5588     }
5589     ///
5590     F mean(F = T)() @property
5591     {
5592         return meanAccumulator.mean!F;
5593     }
5594     ///
5595     F variance(F = T)(bool isPopulation) @property
5596     in
5597     {
5598         assert(count > 1, "SkewnessAccumulator.varaince: count must be larger than one");
5599     }
5600     do
5601     {
5602         return sumOfSquares!F / (count + isPopulation - 1) - 
5603             mean!F * mean!F * count / (count + isPopulation - 1);
5604     }
5605     ///
5606     F sumOfCubes(F = T)()
5607     {
5608         return cast(F) summatorOfCubes.sum;
5609     }
5610     ///
5611     F sumOfSquares(F = T)()
5612     {
5613         return cast(F) summatorOfSquares.sum;
5614     }
5615     ///
5616     F centeredSumOfSquares(F = T)()
5617     {
5618         return sumOfSquares!F - count * mean!F * mean!F;
5619     }
5620     ///
5621     F centeredSumOfCubes(F = T)()
5622     {
5623         F mu = mean!F;
5624         return sumOfCubes!F - 3 * mu * sumOfSquares!F + 2 * count * mu * mu * mu;
5625     }
5626     ///
5627     F scaledSumOfCubes(F = T)(bool isPopulation)
5628     {
5629         import mir.math.common: sqrt;
5630         F var = variance!F(isPopulation);
5631         return centeredSumOfCubes!F / (var * var.sqrt);
5632     }
5633     ///
5634     F skewness(F = T)(bool isPopulation)
5635     in
5636     {
5637         assert(count > 2, "SkewnessAccumulator.skewness: count must be larger than two");
5638         assert(variance(true) > 0, "SkewnessAccumulator.skewness: variance must be larger than zero");
5639     }
5640     do
5641     {
5642         import mir.math.common: sqrt;
5643 
5644         return scaledSumOfCubes!F(isPopulation) * count /
5645             ((count + isPopulation - 1) * (count + 2 * isPopulation - 2));
5646         /+ equivalent to
5647         F mu = mean!F;
5648         F avg_centeredSumOfCubes = sumOfCubes!F / count - 3 * mu * variance!F(true) - (mu * mu * mu);
5649         F var = variance!F(isPopulation);
5650         return avg_centeredSumOfCubes / (var * var.sqrt) *
5651                 (cast(F) count * count / ((count + isPopulation - 1) * (count + 2 * isPopulation - 2)));
5652         +/
5653     }
5654 }
5655 
5656 /// naive
5657 version(mir_stat_test)
5658 @safe pure nothrow
5659 unittest
5660 {
5661     import mir.math.common: pow;
5662     import mir.math.sum: Summation;
5663     import mir.ndslice.slice: sliced;
5664     import mir.test: shouldApprox;
5665 
5666     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
5667               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
5668 
5669     SkewnessAccumulator!(double, SkewnessAlgo.naive, Summation.naive) v;
5670     v.put(x);
5671     v.skewness(true).shouldApprox == (117.005859 / 12) / pow(54.765625 / 12, 1.5);
5672     v.skewness(false).shouldApprox == (117.005859 / 12) / pow(54.765625 / 11, 1.5) * (12.0 ^^ 2) / (11.0 * 10.0);
5673 
5674     v.put(4.0);
5675     v.skewness(true).shouldApprox == (100.238166 / 13) / pow(57.019231 / 13, 1.5);
5676     v.skewness(false).shouldApprox == (100.238166 / 13) / pow(57.019231 / 12, 1.5) * (13.0 ^^ 2) / (12.0 * 11.0);
5677 }
5678 
5679 // check two-dimensional
5680 version(mir_stat_test)
5681 @safe pure
5682 unittest
5683 {
5684     import mir.math.common: pow;
5685     import mir.math.sum: Summation;
5686     import mir.ndslice.fuse: fuse;
5687     import mir.test: shouldApprox;
5688 
5689     auto x = [[0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
5690               [2.0, 7.5, 5.0, 1.0, 1.5, 0.00]].fuse;
5691 
5692     SkewnessAccumulator!(double, SkewnessAlgo.naive, Summation.naive) v;
5693     v.put(x);
5694     v.skewness(true).shouldApprox == (117.005859 / 12) / pow(54.765625 / 12, 1.5);
5695     v.skewness(false).shouldApprox == (117.005859 / 12) / pow(54.765625 / 11, 1.5) * (12.0 ^^ 2) / (11.0 * 10.0);
5696 }
5697 
5698 // Can put SkewnessAccumulator
5699 version(mir_stat_test)
5700 @safe pure nothrow
5701 unittest
5702 {
5703     import mir.math.common: pow;
5704     import mir.ndslice.slice: sliced;
5705     import mir.test: shouldApprox;
5706 
5707     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
5708     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
5709 
5710     SkewnessAccumulator!(double, SkewnessAlgo.naive, Summation.naive) v;
5711     v.put(x);
5712     SkewnessAccumulator!(double, SkewnessAlgo.naive, Summation.naive) w;
5713     w.put(y);
5714     v.put(w);
5715     v.skewness(true).shouldApprox == (117.005859 / 12) / pow(54.765625 / 12, 1.5);
5716 }
5717 
5718 // Test input range
5719 version(mir_stat_test)
5720 @safe pure nothrow
5721 unittest
5722 {
5723     import mir.math.sum: Summation;
5724     import mir.test: should;
5725     import std.range: iota;
5726     import std.algorithm: map;
5727 
5728     auto x1 = iota(0, 5);
5729     auto v1 = SkewnessAccumulator!(double, SkewnessAlgo.naive, Summation.naive)(x1);
5730     v1.skewness(true).should == 0;
5731     auto x2 = x1.map!(a => 2 * a);
5732     auto v2 = SkewnessAccumulator!(double, SkewnessAlgo.naive, Summation.naive)(x2);
5733     v2.skewness(true).should == 0;
5734 }
5735 
5736 ///
5737 struct SkewnessAccumulator(T, SkewnessAlgo skewnessAlgo, Summation summation)
5738     if (isMutable!T && skewnessAlgo == SkewnessAlgo.online)
5739 {
5740     import mir.math.sum: Summator;
5741     import std.traits: isIterable;
5742 
5743     ///
5744     private MeanAccumulator!(T, summation) meanAccumulator;
5745     ///
5746     alias S = Summator!(T, summation);
5747     ///
5748     private S centeredSummatorOfSquares;
5749     ///
5750     private S centeredSummatorOfCubes;
5751 
5752     ///
5753     this(Range)(Range r)
5754         if (isIterable!Range)
5755     {
5756         import core.lifetime: move;
5757         this.put(r.move);
5758     }
5759 
5760     ///
5761     this()(T x)
5762     {
5763         this.put(x);
5764     }
5765 
5766     ///
5767     void put(Range)(Range r)
5768         if (isIterable!Range)
5769     {
5770         foreach(x; r)
5771         {
5772             this.put(x);
5773         }
5774     }
5775 
5776     ///
5777     void put()(T x)
5778     {
5779         T deltaOld = x;
5780         if (count > 0) {
5781             deltaOld -= mean;
5782         }
5783         meanAccumulator.put(x);
5784         T deltaNew = x - mean;
5785         centeredSummatorOfCubes.put(deltaOld * deltaOld * deltaOld * (count - 1) * (count - 2) / (count * count) -
5786                                     3 * deltaOld * centeredSumOfSquares / count);
5787         centeredSummatorOfSquares.put(deltaOld * deltaNew);
5788     }
5789 
5790     ///
5791     void put(U, SkewnessAlgo skewAlgo, Summation sumAlgo)(SkewnessAccumulator!(U, skewAlgo, sumAlgo) v)
5792         if (skewAlgo != SkewnessAlgo.assumeZeroMean)
5793     {
5794         size_t oldCount = count;
5795         T delta = v.mean;
5796         if (oldCount > 0) {
5797             delta -= mean;
5798         }
5799         meanAccumulator.put!T(v.meanAccumulator);
5800         centeredSummatorOfCubes.put(v.centeredSumOfCubes!T + 
5801                                     delta * delta * delta * v.count * oldCount * (oldCount - v.count) / (count * count) +
5802                                     3 * delta * (oldCount * v.centeredSumOfSquares!T - v.count * centeredSumOfSquares!T) / count);
5803         centeredSummatorOfSquares.put(v.centeredSumOfSquares!T + delta * delta * v.count * oldCount / count);
5804     }
5805 
5806 const:
5807 
5808     ///
5809     size_t count() @property
5810     {
5811         return meanAccumulator.count;
5812     }
5813     ///
5814     F mean(F = T)() @property
5815     {
5816         return meanAccumulator.mean!F;
5817     }
5818     ///
5819     F variance(F = T)(bool isPopulation) @property
5820     in
5821     {
5822         assert(count > 1, "SkewnessAccumulator.variance: count must be larger than one");
5823     }
5824     do
5825     {
5826         return centeredSumOfSquares!F / (count + isPopulation - 1);
5827     }
5828     ///
5829     F centeredSumOfSquares(F = T)()
5830     {
5831         return cast(F) centeredSummatorOfSquares.sum;
5832     }
5833     ///
5834     F centeredSumOfCubes(F = T)()
5835     {
5836         return cast(F) centeredSummatorOfCubes.sum;
5837     }
5838     ///
5839     F scaledSumOfCubes(F = T)(bool isPopulation)
5840     {
5841         import mir.math.common: sqrt;
5842         F var = variance!F(isPopulation);
5843         return centeredSumOfCubes!F / (var * var.sqrt);
5844     }
5845     ///
5846     F skewness(F = T)(bool isPopulation)
5847     in
5848     {
5849         assert(count > 2, "SkewnessAccumulator.skewness: count must be larger than two");
5850         assert(centeredSummatorOfSquares.sum > 0, "SkewnessAccumulator.skewness: variance must be larger than zero");
5851     }
5852     do
5853     {
5854         import mir.math.common: sqrt;
5855         F s = centeredSumOfSquares!F;
5856         return centeredSumOfCubes!F / (s * s.sqrt) * count * sqrt(cast(F) count + isPopulation - 1) /
5857             (count + 2 * isPopulation - 2);
5858         /+ Equivalent to
5859         return scaledSumOfCubes!F(isPopulation) / count *
5860                 (cast(F) count * count / ((count + isPopulation - 1) * (count + 2 * isPopulation - 2)));
5861         +/
5862     }
5863 }
5864 
5865 /// online
5866 version(mir_stat_test)
5867 @safe pure nothrow
5868 unittest
5869 {
5870     import mir.math.common: approxEqual, pow;
5871     import mir.math.sum: Summation;
5872     import mir.ndslice.slice: sliced;
5873 
5874     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
5875               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
5876 
5877     SkewnessAccumulator!(double, SkewnessAlgo.online, Summation.naive) v;
5878     v.put(x);
5879     assert(v.skewness(true).approxEqual((117.005859 / 12) / pow(54.765625 / 12, 1.5)));
5880     assert(v.skewness(false).approxEqual((117.005859 / 12) / pow(54.765625 / 11, 1.5) * (12.0 ^^ 2) / (11.0 * 10.0)));
5881 
5882     v.put(4.0);
5883     assert(v.skewness(true).approxEqual((100.238166 / 13) / pow(57.019231 / 13, 1.5)));
5884     assert(v.skewness(false).approxEqual((100.238166 / 13) / pow(57.019231 / 12, 1.5) * (13.0 ^^ 2) / (12.0 * 11.0)));
5885 }
5886 
5887 // Can put slice
5888 version(mir_stat_test)
5889 @safe pure nothrow
5890 unittest
5891 {
5892     import mir.math.common: approxEqual, pow;
5893     import mir.math.sum: Summation;
5894     import mir.ndslice.slice: sliced;
5895 
5896     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
5897     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
5898 
5899     SkewnessAccumulator!(double, SkewnessAlgo.online, Summation.naive) v;
5900     v.put(x);
5901     assert(v.centeredSumOfCubes.approxEqual(4.071181));
5902     assert(v.centeredSumOfSquares.approxEqual(12.552083));
5903 
5904     v.put(y);
5905     assert(v.centeredSumOfCubes.approxEqual(117.005859));
5906     assert(v.centeredSumOfSquares.approxEqual(54.765625));
5907 }
5908 
5909 // Can put SkewnessAccumulator
5910 version(mir_stat_test)
5911 @safe pure nothrow
5912 unittest
5913 {
5914     import mir.math.common: approxEqual, pow;
5915     import mir.ndslice.slice: sliced;
5916 
5917     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
5918     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
5919 
5920     SkewnessAccumulator!(double, SkewnessAlgo.online, Summation.naive) v;
5921     v.put(x);
5922     assert(v.centeredSumOfCubes.approxEqual(4.071181));
5923     assert(v.centeredSumOfSquares.approxEqual(12.552083));
5924 
5925     SkewnessAccumulator!(double, SkewnessAlgo.online, Summation.naive) w;
5926     w.put(y);
5927     v.put(w);
5928     assert(v.centeredSumOfCubes.approxEqual(117.005859));
5929     assert(v.centeredSumOfSquares.approxEqual(54.765625));
5930 }
5931 
5932 // Can put SkewnessAccumulator (naive)
5933 version(mir_stat_test)
5934 @safe pure nothrow
5935 unittest
5936 {
5937     import mir.math.common: approxEqual, pow;
5938     import mir.ndslice.slice: sliced;
5939 
5940     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
5941     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
5942 
5943     SkewnessAccumulator!(double, SkewnessAlgo.online, Summation.naive) v;
5944     v.put(x);
5945     assert(v.centeredSumOfCubes.approxEqual(4.071181));
5946     assert(v.centeredSumOfSquares.approxEqual(12.552083));
5947 
5948     SkewnessAccumulator!(double, SkewnessAlgo.naive, Summation.naive) w;
5949     w.put(y);
5950     v.put(w);
5951     assert(v.centeredSumOfCubes.approxEqual(117.005859));
5952     assert(v.centeredSumOfSquares.approxEqual(54.765625));
5953 }
5954 
5955 // Can put SkewnessAccumulator (twoPass)
5956 version(mir_stat_test)
5957 @safe pure nothrow
5958 unittest
5959 {
5960     import mir.math.common: approxEqual, pow;
5961     import mir.ndslice.slice: sliced;
5962 
5963     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
5964     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
5965 
5966     SkewnessAccumulator!(double, SkewnessAlgo.online, Summation.naive) v;
5967     v.put(x);
5968     assert(v.centeredSumOfCubes.approxEqual(4.071181));
5969     assert(v.centeredSumOfSquares.approxEqual(12.552083));
5970 
5971     auto w = SkewnessAccumulator!(double, SkewnessAlgo.twoPass, Summation.naive)(y);
5972     v.put(w);
5973     assert(v.centeredSumOfCubes.approxEqual(117.005859));
5974     assert(v.centeredSumOfSquares.approxEqual(54.765625));
5975 }
5976 
5977 // Can put SkewnessAccumulator (threePass)
5978 version(mir_stat_test)
5979 @safe pure nothrow
5980 unittest
5981 {
5982     import mir.math.common: approxEqual, pow;
5983     import mir.ndslice.slice: sliced;
5984 
5985     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
5986     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
5987 
5988     SkewnessAccumulator!(double, SkewnessAlgo.online, Summation.naive) v;
5989     v.put(x);
5990     assert(v.centeredSumOfCubes.approxEqual(4.071181));
5991     assert(v.centeredSumOfSquares.approxEqual(12.552083));
5992 
5993     auto w = SkewnessAccumulator!(double, SkewnessAlgo.threePass, Summation.naive)(y);
5994     v.put(w);
5995     assert(v.centeredSumOfCubes.approxEqual(117.005859));
5996     assert(v.centeredSumOfSquares.approxEqual(54.765625));
5997 }
5998 
5999 // check variance/scaledSumOfCubes
6000 version(mir_stat_test)
6001 @safe pure nothrow
6002 unittest
6003 {
6004     import mir.math.common: sqrt;
6005     import mir.math.sum: Summation;
6006     import mir.ndslice.slice: sliced;
6007     import mir.test: shouldApprox;
6008 
6009     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6010               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6011 
6012     SkewnessAccumulator!(double, SkewnessAlgo.online, Summation.naive) v;
6013     v.put(x);
6014     auto varP = x.variance!"online"(true);
6015     auto varS = x.variance!"online"(false);
6016     v.variance(true).shouldApprox == varP;
6017     v.variance(false).shouldApprox == varS;
6018     v.scaledSumOfCubes(true).shouldApprox == v.centeredSumOfCubes / (varP * varP.sqrt);
6019     v.scaledSumOfCubes(false).shouldApprox == v.centeredSumOfCubes / (varS * varS.sqrt);
6020 }
6021 
6022 ///
6023 struct SkewnessAccumulator(T, SkewnessAlgo skewnessAlgo, Summation summation)
6024     if (isMutable!T && skewnessAlgo == SkewnessAlgo.twoPass)
6025 {
6026     import mir.math.sum: elementType, Summator;
6027     import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind;
6028     import std.range: isInputRange;
6029 
6030     ///
6031     private MeanAccumulator!(T, summation) meanAccumulator;
6032     ///
6033     alias S = Summator!(T, summation);
6034     ///
6035     private S centeredSummatorOfSquares;
6036     ///
6037     private S centeredSummatorOfCubes;
6038 
6039     ///
6040     this(Iterator, size_t N, SliceKind kind)(Slice!(Iterator, N, kind) slice)
6041     {
6042         import mir.functional: naryFun;
6043         import mir.ndslice.topology: vmap, map;
6044         import mir.ndslice.internal: LeftOp;
6045 
6046         meanAccumulator.put(slice.lightScope);
6047 
6048         auto sliceMap = slice.vmap(LeftOp!("-", T)(mean)).map!(naryFun!"a * a", naryFun!"a * a * a");
6049         centeredSummatorOfSquares.put(sliceMap.map!"a[0]");
6050         centeredSummatorOfCubes.put(sliceMap.map!"a[1]");
6051     }
6052 
6053     ///
6054     this(SliceLike)(SliceLike x)
6055         if (isConvertibleToSlice!SliceLike && !isSlice!SliceLike)
6056     {
6057         import mir.ndslice.slice: toSlice;
6058         this(x.toSlice);
6059     }
6060 
6061     ///
6062     this(Range)(Range range)
6063         if (isInputRange!Range && !isConvertibleToSlice!Range && is(elementType!Range : T))
6064     {
6065         import std.algorithm: map;
6066         meanAccumulator.put(range);
6067 
6068         auto centeredRangeMultiplier = range.map!(a => (a - mean)).map!("a * a", "a * a * a");
6069         centeredSummatorOfSquares.put(centeredRangeMultiplier.map!"a[0]");
6070         centeredSummatorOfCubes.put(centeredRangeMultiplier.map!"a[1]");
6071     }
6072 
6073 const:
6074 
6075     ///
6076     size_t count()()
6077     {
6078         return meanAccumulator.count;
6079     }
6080     ///
6081     F mean(F = T)()
6082     {
6083         return meanAccumulator.mean!F;
6084     }
6085     ///
6086     F variance(F = T)(bool isPopulation)
6087     in
6088     {
6089         assert(count > 1, "SkewnessAccumulator.variance: count must be larger than one");
6090     }
6091     do
6092     {
6093         return centeredSumOfSquares!F / (count + isPopulation - 1);
6094     }
6095     ///
6096     F centeredSumOfSquares(F = T)()
6097     {
6098         return cast(F) centeredSummatorOfSquares.sum;
6099     }
6100     ///
6101     F centeredSumOfCubes(F = T)()
6102     {
6103         return cast(F) centeredSummatorOfCubes.sum;
6104     }
6105     ///
6106     F scaledSumOfCubes(F = T)(bool isPopulation)
6107     {
6108         import mir.math.common: sqrt;
6109         F var = variance!F(isPopulation);
6110         return centeredSumOfCubes!F / (var * var.sqrt);
6111     }
6112     ///
6113     F skewness(F = T)(bool isPopulation)
6114     in
6115     {
6116         assert(count > 2, "SkewnessAccumulator.skewness: count must be larger than two");
6117         assert(centeredSummatorOfSquares.sum > 0, "SkewnessAccumulator.skewness: variance must be larger than zero");
6118     }
6119     do
6120     {
6121         import mir.math.common: sqrt;
6122         F s = centeredSumOfSquares!F;
6123         return centeredSumOfCubes!F / (s * s.sqrt) * count * sqrt(cast(F) count + isPopulation - 1) /
6124             (count + 2 * isPopulation - 2);
6125         /+ Equivalent to
6126         return scaledSumOfCubes!F(isPopulation) / count *
6127                 (cast(F) count * count / ((count + isPopulation - 1) * (count + 2 * isPopulation - 2)));
6128         +/
6129     }
6130 }
6131 
6132 /// twoPass
6133 version(mir_stat_test)
6134 @safe pure nothrow
6135 unittest
6136 {
6137     import mir.math.common: approxEqual, sqrt;
6138     import mir.math.sum: Summation;
6139     import mir.ndslice.slice: sliced;
6140 
6141     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6142               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6143 
6144     auto v = SkewnessAccumulator!(double, SkewnessAlgo.twoPass, Summation.naive)(x);
6145     assert(v.skewness(true).approxEqual(12.000999 / 12));
6146     assert(v.skewness(false).approxEqual(12.000999 / 12 * sqrt(12.0 * 11.0) / 10.0));
6147 }
6148 
6149 // check withAsSlice
6150 version(mir_stat_test)
6151 @safe pure nothrow
6152 unittest
6153 {
6154     import mir.math.sum: Summation;
6155     import mir.rc.array: RCArray;
6156     import mir.test: shouldApprox;
6157 
6158     static immutable a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6159                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
6160 
6161     auto x = RCArray!double(12);
6162     foreach(i, ref e; x)
6163         e = a[i];
6164 
6165     auto v = SkewnessAccumulator!(double, SkewnessAlgo.twoPass, Summation.naive)(x);
6166     v.scaledSumOfCubes(true).shouldApprox == 12.000999;
6167 }
6168 
6169 // check dynamic array
6170 version(mir_stat_test)
6171 @safe pure nothrow
6172 unittest
6173 {
6174     import mir.math.sum: Summation;
6175     import mir.test: shouldApprox;
6176 
6177     double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6178                   2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
6179 
6180     auto v = SkewnessAccumulator!(double, SkewnessAlgo.twoPass, Summation.naive)(x);
6181     v.scaledSumOfCubes(true).shouldApprox == 12.000999;
6182 }
6183 
6184 // Test input range
6185 version(mir_stat_test)
6186 @safe pure nothrow
6187 unittest
6188 {
6189     import mir.math.sum: Summation;
6190     import mir.test: should;
6191     import std.range: iota;
6192     import std.algorithm: map;
6193 
6194     auto x1 = iota(0, 5);
6195     auto v1 = SkewnessAccumulator!(double, SkewnessAlgo.twoPass, Summation.naive)(x1);
6196     v1.skewness(true).should == 0;
6197     auto x2 = x1.map!(a => 2 * a);
6198     auto v2 = SkewnessAccumulator!(double, SkewnessAlgo.twoPass, Summation.naive)(x2);
6199     v2.skewness(true).should == 0;
6200 }
6201 
6202 ///
6203 struct SkewnessAccumulator(T, SkewnessAlgo skewnessAlgo, Summation summation)
6204     if (isMutable!T && skewnessAlgo == SkewnessAlgo.threePass)
6205 {
6206     import mir.math.sum: elementType, Summator;
6207     import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind;
6208     import std.range: isInputRange;
6209 
6210     ///
6211     private MeanAccumulator!(T, summation) meanAccumulator;
6212     ///
6213     alias S = Summator!(T, summation);
6214     ///
6215     private S centeredSummatorOfSquares;
6216     ///
6217     private S scaledSummatorOfCubes;
6218 
6219     ///
6220     this(Iterator, size_t N, SliceKind kind)(Slice!(Iterator, N, kind) slice)
6221     {
6222         import mir.functional: naryFun;
6223         import mir.ndslice.topology: vmap, map;
6224         import mir.ndslice.internal: LeftOp;
6225         import mir.math.common: sqrt;
6226 
6227         meanAccumulator.put(slice.lightScope);
6228         centeredSummatorOfSquares.put(slice.vmap(LeftOp!("-", T)(mean)).map!(naryFun!"a * a"));
6229 
6230         T stdP = variance!T(true).sqrt;
6231         assert(stdP > 0, "SkewnessAccumulator.this: must divide by positive standard deviation");
6232 
6233         scaledSummatorOfCubes.put(slice.
6234             vmap(LeftOp!("-", T)(mean)).
6235             vmap(LeftOp!("*", T)(1 / stdP)).
6236             map!(naryFun!"a * a * a"));
6237     }
6238 
6239     ///
6240     this(SliceLike)(SliceLike x)
6241         if (isConvertibleToSlice!SliceLike && !isSlice!SliceLike)
6242     {
6243         import mir.ndslice.slice: toSlice;
6244         this(x.toSlice);
6245     }
6246 
6247     ///
6248     this(Range)(Range range)
6249         if (isInputRange!Range && !isConvertibleToSlice!Range && is(elementType!Range : T))
6250     {
6251         import mir.math.common: sqrt;
6252         import std.algorithm: map;
6253 
6254         meanAccumulator.put(range);
6255         auto centeredRange = range.map!(a => (a - mean));
6256         centeredSummatorOfSquares.put(centeredRange.map!"a * a");
6257         T stdP = variance!T(true).sqrt;
6258         auto scaledRange = centeredRange.map!(a => a / stdP);
6259         scaledSummatorOfCubes.put(scaledRange.map!"a * a * a");
6260     }
6261 
6262 const:
6263 
6264     ///
6265     size_t count()()
6266     {
6267         return meanAccumulator.count;
6268     }
6269     ///
6270     F mean(F = T)()
6271     {
6272         return meanAccumulator.mean!F;
6273     }
6274     ///
6275     F variance(F = T)(bool isPopulation)
6276     in
6277     {
6278         assert(count > 1, "SkewnessAccumulator.variance: count must be larger than one");
6279     }
6280     do
6281     {
6282         return centeredSumOfSquares!F / (count + isPopulation - 1);
6283     }
6284     ///
6285     F centeredSumOfSquares(F = T)()
6286     {
6287         return cast(F) centeredSummatorOfSquares.sum;
6288     }
6289     ///
6290     F centeredSumOfCubes(F = T)()
6291     {
6292         import mir.math.common: sqrt;
6293         F varP = variance!F(true); // based on using the population variance as divisor above
6294         return scaledSumOfCubes!F * varP * varP.sqrt;
6295     }
6296     ///
6297     F scaledSumOfCubes(F = T)()
6298     {
6299         return cast(F) scaledSummatorOfCubes.sum;
6300     }
6301     ///
6302     F skewness(F = T)(bool isPopulation)
6303     in
6304     {
6305         assert(count > 2, "SkewnessAccumulator.skewness: count must be larger than two");
6306     }
6307     do
6308     {
6309         // formula for other skewness accumulators doesn't work here since we are
6310         // enforcing the the scaledSumOfCubes uses population variance and not that it can switch
6311         import mir.math.common: sqrt;
6312         return scaledSumOfCubes!F / (count + 2 * isPopulation - 2) *
6313                 sqrt(cast(F) (count + isPopulation - 1) / count);
6314         /+ Equivalent to
6315         return scaledSumOfCubes!F / count * 
6316                 sqrt(cast(F) count * (count + isPopulation - 1)) / (count + 2 * isPopulation - 2)
6317         +/
6318     }
6319 }
6320 
6321 /// threePass
6322 version(mir_stat_test)
6323 @safe pure nothrow
6324 unittest
6325 {
6326     import mir.math.common: approxEqual, sqrt;
6327     import mir.math.sum: Summation;
6328     import mir.ndslice.slice: sliced;
6329 
6330     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6331               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6332 
6333     auto v = SkewnessAccumulator!(double, SkewnessAlgo.threePass, Summation.naive)(x);
6334     assert(v.skewness(true).approxEqual(12.000999 / 12));
6335     assert(v.skewness(false).approxEqual(12.000999 / 12 * sqrt(12.0 * 11.0) / 10.0));
6336 }
6337 
6338 // check withAsSlice
6339 version(mir_stat_test)
6340 @safe pure nothrow
6341 unittest
6342 {
6343     import mir.math.common: approxEqual;
6344     import mir.math.sum: Summation;
6345     import mir.rc.array: RCArray;
6346 
6347     static immutable a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6348                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
6349 
6350     auto x = RCArray!double(12);
6351     foreach(i, ref e; x)
6352         e = a[i];
6353 
6354     auto v = SkewnessAccumulator!(double, SkewnessAlgo.threePass, Summation.naive)(x);
6355     assert(v.scaledSumOfCubes.approxEqual(12.000999));
6356 }
6357 
6358 // check dynamic array
6359 version(mir_stat_test)
6360 @safe pure nothrow
6361 unittest
6362 {
6363     import mir.math.common: approxEqual;
6364     import mir.math.sum: Summation;
6365 
6366     double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6367                   2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
6368 
6369     auto v = SkewnessAccumulator!(double, SkewnessAlgo.threePass, Summation.naive)(x);
6370     assert(v.scaledSumOfCubes.approxEqual(12.000999));
6371 }
6372 
6373 // Test input range
6374 version(mir_stat_test)
6375 @safe pure nothrow
6376 unittest
6377 {
6378     import mir.math.sum: Summation;
6379     import mir.test: should;
6380     import std.range: iota;
6381     import std.algorithm: map;
6382 
6383     auto x1 = iota(0, 5);
6384     auto v1 = SkewnessAccumulator!(double, SkewnessAlgo.threePass, Summation.naive)(x1);
6385     v1.skewness(true).should == 0;
6386     auto x2 = x1.map!(a => 2 * a);
6387     auto v2 = SkewnessAccumulator!(double, SkewnessAlgo.threePass, Summation.naive)(x2);
6388     v2.skewness(true).should == 0;
6389 }
6390 
6391 ///
6392 struct SkewnessAccumulator(T, SkewnessAlgo skewnessAlgo, Summation summation)
6393     if (isMutable!T && skewnessAlgo == SkewnessAlgo.assumeZeroMean)
6394 {
6395     import mir.math.sum: Summator;
6396     import std.traits: isIterable;
6397 
6398     ///
6399     private size_t _count;
6400     ///
6401     alias S = Summator!(T, summation);
6402     ///
6403     private S centeredSummatorOfSquares;
6404     ///
6405     private S centeredSummatorOfCubes;
6406 
6407     ///
6408     this(Range)(Range r)
6409         if (isIterable!Range)
6410     {
6411         this.put(r);
6412     }
6413 
6414     ///
6415     this()(T x)
6416     {
6417         this.put(x);
6418     }
6419 
6420     ///
6421     void put(Range)(Range r)
6422         if (isIterable!Range)
6423     {
6424         foreach(x; r)
6425         {
6426             this.put(x);
6427         }
6428     }
6429 
6430     ///
6431     void put()(T x)
6432     {
6433         _count++;
6434         T x2 = x * x;
6435         centeredSummatorOfSquares.put(x2);
6436         centeredSummatorOfCubes.put(x2 * x);
6437     }
6438 
6439     ///
6440     void put(U, Summation sumAlgo)(SkewnessAccumulator!(U, skewnessAlgo, sumAlgo) v)
6441     {
6442         _count += v.count;
6443         centeredSummatorOfSquares.put(v.centeredSumOfSquares!T);
6444         centeredSummatorOfCubes.put(v.centeredSumOfCubes!T);
6445     }
6446 
6447 const:
6448 
6449     ///
6450     size_t count() @property
6451     {
6452         return _count;
6453     }
6454     ///
6455     F mean(F = T)() @property
6456     {
6457         return cast(F) 0;
6458     }
6459     ///
6460     F variance(F = T)(bool isPopulation) @property
6461     in
6462     {
6463         assert(count > 1, "SkewnessAccumulator.variance: count must be larger than one");
6464     }
6465     do
6466     {
6467         return centeredSumOfSquares!F / (count + isPopulation - 1);
6468     }
6469     MeanAccumulator!(T, summation) meanAccumulator()()
6470     {
6471         typeof(return) m = { _count, T(0) };
6472         return m;
6473     }
6474     ///
6475     F centeredSumOfCubes(F = T)() @property
6476     {
6477         return cast(F) centeredSummatorOfCubes.sum;
6478     }
6479     ///
6480     F centeredSumOfSquares(F = T)() @property
6481     {
6482         return cast(F) centeredSummatorOfSquares.sum;
6483     }
6484     ///
6485     F scaledSumOfCubes(F = T)(bool isPopulation) @property
6486     {
6487         import mir.math.common: sqrt;
6488 
6489         F var = variance!F(isPopulation);
6490         return centeredSumOfCubes!F / (var * var.sqrt);
6491     }
6492     ///
6493     F skewness(F = T)(bool isPopulation)
6494     in
6495     {
6496         assert(count > 2, "SkewnessAccumulator.skewness: count must be larger than two");
6497         assert(centeredSummatorOfSquares.sum > 0, "SkewnessAccumulator.skewness: variance must be larger than zero");
6498     }
6499     do
6500     {
6501         import mir.math.common: sqrt;
6502         F s = centeredSumOfSquares!F;
6503         return centeredSumOfCubes!F / (s * s.sqrt) * count * sqrt(cast(F) count + isPopulation - 1) /
6504             (count + 2 * isPopulation - 2);
6505         /+ Equivalent to
6506         return scaledSumOfCubes!F(isPopulation) / count *
6507                 (cast(F) count * count / ((count + isPopulation - 1) * (count + 2 * isPopulation - 2)));
6508         +/
6509     }
6510 }
6511 
6512 /// assumeZeroMean
6513 version(mir_stat_test)
6514 @safe pure nothrow
6515 unittest
6516 {
6517     import mir.math.common: approxEqual, pow;
6518     import mir.math.sum: Summation;
6519     import mir.ndslice.slice: sliced;
6520     import mir.stat.transform: center;
6521 
6522     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6523               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6524     auto x = a.center;
6525 
6526     SkewnessAccumulator!(double, SkewnessAlgo.assumeZeroMean, Summation.naive) v;
6527     v.put(x);
6528     assert(v.skewness(true).approxEqual((117.005859 / 12) / pow(54.765625 / 12, 1.5)));
6529     assert(v.skewness(false).approxEqual((117.005859 / 12) / pow(54.765625 / 11, 1.5) * 12.0 ^^ 2 / (11.0 * 10.0)));
6530 
6531     v.put(4.0);
6532     assert(v.skewness(true).approxEqual((181.005859 / 13) / pow(70.765625 / 13, 1.5)));
6533     assert(v.skewness(false).approxEqual((181.005859 / 13) / pow(70.765625 / 12, 1.5) * 13.0 ^^ 2 / (12.0 * 11.0)));
6534 }
6535 
6536 // Can put slices
6537 version(mir_stat_test)
6538 @safe pure nothrow
6539 unittest
6540 {
6541     import mir.math.common: approxEqual;
6542     import mir.math.sum: Summation;
6543     import mir.ndslice.slice: sliced;
6544     import mir.stat.transform: center;
6545 
6546     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6547               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6548     auto b = a.center;
6549     auto x = b[0 .. 6];
6550     auto y = b[6 .. $];
6551 
6552     SkewnessAccumulator!(double, SkewnessAlgo.assumeZeroMean, Summation.naive) v;
6553     v.put(x);
6554     assert(v.centeredSumOfCubes.approxEqual(-11.206543));
6555     assert(v.centeredSumOfSquares.approxEqual(13.49219));
6556 
6557     v.put(y);
6558     assert(v.centeredSumOfCubes.approxEqual(117.005859));
6559     assert(v.centeredSumOfSquares.approxEqual(54.765625));
6560 }
6561 
6562 // Can put SkewnessAccumulator
6563 version(mir_stat_test)
6564 @safe pure nothrow
6565 unittest
6566 {
6567     import mir.math.common: approxEqual;
6568     import mir.ndslice.slice: sliced;
6569     import mir.stat.transform: center;
6570 
6571     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6572               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6573     auto b = a.center;
6574     auto x = b[0 .. 6];
6575     auto y = b[6 .. $];
6576 
6577     SkewnessAccumulator!(double, SkewnessAlgo.assumeZeroMean, Summation.naive) v;
6578     v.put(x);
6579     assert(v.centeredSumOfCubes.approxEqual(-11.206543));
6580     assert(v.centeredSumOfSquares.approxEqual(13.49219));
6581 
6582     SkewnessAccumulator!(double, SkewnessAlgo.assumeZeroMean, Summation.naive) w;
6583     w.put(y);
6584     v.put(w);
6585     assert(v.centeredSumOfCubes.approxEqual(117.005859));
6586     assert(v.centeredSumOfSquares.approxEqual(54.765625));
6587 }
6588 
6589 // check variance/scaledSumOfCubes
6590 version(mir_stat_test)
6591 @safe pure nothrow
6592 unittest
6593 {
6594     import mir.math.common: sqrt;
6595     import mir.math.sum: Summation;
6596     import mir.ndslice.slice: sliced;
6597     import mir.stat.transform: center;
6598     import mir.test: shouldApprox;
6599 
6600     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6601               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6602     auto x = a.center;
6603 
6604     SkewnessAccumulator!(double, SkewnessAlgo.assumeZeroMean, Summation.naive) v;
6605     v.put(x);
6606     auto varP = x.variance!"assumeZeroMean"(true);
6607     auto varS = x.variance!"assumeZeroMean"(false);
6608     v.variance(true).shouldApprox == varP;
6609     v.variance(false).shouldApprox == varS;
6610     v.scaledSumOfCubes(true).shouldApprox == v.centeredSumOfCubes / (varP * varP.sqrt);
6611     v.scaledSumOfCubes(false).shouldApprox == v.centeredSumOfCubes / (varS * varS.sqrt);
6612 }
6613 
6614 ///
6615 struct SkewnessAccumulator(T, SkewnessAlgo skewnessAlgo, Summation summation)
6616     if (isMutable!T && skewnessAlgo == SkewnessAlgo.hybrid)
6617 {
6618     import mir.math.sum: elementType, Summator;
6619     import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind;
6620     import std.range: isInputRange;
6621     import std.traits: isIterable;
6622 
6623     ///
6624     private MeanAccumulator!(T, summation) meanAccumulator;
6625     ///
6626     alias S = Summator!(T, summation);
6627     ///
6628     private S centeredSummatorOfSquares;
6629     ///
6630     private S centeredSummatorOfCubes;
6631 
6632     ///
6633     this(Iterator, size_t N, SliceKind kind)(Slice!(Iterator, N, kind) slice)
6634     {
6635         import mir.functional: naryFun;
6636         import mir.ndslice.topology: vmap, map;
6637         import mir.ndslice.internal: LeftOp;
6638 
6639         meanAccumulator.put(slice.lightScope);
6640 
6641         auto sliceMap = slice.vmap(LeftOp!("-", T)(mean)).map!(naryFun!"a * a", naryFun!"a * a * a");
6642         centeredSummatorOfSquares.put(sliceMap.map!"a[0]");
6643         centeredSummatorOfCubes.put(sliceMap.map!"a[1]");
6644     }
6645 
6646     ///
6647     this(SliceLike)(SliceLike x)
6648         if (isConvertibleToSlice!SliceLike && !isSlice!SliceLike)
6649     {
6650         import mir.ndslice.slice: toSlice;
6651         this(x.toSlice);
6652     }
6653 
6654     ///
6655     this(Range)(Range range)
6656         if (isIterable!Range && !isConvertibleToSlice!Range)
6657     {
6658         static if (isInputRange!Range && is(elementType!Range : T)) {
6659             import std.algorithm: map;
6660             meanAccumulator.put(range);
6661 
6662             auto centeredRangeMultiplier = range.map!(a => (a - mean)).map!("a * a", "a * a * a");
6663             centeredSummatorOfSquares.put(centeredRangeMultiplier.map!"a[0]");
6664             centeredSummatorOfCubes.put(centeredRangeMultiplier.map!"a[1]");
6665         } else {
6666             this.put(range);
6667         }
6668     }
6669 
6670     ///
6671     this()(T x)
6672     {
6673         this.put(x);
6674     }
6675 
6676     ///
6677     void put(Range)(Range r)
6678         if (isIterable!Range)
6679     {
6680         static if (isInputRange!Range && is(elementType!Range : T)) {
6681             auto v = typeof(this)(r);
6682             this.put(v);
6683         } else {
6684             foreach(x; r)
6685             {
6686                 this.put(x);
6687             }
6688         }
6689     }
6690 
6691     ///
6692     void put()(T x)
6693     {
6694         T deltaOld = x;
6695         if (count > 0) {
6696             deltaOld -= mean;
6697         }
6698         meanAccumulator.put(x);
6699         T deltaNew = x - mean;
6700         centeredSummatorOfCubes.put(deltaOld * deltaOld * deltaOld * (count - 1) * (count - 2) / (count * count) -
6701                                     3 * deltaOld * centeredSumOfSquares / count);
6702         centeredSummatorOfSquares.put(deltaOld * deltaNew);
6703     }
6704 
6705     ///
6706     void put(U, SkewnessAlgo skewAlgo, Summation sumAlgo)(SkewnessAccumulator!(U, skewAlgo, sumAlgo) v)
6707         if (skewAlgo != SkewnessAlgo.assumeZeroMean)
6708     {
6709         size_t oldCount = count;
6710         T delta = v.mean;
6711         if (oldCount > 0) {
6712             delta -= mean;
6713         }
6714         meanAccumulator.put!T(v.meanAccumulator);
6715         centeredSummatorOfCubes.put(v.centeredSumOfCubes!T + 
6716                                     delta * delta * delta * v.count * oldCount * (oldCount - v.count) / (count * count) +
6717                                     3 * delta * (oldCount * v.centeredSumOfSquares!T - v.count * centeredSumOfSquares!T) / count);
6718         centeredSummatorOfSquares.put(v.centeredSumOfSquares!T + delta * delta * v.count * oldCount / count);
6719     }
6720 
6721 const:
6722 
6723     ///
6724     size_t count() @property
6725     {
6726         return meanAccumulator.count;
6727     }
6728     ///
6729     F mean(F = T)() @property
6730     {
6731         return meanAccumulator.mean!F;
6732     }
6733     ///
6734     F variance(F = T)(bool isPopulation) @property
6735     in
6736     {
6737         assert(count > 1, "SkewnessAccumulator.variance: count must be larger than one");
6738     }
6739     do
6740     {
6741         return centeredSumOfSquares!F / (count + isPopulation - 1);
6742     }
6743     ///
6744     F centeredSumOfSquares(F = T)()
6745     {
6746         return cast(F) centeredSummatorOfSquares.sum;
6747     }
6748     ///
6749     F centeredSumOfCubes(F = T)()
6750     {
6751         return cast(F) centeredSummatorOfCubes.sum;
6752     }
6753     ///
6754     F scaledSumOfCubes(F = T)(bool isPopulation)
6755     {
6756         import mir.math.common: sqrt;
6757         F var = variance!F(isPopulation);
6758         return centeredSumOfCubes!F / (var * var.sqrt);
6759     }
6760     ///
6761     F skewness(F = T)(bool isPopulation)
6762     in
6763     {
6764         assert(count > 2, "SkewnessAccumulator.skewness: count must be larger than two");
6765         assert(centeredSummatorOfSquares.sum > 0, "SkewnessAccumulator.skewness: variance must be larger than zero");
6766     }
6767     do
6768     {
6769         import mir.math.common: sqrt;
6770         F s = centeredSumOfSquares!F;
6771         return centeredSumOfCubes!F / (s * s.sqrt) * count * sqrt(cast(F) count + isPopulation - 1) /
6772             (count + 2 * isPopulation - 2);
6773         /+ Equivalent to
6774         return scaledSumOfCubes!F(isPopulation) / count *
6775                 (cast(F) count * count / ((count + isPopulation - 1) * (count + 2 * isPopulation - 2)));
6776         +/
6777     }
6778 }
6779 
6780 /// hybrid
6781 version(mir_stat_test)
6782 @safe pure nothrow
6783 unittest
6784 {
6785     import mir.math.common: approxEqual, pow;
6786     import mir.math.sum: Summation;
6787     import mir.ndslice.slice: sliced;
6788 
6789     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6790               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6791 
6792     auto v = SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive)(x);
6793     assert(v.skewness(true).approxEqual((117.005859 / 12) / pow(54.765625 / 12, 1.5)));
6794     assert(v.skewness(false).approxEqual((117.005859 / 12) / pow(54.765625 / 11, 1.5) * (12.0 ^^ 2) / (11.0 * 10.0)));
6795 
6796     v.put(4.0);
6797     assert(v.skewness(true).approxEqual((100.238166 / 13) / pow(57.019231 / 13, 1.5)));
6798     assert(v.skewness(false).approxEqual((100.238166 / 13) / pow(57.019231 / 12, 1.5) * (13.0 ^^ 2) / (12.0 * 11.0)));
6799 }
6800 
6801 // check withAsSlice
6802 version(mir_stat_test)
6803 @safe pure nothrow
6804 unittest
6805 {
6806     import mir.math.sum: Summation;
6807     import mir.rc.array: RCArray;
6808     import mir.test: shouldApprox;
6809 
6810     static immutable a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6811                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
6812 
6813     auto x = RCArray!double(12);
6814     foreach(i, ref e; x)
6815         e = a[i];
6816 
6817     auto v = SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive)(x);
6818     v.scaledSumOfCubes(true).shouldApprox == 12.000999;
6819 }
6820 
6821 // check dynamic array
6822 version(mir_stat_test)
6823 @safe pure nothrow
6824 unittest
6825 {
6826     import mir.math.sum: Summation;
6827     import mir.test: shouldApprox;
6828 
6829     double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6830                   2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
6831 
6832     auto v = SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive)(x);
6833     v.scaledSumOfCubes(true).shouldApprox == 12.000999;
6834 }
6835 
6836 // Test input range
6837 version(mir_stat_test)
6838 @safe pure nothrow
6839 unittest
6840 {
6841     import mir.math.sum: Summation;
6842     import mir.test: should;
6843     import std.algorithm: map;
6844     import std.range: chunks, iota;
6845 
6846     auto x1 = iota(0, 5);
6847     auto v1 = SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive)(x1);
6848     v1.skewness(true).should == 0;
6849     auto x2 = x1.map!(a => 2 * a);
6850     auto v2 = SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive)(x2);
6851     v2.skewness(true).should == 0;
6852     SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive) v3;
6853     v3.put(x1.chunks(1));
6854     v3.skewness(true).should == 0;
6855     auto v4 = SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive)(x1.chunks(1));
6856     v4.skewness(true).should == 0;
6857 }
6858 
6859 // Can put slice
6860 version(mir_stat_test)
6861 @safe pure nothrow
6862 unittest
6863 {
6864     import mir.math.common: approxEqual, pow;
6865     import mir.math.sum: Summation;
6866     import mir.ndslice.slice: sliced;
6867 
6868     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
6869     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6870 
6871     auto v = SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive)(x);
6872     assert(v.centeredSumOfCubes.approxEqual(4.071181));
6873     assert(v.centeredSumOfSquares.approxEqual(12.552083));
6874 
6875     v.put(y);
6876     assert(v.centeredSumOfCubes.approxEqual(117.005859));
6877     assert(v.centeredSumOfSquares.approxEqual(54.765625));
6878 }
6879 
6880 // Can put SkewnessAccumulator
6881 version(mir_stat_test)
6882 @safe pure nothrow
6883 unittest
6884 {
6885     import mir.math.common: approxEqual, pow;
6886     import mir.ndslice.slice: sliced;
6887 
6888     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
6889     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6890 
6891     auto v = SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive)(x);
6892     assert(v.centeredSumOfCubes.approxEqual(4.071181));
6893     assert(v.centeredSumOfSquares.approxEqual(12.552083));
6894 
6895     auto w = SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive)(y);
6896     v.put(w);
6897     assert(v.centeredSumOfCubes.approxEqual(117.005859));
6898     assert(v.centeredSumOfSquares.approxEqual(54.765625));
6899 }
6900 
6901 // Can put SkewnessAccumulator (naive)
6902 version(mir_stat_test)
6903 @safe pure nothrow
6904 unittest
6905 {
6906     import mir.math.common: approxEqual, pow;
6907     import mir.ndslice.slice: sliced;
6908 
6909     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
6910     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6911 
6912     auto v = SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive)(x);
6913     assert(v.centeredSumOfCubes.approxEqual(4.071181));
6914     assert(v.centeredSumOfSquares.approxEqual(12.552083));
6915 
6916     auto w = SkewnessAccumulator!(double, SkewnessAlgo.naive, Summation.naive)(y);
6917     v.put(w);
6918     assert(v.centeredSumOfCubes.approxEqual(117.005859));
6919     assert(v.centeredSumOfSquares.approxEqual(54.765625));
6920 }
6921 
6922 // Can put SkewnessAccumulator (online)
6923 version(mir_stat_test)
6924 @safe pure nothrow
6925 unittest
6926 {
6927     import mir.math.common: approxEqual, pow;
6928     import mir.ndslice.slice: sliced;
6929 
6930     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
6931     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6932 
6933     auto v = SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive)(x);
6934     assert(v.centeredSumOfCubes.approxEqual(4.071181));
6935     assert(v.centeredSumOfSquares.approxEqual(12.552083));
6936 
6937     auto w = SkewnessAccumulator!(double, SkewnessAlgo.online, Summation.naive)(y);
6938     v.put(w);
6939     assert(v.centeredSumOfCubes.approxEqual(117.005859));
6940     assert(v.centeredSumOfSquares.approxEqual(54.765625));
6941 }
6942 
6943 // Can put SkewnessAccumulator (twoPass)
6944 version(mir_stat_test)
6945 @safe pure nothrow
6946 unittest
6947 {
6948     import mir.math.common: approxEqual, pow;
6949     import mir.ndslice.slice: sliced;
6950 
6951     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
6952     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6953 
6954     auto v = SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive)(x);
6955     assert(v.centeredSumOfCubes.approxEqual(4.071181));
6956     assert(v.centeredSumOfSquares.approxEqual(12.552083));
6957 
6958     auto w = SkewnessAccumulator!(double, SkewnessAlgo.twoPass, Summation.naive)(y);
6959     v.put(w);
6960     assert(v.centeredSumOfCubes.approxEqual(117.005859));
6961     assert(v.centeredSumOfSquares.approxEqual(54.765625));
6962 }
6963 
6964 // Can put SkewnessAccumulator (threePass)
6965 version(mir_stat_test)
6966 @safe pure nothrow
6967 unittest
6968 {
6969     import mir.math.common: approxEqual, pow;
6970     import mir.ndslice.slice: sliced;
6971 
6972     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
6973     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6974 
6975     auto v = SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive)(x);
6976     assert(v.centeredSumOfCubes.approxEqual(4.071181));
6977     assert(v.centeredSumOfSquares.approxEqual(12.552083));
6978 
6979     auto w = SkewnessAccumulator!(double, SkewnessAlgo.threePass, Summation.naive)(y);
6980     v.put(w);
6981     assert(v.centeredSumOfCubes.approxEqual(117.005859));
6982     assert(v.centeredSumOfSquares.approxEqual(54.765625));
6983 }
6984 
6985 // check variance/scaledSumOfCubes
6986 version(mir_stat_test)
6987 @safe pure nothrow
6988 unittest
6989 {
6990     import mir.math.common: sqrt;
6991     import mir.math.sum: Summation;
6992     import mir.ndslice.slice: sliced;
6993     import mir.test: shouldApprox;
6994 
6995     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
6996               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
6997 
6998     auto v = SkewnessAccumulator!(double, SkewnessAlgo.hybrid, Summation.naive)(x);
6999     auto varP = x.variance!"twoPass"(true);
7000     auto varS = x.variance!"twoPass"(false);
7001     v.variance(true).shouldApprox == varP;
7002     v.variance(false).shouldApprox == varS;
7003     v.scaledSumOfCubes(true).shouldApprox == v.centeredSumOfCubes / (varP * varP.sqrt);
7004     v.scaledSumOfCubes(false).shouldApprox == v.centeredSumOfCubes / (varS * varS.sqrt);
7005 }
7006 
7007 /++
7008 Calculates the skewness of the input
7009 
7010 By default, if `F` is not floating point type, then the result will have a
7011 `double` type if `F` is implicitly convertible to a floating point type.
7012 
7013 Params:
7014     F = controls type of output
7015     skewnessAlgo = algorithm for calculating skewness (default: SkewnessAlgo.hybrid)
7016     summation = algorithm for calculating sums (default: Summation.appropriate)
7017 
7018 Returns:
7019     The skewness of the input, must be floating point or complex type
7020 
7021 See_also:
7022     $(LREF SkewnessAlgo)
7023 +/
7024 template skewness(F, 
7025                   SkewnessAlgo skewnessAlgo = SkewnessAlgo.hybrid, 
7026                   Summation summation = Summation.appropriate)
7027 {
7028     import std.traits: isIterable;
7029 
7030     /++
7031     Params:
7032         r = range, must be finite iterable
7033         isPopulation = true if population skewness, false if sample skewness (default)
7034     +/
7035     @fmamath stdevType!F skewness(Range)(Range r, bool isPopulation = false)
7036         if (isIterable!Range)
7037     {
7038         import core.lifetime: move;
7039         alias G = typeof(return);
7040         auto skewnessAccumulator = SkewnessAccumulator!(G, skewnessAlgo, ResolveSummationType!(summation, Range, G))(r.move);
7041         return skewnessAccumulator.skewness(isPopulation);
7042     }
7043 
7044     /++
7045     Params:
7046         ar = values
7047     +/
7048     @fmamath stdevType!F skewness(scope const F[] ar...)
7049     {
7050         alias G = typeof(return);
7051         auto skewnessAccumulator = SkewnessAccumulator!(G, skewnessAlgo, ResolveSummationType!(summation, const(G)[], G))(ar);
7052         return skewnessAccumulator.skewness(false);
7053     }
7054 }
7055 
7056 /// ditto
7057 template skewness(SkewnessAlgo skewnessAlgo = SkewnessAlgo.hybrid, 
7058                   Summation summation = Summation.appropriate)
7059 {
7060     import std.traits: isIterable;
7061 
7062     /++
7063     Params:
7064         r = range, must be finite iterable
7065         isPopulation = true if population skewness, false if sample skewness (default)
7066     +/
7067     @fmamath stdevType!Range skewness(Range)(Range r, bool isPopulation = false)
7068         if (isIterable!Range)
7069     {
7070         import core.lifetime: move;
7071         alias F = typeof(return);
7072         return .skewness!(F, skewnessAlgo, summation)(r.move, isPopulation);
7073     }
7074 
7075     /++
7076     Params:
7077         ar = values
7078     +/
7079     @fmamath stdevType!T skewness(T)(scope const T[] ar...)
7080     {
7081         alias F = typeof(return);
7082         return .skewness!(F, skewnessAlgo, summation)(ar);
7083     }
7084 }
7085 
7086 /// ditto
7087 template skewness(F, string skewnessAlgo, string summation = "appropriate")
7088 {
7089     mixin("alias skewness = .skewness!(F, SkewnessAlgo." ~ skewnessAlgo ~ ", Summation." ~ summation ~ ");");
7090 }
7091 
7092 /// ditto
7093 template skewness(string skewnessAlgo, string summation = "appropriate")
7094 {
7095     mixin("alias skewness = .skewness!(SkewnessAlgo." ~ skewnessAlgo ~ ", Summation." ~ summation ~ ");");
7096 }
7097 
7098 /// Simple example
7099 version(mir_stat_test)
7100 @safe pure nothrow
7101 unittest
7102 {
7103     import mir.math.common: approxEqual, pow;
7104     import mir.ndslice.slice: sliced;
7105 
7106     assert(skewness([1.0, 2, 3]).approxEqual(0.0));
7107 
7108     assert(skewness([1.0, 2, 4]).approxEqual((2.222222 / 3) / pow(4.666667 / 2, 1.5) * (3.0 ^^ 2) / (2.0 * 1.0)));
7109     assert(skewness([1.0, 2, 4], true).approxEqual((2.222222 / 3) / pow(4.666667 / 3, 1.5)));
7110 
7111     assert(skewness!float([0, 1, 2, 3, 4, 6].sliced(3, 2)).approxEqual(0.462910));
7112 
7113     static assert(is(typeof(skewness!float([1, 2, 3])) == float));
7114 }
7115 
7116 /// Skewness of vector
7117 version(mir_stat_test)
7118 @safe pure nothrow
7119 unittest
7120 {
7121     import mir.math.common: approxEqual, pow;
7122 
7123     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
7124               2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
7125 
7126     assert(x.skewness.approxEqual((117.005859 / 12) / pow(54.765625 / 11, 1.5) * (12.0 ^^ 2) / (11.0 * 10.0)));
7127 }
7128 
7129 /// Skewness of matrix
7130 version(mir_stat_test)
7131 @safe pure
7132 unittest
7133 {
7134     import mir.math.common: approxEqual, pow;
7135     import mir.ndslice.fuse: fuse;
7136 
7137     auto x = [
7138         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
7139         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
7140     ].fuse;
7141 
7142     assert(x.skewness.approxEqual((117.005859 / 12) / pow(54.765625 / 11, 1.5) * (12.0 ^^ 2) / (11.0 * 10.0)));
7143 }
7144 
7145 /// Column skewness of matrix
7146 version(mir_stat_test)
7147 @safe pure
7148 unittest
7149 {
7150     import mir.algorithm.iteration: all;
7151     import mir.math.common: approxEqual, pow;
7152     import mir.ndslice.fuse: fuse;
7153     import mir.ndslice.topology: alongDim, byDim, map;
7154 
7155     auto x = [
7156         [0.0,  1.0, 1.5, 2.0], 
7157         [3.5, 4.25, 2.0, 7.5],
7158         [5.0,  1.0, 1.5, 0.0]
7159     ].fuse;
7160     auto result = [-1.090291, 1.732051, 1.732051, 1.229809];
7161 
7162     // Use byDim or alongDim with map to compute skewness of row/column.
7163     assert(x.byDim!1.map!skewness.all!approxEqual(result));
7164     assert(x.alongDim!0.map!skewness.all!approxEqual(result));
7165 
7166     // FIXME
7167     // Without using map, computes the skewness of the whole slice
7168     // assert(x.byDim!1.skewness == x.sliced.skewness);
7169     // assert(x.alongDim!0.skewness == x.sliced.skewness);
7170 }
7171 
7172 /// Can also set algorithm type
7173 version(mir_stat_test)
7174 @safe pure
7175 unittest
7176 {
7177     import mir.math.common: approxEqual, pow, sqrt;
7178     import mir.ndslice.slice: sliced;
7179 
7180     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
7181               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
7182 
7183     auto x = a + 100_000_000_000;
7184 
7185     // The online algorithm is numerically unstable in this case
7186     auto y = x.skewness!"online";
7187     assert(!y.approxEqual((117.005859 / 12) / pow(54.765625 / 11, 1.5) * (12.0 ^^ 2) / (11.0 * 10.0)));
7188 
7189     // The naive algorithm has an assert error in this case because standard
7190     // deviation is calculated naively as zero. The skewness formula would then
7191     // be dividing by zero. 
7192     //auto z0 = x.skewness!(real, "naive");
7193 
7194     // However, the two-pass and three-pass algorithms are numerically stable in this case
7195     auto z1 = x.skewness!"twoPass";
7196     assert(z1.approxEqual(12.000999 / 12 * sqrt(12.0 * 11.0) / 10.0));
7197     assert(!z1.approxEqual(y));
7198     auto z2 = x.skewness!"threePass";
7199     assert(z2.approxEqual((12.000999 / 12) * sqrt(12.0 * 11.0) / 10.0));
7200     assert(!z2.approxEqual(y));
7201 
7202     // And the assumeZeroMean algorithm provides the incorrect answer, as expected
7203     auto z3 = x.skewness!"assumeZeroMean";
7204     assert(!z3.approxEqual(y));
7205 }
7206 
7207 // Alt version with x a tenth of above's value
7208 version(mir_stat_test)
7209 @safe pure
7210 unittest
7211 {
7212     import mir.math.common: approxEqual, pow, sqrt;
7213     import mir.ndslice.slice: sliced;
7214 
7215     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
7216               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
7217 
7218     auto x = a + 10_000_000_000;
7219 
7220     // The online algorithm is numerically stable in this case
7221     auto y = x.skewness!"online";
7222     assert(y.approxEqual((117.005859 / 12) / pow(54.765625 / 11, 1.5) * (12.0 ^^ 2) / (11.0 * 10.0)));
7223 
7224     // The naive algorithm has an assert error in this case because standard
7225     // deviation is calculated naively as zero. The skewness formula would then
7226     // be dividing by zero. 
7227     //auto z0 = x.skewness!(real, "naive");
7228 
7229     // The two-pass algorithm is  numerically stable in this case
7230     auto z1 = x.skewness!"twoPass";
7231     assert(z1.approxEqual(12.000999 / 12 * sqrt(12.0 * 11.0) / 10.0));
7232     assert(z1.approxEqual(y));
7233 
7234     // However, the three-pass algorithm is numerically stable in this case
7235     auto z2 = x.skewness!"threePass";
7236     assert(z2.approxEqual((12.000999 / 12) * sqrt(12.0 * 11.0) / 10.0));
7237     assert(z2.approxEqual(y));
7238 
7239     // And the assumeZeroMean algorithm provides the incorrect answer, as expected
7240     auto z3 = x.skewness!"assumeZeroMean";
7241     assert(!z3.approxEqual(y));
7242 }
7243 
7244 /// Can also set algorithm or output type
7245 version(mir_stat_test)
7246 @safe pure nothrow
7247 unittest
7248 {
7249     import mir.math.common: approxEqual;
7250     import mir.ndslice.slice: sliced;
7251     import mir.ndslice.topology: repeat;
7252 
7253     //Set population skewness, skewness algorithm, sum algorithm or output type
7254 
7255     auto a = [1.0, 1e98, 1, -1e98].sliced;
7256     auto x = a * 10_000;
7257 
7258     /++
7259     Due to Floating Point precision, when centering `x`, subtracting the mean 
7260     from the second and fourth numbers has no effect. Further, after centering 
7261     and squaring `x`, the first and third numbers in the slice have precision 
7262     too low to be included in the centered sum of squares. 
7263     +/
7264     assert(x.skewness(false).approxEqual(0.0));
7265     assert(x.skewness(true).approxEqual(0.0));
7266 
7267     assert(x.skewness!("online").approxEqual(0.0));
7268     assert(x.skewness!("online", "kbn").approxEqual(0.0));
7269     assert(x.skewness!("online", "kb2").approxEqual(0.0));
7270     assert(x.skewness!("online", "precise").approxEqual(0.0));
7271     assert(x.skewness!(double, "online", "precise").approxEqual(0.0));
7272     assert(x.skewness!(double, "online", "precise")(true).approxEqual(0.0));
7273 
7274     auto y = [uint.max - 2, uint.max - 1, uint.max].sliced;
7275     auto z = y.skewness!(ulong, "threePass");
7276     assert(z == 0.0);
7277     static assert(is(typeof(z) == double));
7278 }
7279 
7280 /++
7281 For integral slices, can pass output type as template parameter to ensure output
7282 type is correct.
7283 +/
7284 version(mir_stat_test)
7285 @safe pure nothrow
7286 unittest
7287 {
7288     import mir.math.common: approxEqual;
7289     import mir.ndslice.slice: sliced;
7290 
7291     auto x = [0, 1, 1, 2, 4, 4,
7292               2, 7, 5, 1, 2, 0].sliced;
7293 
7294     auto y = x.skewness;
7295     assert(y.approxEqual(0.925493));
7296     static assert(is(typeof(y) == double));
7297 
7298     assert(x.skewness!float.approxEqual(0.925493));
7299 }
7300 
7301 /++
7302 Skewness works for other user-defined types (provided they
7303 can be converted to a floating point)
7304 +/
7305 version(mir_stat_test)
7306 @safe pure nothrow
7307 unittest
7308 {
7309     static struct Foo {
7310         float x;
7311         alias x this;
7312     }
7313 
7314     Foo[] foo = [Foo(1f), Foo(2f), Foo(3f)];
7315     assert(foo.skewness == 0f);
7316 }
7317 
7318 /// Compute skewness along specified dimention of tensors
7319 version(mir_stat_test)
7320 @safe pure
7321 unittest
7322 {
7323     import mir.algorithm.iteration: all;
7324     import mir.math.common: approxEqual;
7325     import mir.ndslice.fuse: fuse;
7326     import mir.ndslice.topology: as, iota, alongDim, map, repeat;
7327 
7328     auto x = [
7329         [0.0, 1, 3],
7330         [3.0, 4, 5],
7331         [6.0, 7, 7],
7332     ].fuse;
7333 
7334     assert(x.skewness.approxEqual(-0.308571));
7335 
7336     auto m0 = [0, 0.0, 0.0];
7337     assert(x.alongDim!0.map!skewness.all!approxEqual(m0));
7338     assert(x.alongDim!(-2).map!skewness.all!approxEqual(m0));
7339 
7340     auto m1 = [0.935220, 0.0, -1.732051];
7341     assert(x.alongDim!1.map!skewness.all!approxEqual(m1));
7342     assert(x.alongDim!(-1).map!skewness.all!approxEqual(m1));
7343     assert(iota(3, 4, 5, 6).as!double.alongDim!0.map!skewness.all!approxEqual(repeat(0.0, 4, 5, 6)));
7344 }
7345 
7346 /// Arbitrary skewness
7347 version(mir_stat_test)
7348 @safe pure nothrow @nogc
7349 unittest
7350 {
7351     assert(skewness(1.0, 2, 3) == 0.0);
7352     assert(skewness!float(1, 2, 3) == 0f);
7353 }
7354 
7355 // Check skewness vector UFCS
7356 version(mir_stat_test)
7357 @safe pure nothrow
7358 unittest
7359 {
7360     import mir.math.common: approxEqual;
7361     assert([1.0, 2, 3, 4].skewness.approxEqual(0.0));
7362 }
7363 
7364 // Double-check correct output types
7365 version(mir_stat_test)
7366 @safe pure nothrow
7367 unittest
7368 {
7369     import mir.algorithm.iteration: all;
7370     import mir.math.common: approxEqual;
7371     import mir.ndslice.topology: iota, alongDim, map;
7372 
7373     auto x = iota([3, 3], 1);
7374     auto y = x.alongDim!1.map!skewness;
7375     assert(y.all!approxEqual([0.0, 0.0, 0.0]));
7376     static assert(is(stdevType!(typeof(y)) == double));
7377 }
7378 
7379 // @nogc skewness test
7380 version(mir_stat_test)
7381 @safe pure @nogc nothrow
7382 unittest
7383 {
7384     import mir.math.common: approxEqual, pow;
7385     import mir.ndslice.slice: sliced;
7386 
7387     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
7388                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
7389 
7390     assert(x.sliced.skewness.approxEqual((117.005859 / 12) / pow(54.765625 / 11, 1.5) * (12.0 ^^ 2) / (11.0 * 10.0)));
7391     assert(x.sliced.skewness!float.approxEqual((117.005859 / 12) / pow(54.765625 / 11, 1.5) * (12.0 ^^ 2) / (11.0 * 10.0)));
7392 }
7393 
7394 // Test skewness with values
7395 version(mir_stat_test)
7396 @safe pure nothrow
7397 unittest
7398 {
7399     import mir.math.common: approxEqual, pow;
7400     import mir.stat.transform: center;
7401 
7402     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
7403               2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
7404 
7405     assert(x.skewness.approxEqual(1.149008));
7406     assert(x.skewness(true).approxEqual(1.000083));
7407     assert(x.skewness!"naive".approxEqual(1.149008));
7408     assert(x.skewness!"naive"(true).approxEqual(1.000083));
7409     assert(x.skewness!"online".approxEqual(1.149008));
7410     assert(x.skewness!"online"(true).approxEqual(1.000083));
7411     assert(x.skewness!"twoPass".approxEqual(1.149008));
7412     assert(x.skewness!"twoPass"(true).approxEqual(1.000083));
7413     assert(x.skewness!"threePass".approxEqual(1.149008));
7414     assert(x.skewness!"threePass"(true).approxEqual(1.000083));
7415 
7416     auto y = x.center;
7417     assert(y.skewness!"assumeZeroMean".approxEqual(1.149008));
7418     assert(y.skewness!"assumeZeroMean"(true).approxEqual(1.000083));
7419 }
7420 
7421 // compile with dub test --build=unittest-perf --config=unittest-perf --compiler=ldc2
7422 version(mir_stat_test_skew_performance)
7423 unittest
7424 {
7425     import mir.math.sum: Summation;
7426     import mir.math.internal.benchmark;
7427     import std.stdio: writeln;
7428     import std.traits: EnumMembers;
7429 
7430     template staticMap(alias fun, alias S, args...)
7431     {
7432         import std.meta: AliasSeq;
7433         alias staticMap = AliasSeq!();
7434         static foreach (arg; args)
7435             staticMap = AliasSeq!(staticMap, fun!(double, arg, S));
7436     }
7437 
7438     size_t n = 10_000;
7439     size_t m = 1_000;
7440 
7441     alias S = Summation.fast;
7442     alias E = EnumMembers!SkewnessAlgo;
7443     alias fs = staticMap!(skewness, S, E);
7444     double[fs.length] output;
7445 
7446     auto e = [E];
7447     auto time = benchmarkRandom!(fs)(n, m, output);
7448     writeln("Skewness performance test");
7449     foreach (size_t i; 0 .. fs.length) {
7450         writeln("Function ", i + 1, ", Algo: ", e[i], ", Output: ", output[i], ", Elapsed time: ", time[i]);
7451     }
7452     writeln();
7453 }
7454 
7455 /++
7456 Kurtosis algorithms.
7457 
7458 See_also:
7459     $(WEB en.wikipedia.org/wiki/Kurtosis, Kurtosis),
7460     $(WEB en.wikipedia.org/wiki/Algorithms_for_calculating_variance, Algorithms for calculating variance)
7461 +/
7462 enum KurtosisAlgo
7463 {
7464     /++
7465     Similar to Welford's algorithm for updating variance, but adjusted for
7466     kurtosis. Can also `put` another KurtosisAccumulator of the same type, which
7467     uses the parallel algorithm from Terriberry that extends the work of Chan et
7468     al. 
7469     +/
7470     online,
7471     /++
7472     Calculates kurtosis using
7473     (E(x^^4) - 4 * E(x) * E(x ^^ 3) + 6 * (E(x) ^^ 2) E(X ^^ 2) + 3 E(x) ^^ 4) / sigma ^ 2 
7474     (allowing for adjustments for population/sample kurtosis). 
7475 
7476     This algorithm can be numerically unstable.
7477     +/
7478     naive,
7479 
7480     /++
7481     Calculates kurtosis by first calculating the mean, then calculating
7482     E((x - E(x)) ^^ 4) / (E((x - E(x)) ^^ 2) ^^ 2)
7483     +/
7484     twoPass,
7485 
7486     /++
7487     Calculates kurtosis by first calculating the mean, then the standard deviation, then calculating
7488     E(((x - E(x)) / (E((x - E(x)) ^^ 2) ^^ 0.5)) ^^ 4)
7489     +/
7490     threePass,
7491 
7492     /++
7493     Calculates kurtosis assuming the mean of the input is zero. 
7494     +/
7495     assumeZeroMean,
7496 
7497     /++
7498     When slices, slice-like objects, or ranges are the inputs, uses the two-pass
7499     algorithm. When an individual data-point is added, uses the online algorithm.
7500     +/
7501     hybrid
7502 }
7503 
7504 // Make sure skew algos and kurtosis algos match up
7505 version(mir_stat_test)
7506 @safe pure nothrow @nogc
7507 unittest
7508 {
7509     import std.conv: to;
7510     assert(SkewnessAlgo.online.to!int == KurtosisAlgo.online.to!int);
7511     assert(SkewnessAlgo.naive.to!int == KurtosisAlgo.naive.to!int);
7512     assert(SkewnessAlgo.twoPass.to!int == KurtosisAlgo.twoPass.to!int);
7513     assert(SkewnessAlgo.threePass.to!int == KurtosisAlgo.threePass.to!int);
7514     assert(SkewnessAlgo.assumeZeroMean.to!int == KurtosisAlgo.assumeZeroMean.to!int);
7515     assert(SkewnessAlgo.hybrid.to!int == KurtosisAlgo.hybrid.to!int);
7516 }
7517 
7518 ///
7519 struct KurtosisAccumulator(T, KurtosisAlgo kurtosisAlgo, Summation summation)
7520     if (isMutable!T && kurtosisAlgo == KurtosisAlgo.naive)
7521 {
7522     import mir.math.sum: Summator;
7523     import std.traits: isIterable;
7524 
7525     ///
7526     private MeanAccumulator!(T, summation) meanAccumulator;
7527     ///
7528     alias S = Summator!(T, summation);
7529     ///
7530     private S summatorOfSquares;
7531     ///
7532     private S summatorOfCubes;
7533     ///
7534     private S summatorOfQuarts;
7535 
7536     ///
7537     this(Range)(Range r)
7538         if (isIterable!Range)
7539     {
7540         import core.lifetime: move;
7541         this.put(r.move);
7542     }
7543 
7544     ///
7545     this()(T x)
7546     {
7547         this.put(x);
7548     }
7549 
7550     ///
7551     void put(Range)(Range r)
7552         if (isIterable!Range)
7553     {
7554         foreach(x; r)
7555         {
7556             this.put(x);
7557         }
7558     }
7559 
7560     ///
7561     void put()(T x)
7562     {
7563         meanAccumulator.put(x);
7564         T x2 = x * x;
7565         summatorOfSquares.put(x2);
7566         summatorOfCubes.put(x2 * x);
7567         summatorOfQuarts.put(x2 * x2);
7568     }
7569 
7570     ///
7571     void put(U, Summation sumAlgo)(KurtosisAccumulator!(U, kurtosisAlgo, sumAlgo) v)
7572     {
7573         meanAccumulator.put(v.meanAccumulator);
7574         summatorOfSquares.put(v.sumOfSquares!T);
7575         summatorOfCubes.put(v.sumOfCubes!T);
7576         summatorOfQuarts.put(v.sumOfQuarts!T);
7577     }
7578 
7579 const:
7580     ///
7581     size_t count()
7582     {
7583         return meanAccumulator.count;
7584     }
7585     ///
7586     F mean(F = T)()
7587     {
7588         return meanAccumulator.mean!F;
7589     }
7590     ///
7591     F variance(F = T)(bool isPopulation) @property
7592     in
7593     {
7594         assert(count > 1, "SkewnessAccumulator.varaince: count must be larger than one");
7595     }
7596     do
7597     {
7598         return centeredSumOfSquares!F / (count + isPopulation - 1);
7599     }
7600     ///
7601     F sumOfSquares(F = T)()
7602     {
7603         return cast(F) summatorOfSquares.sum;
7604     }
7605     ///
7606     F sumOfCubes(F = T)()
7607     {
7608         return cast(F) summatorOfCubes.sum;
7609     }
7610     ///
7611     F sumOfQuarts(F = T)()
7612     {
7613         return cast(F) summatorOfQuarts.sum;
7614     }
7615     ///
7616     F centeredSumOfSquares(F = T)()
7617     {
7618         return sumOfSquares!F - count * mean!F * mean!F;
7619     }
7620     ///
7621     F centeredSumOfCubes(F = T)()
7622     {
7623         F mu = mean!F;
7624         return sumOfCubes!F - 3 * mu * sumOfSquares!F + 2 * count * mu * mu * mu;
7625     }
7626     ///
7627     F centeredSumOfQuarts(F = T)()
7628     {
7629         F mu = mean!F;
7630         F mu2 = mu * mu;
7631         return sumOfQuarts!F - 4 * mu * sumOfCubes!F + 6 * mu2 * sumOfSquares!F - 3 * count * mu2 * mu2;
7632     }
7633     ///
7634     F scaledSumOfCubes(F = T)(bool isPopulation)
7635     {
7636         import mir.math.common: sqrt;
7637         F var = variance!F(isPopulation);
7638         return centeredSumOfCubes!F / (var * var.sqrt);
7639     }
7640     ///
7641     F scaledSumOfQuarts(F = T)(bool isPopulation)
7642     {
7643         F var = variance!F(isPopulation);
7644         return centeredSumOfQuarts!F / (var * var);
7645     }
7646     ///
7647     F skewness(F = T)(bool isPopulation)
7648     in
7649     {
7650         assert(count > 2, "SkewnessAccumulator.skewness: count must be larger than two");
7651         assert(centeredSumOfSquares > 0, "SkewnessAccumulator.skewness: variance must be larger than zero");
7652     }
7653     do
7654     {
7655         import mir.math.common: sqrt;
7656         F s = centeredSumOfSquares!F;
7657         return centeredSumOfCubes!F / (s * s.sqrt) * count * sqrt(cast(F) count + isPopulation - 1) /
7658             (count + 2 * isPopulation - 2);
7659         /+ Equivalent to
7660         return scaledSumOfCubes!F(isPopulation) / count *
7661                 (cast(F) count * count / ((count + isPopulation - 1) * (count + 2 * isPopulation - 2)));
7662         +/
7663     }
7664     ///
7665     F kurtosis(F = T)(bool isPopulation, bool isRaw)
7666     in
7667     {
7668         assert(count > 3, "KurtosisAccumulator.kurtosis: count must be larger than three");
7669         assert(variance(true) > 0, "KurtosisAccumulator.kurtosis: variance must be larger than zero");
7670     }
7671     do
7672     {
7673         F mult1 = cast(F) count * (count + isPopulation - 1) * (count - isPopulation + 1) / ((count + 2 * isPopulation - 2) * (count + 3 * isPopulation - 3));
7674         F mult2 = cast(F) (count + isPopulation - 1) * (count + isPopulation - 1) / ((count + 2 * isPopulation - 2) * (count + 3 * isPopulation - 3));
7675         F s = centeredSumOfSquares!F;
7676         return centeredSumOfQuarts!F / (s * s) * mult1 + 3 * (isRaw - mult2);
7677     }
7678 }
7679 
7680 /// naive
7681 version(mir_stat_test)
7682 @safe pure nothrow
7683 unittest
7684 {
7685     import mir.math.common: pow;
7686     import mir.ndslice.slice: sliced;
7687     import mir.test: shouldApprox;
7688 
7689     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
7690               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
7691 
7692     KurtosisAccumulator!(double, KurtosisAlgo.naive, Summation.naive) v;
7693     v.put(x);
7694 
7695     v.kurtosis(true, true).shouldApprox == (792.784119 / 12) / pow(54.765625 / 12, 2.0);
7696     v.kurtosis(true, false).shouldApprox == (792.784119 / 12) / pow(54.765625 / 12, 2.0) - 3;
7697     v.kurtosis(false, false).shouldApprox == (792.784119 / 12) / pow(54.765625 / 12, 2.0) * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0);
7698     v.kurtosis(false, true).shouldApprox == (792.784119 / 12) / pow(54.765625 / 12, 2.0) * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0) + 3;
7699 
7700     v.skewness(true).shouldApprox == (117.005859 / 12) / pow(54.765625 / 12, 1.5);
7701 
7702     v.put(4.0);
7703     v.kurtosis(true, true).shouldApprox == (745.608180 / 13) / pow(57.019231 / 13, 2.0);
7704     v.kurtosis(true, false).shouldApprox == (745.608180 / 13) / pow(57.019231 / 13, 2.0) - 3;
7705     v.kurtosis(false, false).shouldApprox == (745.608180 / 13) / pow(57.019231 / 13, 2.0) * (12.0 * 14.0) / (11.0 * 10.0) - 3.0 * (12.0 * 12.0) / (11.0 * 10.0);
7706     v.kurtosis(false, true).shouldApprox == (745.608180 / 13) / pow(57.019231 / 13, 2.0) * (12.0 * 14.0) / (11.0 * 10.0) - 3.0 * (12.0 * 12.0) / (11.0 * 10.0) + 3;
7707 
7708     v.skewness(true).shouldApprox == (100.238166 / 13) / pow(57.019231 / 13, 1.5);
7709 }
7710 
7711 // check two-dimensional
7712 version(mir_stat_test)
7713 @safe pure
7714 unittest
7715 {
7716     import mir.math.common: pow;
7717     import mir.math.sum: Summation;
7718     import mir.ndslice.fuse: fuse;
7719     import mir.test: shouldApprox;
7720 
7721     auto x = [[0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
7722               [2.0, 7.5, 5.0, 1.0, 1.5, 0.00]].fuse;
7723 
7724     KurtosisAccumulator!(double, KurtosisAlgo.naive, Summation.naive) v;
7725     v.put(x);
7726     v.kurtosis(true, true).shouldApprox == (792.784119 / 12) / pow(54.765625 / 12, 2.0);
7727 }
7728 
7729 // Can put KurtosisAccumulator
7730 version(mir_stat_test)
7731 @safe pure nothrow
7732 unittest
7733 {
7734     import mir.math.common: pow;
7735     import mir.ndslice.slice: sliced;
7736     import mir.test: shouldApprox;
7737 
7738     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
7739     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
7740 
7741     KurtosisAccumulator!(double, KurtosisAlgo.naive, Summation.naive) v;
7742     v.put(x);
7743     KurtosisAccumulator!(double, KurtosisAlgo.naive, Summation.naive) w;
7744     w.put(y);
7745     v.put(w);
7746     v.kurtosis(true, true).shouldApprox == (792.784119 / 12) / pow(54.765625 / 12, 2.0);
7747 }
7748 
7749 // Test input range
7750 version(mir_stat_test)
7751 @safe pure nothrow
7752 unittest
7753 {
7754     import mir.math.sum: Summation;
7755     import mir.test: shouldApprox;
7756     import std.range: iota;
7757     import std.algorithm: map;
7758 
7759     auto x1 = iota(0, 5);
7760     KurtosisAccumulator!(double, KurtosisAlgo.naive, Summation.naive) v1;
7761     v1.put(x1);
7762     v1.kurtosis(false, true).shouldApprox == 1.8;
7763     auto x2 = x1.map!(a => 2 * a);
7764     KurtosisAccumulator!(double, KurtosisAlgo.naive, Summation.naive) v2;
7765     v2.put(x2);
7766     v2.kurtosis(false, true).shouldApprox == 1.8;
7767 }
7768 
7769 // check scaledSumOfCubes/scaledSumOfQuarts/skewness
7770 version(mir_stat_test)
7771 @safe pure nothrow
7772 unittest
7773 {
7774     import mir.math.common: sqrt;
7775     import mir.math.sum: Summation;
7776     import mir.ndslice.slice: sliced;
7777     import mir.test: shouldApprox;
7778 
7779     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
7780               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
7781 
7782     KurtosisAccumulator!(double, KurtosisAlgo.naive, Summation.naive) v;
7783     v.put(x);
7784     auto varP = x.variance!"naive"(true);
7785     auto varS = x.variance!"naive"(false);
7786     v.scaledSumOfCubes(true).shouldApprox == v.centeredSumOfCubes / (varP * varP.sqrt);
7787     v.scaledSumOfCubes(false).shouldApprox == v.centeredSumOfCubes / (varS * varS.sqrt);
7788     v.scaledSumOfQuarts(true).shouldApprox == v.centeredSumOfQuarts / (varP * varP);
7789     v.scaledSumOfQuarts(false).shouldApprox == v.centeredSumOfQuarts / (varS * varS);
7790     v.skewness(true).shouldApprox == x.skewness!"naive"(true);
7791     v.skewness(false).shouldApprox == x.skewness!"naive"(false);
7792 }
7793 
7794 ///
7795 struct KurtosisAccumulator(T, KurtosisAlgo kurtosisAlgo, Summation summation)
7796     if (isMutable!T && kurtosisAlgo == KurtosisAlgo.online)
7797 {
7798     import mir.math.sum: Summator;
7799     import std.traits: isIterable;
7800 
7801     ///
7802     private MeanAccumulator!(T, summation) meanAccumulator;
7803     ///
7804     alias S = Summator!(T, summation);
7805     ///
7806     private S centeredSummatorOfSquares;
7807     ///
7808     private S centeredSummatorOfCubes;
7809     ///
7810     private S centeredSummatorOfQuarts;
7811 
7812     ///
7813     this(Range)(Range r)
7814         if (isIterable!Range)
7815     {
7816         import core.lifetime: move;
7817         this.put(r.move);
7818     }
7819 
7820     ///
7821     this()(T x)
7822     {
7823         this.put(x);
7824     }
7825 
7826     ///
7827     void put(Range)(Range r)
7828         if (isIterable!Range)
7829     {
7830         foreach(x; r)
7831         {
7832             this.put(x);
7833         }
7834     }
7835 
7836     ///
7837     void put()(T x)
7838     {
7839         T deltaOld = x;
7840         if (count > 0) {
7841             deltaOld -= mean;
7842         }
7843         meanAccumulator.put(x);
7844         T deltaNew = x - mean;
7845         centeredSummatorOfQuarts.put(deltaOld * deltaOld * deltaOld * deltaOld * ((count - 1) * (count * count - 3 * count + 3)) / (count * count * count) +
7846                                 6 * deltaOld * deltaOld * centeredSumOfSquares!T / (count * count) -
7847                                 4 * deltaOld * centeredSumOfCubes!T / count);
7848         centeredSummatorOfCubes.put(deltaOld * deltaOld * deltaOld * (count - 1) * (count - 2) / (count * count) -
7849                                3 * deltaOld * centeredSumOfSquares!T / count);
7850         centeredSummatorOfSquares.put(deltaOld * deltaNew);
7851     }
7852 
7853     ///
7854     void put(U, KurtosisAlgo kurtAlgo, Summation sumAlgo)(KurtosisAccumulator!(U, kurtAlgo, sumAlgo) v)
7855     {
7856         size_t oldCount = count;
7857         T delta = v.mean;
7858         if (oldCount > 0) {
7859             delta -= mean;
7860         }
7861         meanAccumulator.put!T(v.meanAccumulator);
7862         centeredSummatorOfQuarts.put(v.centeredSumOfQuarts!T + 
7863                                delta * delta * delta * delta * ((v.count * oldCount) * (oldCount * oldCount - v.count * oldCount + v.count * v.count)) / (count * count * count) +
7864                                6 * delta * delta * ((oldCount * oldCount) * v.centeredSumOfSquares!T + (v.count * v.count) * centeredSumOfSquares!T) / (count * count) +
7865                                4 * delta * (oldCount * v.centeredSumOfCubes!T - v.count * centeredSumOfCubes!T) / count);
7866         centeredSummatorOfCubes.put(v.centeredSumOfCubes!T + 
7867                                delta * delta * delta * v.count * oldCount * (oldCount - v.count) / (count * count) +
7868                                3 * delta * (oldCount * v.centeredSumOfSquares!T - v.count * centeredSumOfSquares!T) / count);
7869         centeredSummatorOfSquares.put(v.centeredSumOfSquares!T + delta * delta * v.count * oldCount / count);
7870     }
7871 
7872 const:
7873 
7874     ///
7875     size_t count()
7876     {
7877         return meanAccumulator.count;
7878     }
7879     ///
7880     F centeredSumOfQuarts(F = T)()
7881     {
7882         return cast(F) centeredSummatorOfQuarts.sum;
7883     }
7884     ///
7885     F centeredSumOfCubes(F = T)()
7886     {
7887         return cast(F) centeredSummatorOfCubes.sum;
7888     }
7889     ///
7890     F centeredSumOfSquares(F = T)()
7891     {
7892         return cast(F) centeredSummatorOfSquares.sum;
7893     }
7894     ///
7895     F scaledSumOfCubes(F = T)(bool isPopulation)
7896     {
7897         import mir.math.common: sqrt;
7898         F var = variance!F(isPopulation);
7899         return centeredSumOfCubes!F/ (var * var.sqrt);
7900     }
7901     ///
7902     F scaledSumOfQuarts(F = T)(bool isPopulation)
7903     {
7904         F var = variance!F(isPopulation);
7905         return centeredSumOfQuarts!F/ (var * var);
7906     }
7907     ///
7908     F mean(F = T)()
7909     {
7910         return meanAccumulator.mean!F;
7911     }
7912     ///
7913     F variance(F = T)(bool isPopulation)
7914     in
7915     {
7916         assert(count > 1, "KurtosisAccumulator.variance: count must be larger than one");
7917     }
7918     do
7919     {
7920         return centeredSumOfSquares!F / (count + isPopulation - 1);
7921     }
7922     ///
7923     F skewness(F = T)(bool isPopulation)
7924     in
7925     {
7926         assert(count > 2, "SkewnessAccumulator.skewness: count must be larger than two");
7927         assert(centeredSummatorOfSquares.sum > 0, "SkewnessAccumulator.skewness: variance must be larger than zero");
7928     }
7929     do
7930     {
7931         import mir.math.common: sqrt;
7932         F s = centeredSumOfSquares!F;
7933         return centeredSumOfCubes!F / (s * s.sqrt) * count * sqrt(cast(F) count + isPopulation - 1) /
7934             (count + 2 * isPopulation - 2);
7935         /+ Equivalent to
7936         return scaledSumOfCubes!F(isPopulation) / count *
7937                 (cast(F) count * count / ((count + isPopulation - 1) * (count + 2 * isPopulation - 2)));
7938         +/
7939     }
7940     ///
7941     F kurtosis(F = T)(bool isPopulation, bool isRaw)
7942     in
7943     {
7944         assert(count > 3, "KurtosisAccumulator.kurtosis: count must be larger than three");
7945         assert(variance(true) > 0, "KurtosisAccumulator.kurtosis: variance must be larger than zero");
7946     }
7947     do
7948     {
7949         F mult1 = cast(F) count * (count + isPopulation - 1) * (count - isPopulation + 1) / ((count + 2 * isPopulation - 2) * (count + 3 * isPopulation - 3));
7950         F mult2 = cast(F) (count + isPopulation - 1) * (count + isPopulation - 1) / ((count + 2 * isPopulation - 2) * (count + 3 * isPopulation - 3));
7951         F s = centeredSumOfSquares!F;
7952         return centeredSumOfQuarts!F / (s * s) * mult1 + 3 * (isRaw - mult2);
7953     }
7954 }
7955 
7956 /// online
7957 version(mir_stat_test)
7958 @safe pure nothrow
7959 unittest
7960 {
7961     import mir.math.common: approxEqual, pow;
7962     import mir.ndslice.slice: sliced;
7963     import mir.test: shouldApprox;
7964 
7965     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
7966               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
7967 
7968     KurtosisAccumulator!(double, KurtosisAlgo.online, Summation.naive) v;
7969     v.put(x);
7970     v.kurtosis(true, true).shouldApprox == (792.784119 / 12) / pow(54.765625 / 12, 2.0);
7971     v.kurtosis(true, false).shouldApprox == (792.784119 / 12) / pow(54.765625 / 12, 2.0) - 3;
7972     v.kurtosis(false, false).shouldApprox == (792.784119 / 12) / pow(54.765625 / 12, 2.0) * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0);
7973     v.kurtosis(false, true).shouldApprox == (792.784119 / 12) / pow(54.765625 / 12, 2.0) * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0) + 3;
7974 
7975     v.put(4.0);
7976     v.kurtosis(true, true).shouldApprox == (745.608180 / 13) / pow(57.019231 / 13, 2.0);
7977     v.kurtosis(true, false).shouldApprox == (745.608180 / 13) / pow(57.019231 / 13, 2.0) - 3;
7978     v.kurtosis(false, false).shouldApprox == (745.608180 / 13) / pow(57.019231 / 13, 2.0) * (12.0 * 14.0) / (11.0 * 10.0) - 3.0 * (12.0 * 12.0) / (11.0 * 10.0);
7979     v.kurtosis(false, true).shouldApprox == (745.608180 / 13) / pow(57.019231 / 13, 2.0) * (12.0 * 14.0) / (11.0 * 10.0) - 3.0 * (12.0 * 12.0) / (11.0 * 10.0) + 3;
7980 }
7981 
7982 // Can put slice
7983 version(mir_stat_test)
7984 @safe pure nothrow
7985 unittest
7986 {
7987     import mir.math.common: approxEqual;
7988     import mir.ndslice.slice: sliced;
7989 
7990     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
7991     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
7992 
7993     KurtosisAccumulator!(double, KurtosisAlgo.online, Summation.naive) v;
7994     v.put(x);
7995     assert(v.centeredSumOfQuarts.approxEqual(46.944607));
7996     assert(v.centeredSumOfSquares.approxEqual(12.552083));
7997 
7998     v.put(y);
7999     assert(v.centeredSumOfQuarts.approxEqual(792.784119));
8000     assert(v.centeredSumOfSquares.approxEqual(54.765625));
8001 }
8002 
8003 // Can put KurtosisAccumulator
8004 version(mir_stat_test)
8005 @safe pure nothrow
8006 unittest
8007 {
8008     import mir.math.common: approxEqual;
8009     import mir.ndslice.slice: sliced;
8010 
8011     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
8012     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
8013 
8014     KurtosisAccumulator!(double, KurtosisAlgo.online, Summation.naive) v;
8015     v.put(x);
8016     assert(v.centeredSumOfQuarts.approxEqual(46.944607));
8017     assert(v.centeredSumOfSquares.approxEqual(12.552083));
8018 
8019     KurtosisAccumulator!(double, KurtosisAlgo.online, Summation.naive) w;
8020     w.put(y);
8021     v.put(w);
8022     assert(v.centeredSumOfQuarts.approxEqual(792.784119));
8023     assert(v.centeredSumOfSquares.approxEqual(54.765625));
8024 }
8025 
8026 // Can put KurtosisAccumulator (naive)
8027 version(mir_stat_test)
8028 @safe pure nothrow
8029 unittest
8030 {
8031     import mir.math.common: approxEqual;
8032     import mir.ndslice.slice: sliced;
8033 
8034     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
8035     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
8036 
8037     KurtosisAccumulator!(double, KurtosisAlgo.online, Summation.naive) v;
8038     v.put(x);
8039     assert(v.centeredSumOfQuarts.approxEqual(46.944607));
8040     assert(v.centeredSumOfSquares.approxEqual(12.552083));
8041 
8042     KurtosisAccumulator!(double, KurtosisAlgo.naive, Summation.naive) w;
8043     w.put(y);
8044     v.put(w);
8045     assert(v.centeredSumOfQuarts.approxEqual(792.784119));
8046     assert(v.centeredSumOfSquares.approxEqual(54.765625));
8047 }
8048 
8049 // Can put KurtosisAccumulator (twoPass)
8050 version(mir_stat_test)
8051 @safe pure nothrow
8052 unittest
8053 {
8054     import mir.math.common: approxEqual;
8055     import mir.ndslice.slice: sliced;
8056 
8057     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
8058     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
8059 
8060     KurtosisAccumulator!(double, KurtosisAlgo.online, Summation.naive) v;
8061     v.put(x);
8062     assert(v.centeredSumOfQuarts.approxEqual(46.944607));
8063     assert(v.centeredSumOfSquares.approxEqual(12.552083));
8064 
8065     auto w = KurtosisAccumulator!(double, KurtosisAlgo.twoPass, Summation.naive)(y);
8066     v.put(w);
8067     assert(v.centeredSumOfQuarts.approxEqual(792.784119));
8068     assert(v.centeredSumOfSquares.approxEqual(54.765625));
8069 }
8070 
8071 // Can put KurtosisAccumulator (threePass)
8072 version(mir_stat_test)
8073 @safe pure nothrow
8074 unittest
8075 {
8076     import mir.math.common: approxEqual;
8077     import mir.ndslice.slice: sliced;
8078 
8079     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
8080     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
8081 
8082     KurtosisAccumulator!(double, KurtosisAlgo.online, Summation.naive) v;
8083     v.put(x);
8084     assert(v.centeredSumOfQuarts.approxEqual(46.944607));
8085     assert(v.centeredSumOfSquares.approxEqual(12.552083));
8086 
8087     auto w = KurtosisAccumulator!(double, KurtosisAlgo.threePass, Summation.naive)(y);
8088     v.put(w);
8089     assert(v.centeredSumOfQuarts.approxEqual(792.784119));
8090     assert(v.centeredSumOfSquares.approxEqual(54.765625));
8091 }
8092 
8093 // Can put KurtosisAccumulator (assumeZeroMean)
8094 version(mir_stat_test)
8095 @safe pure nothrow
8096 unittest
8097 {
8098     import mir.math.common: approxEqual;
8099     import mir.ndslice.slice: sliced;
8100     import mir.stat.transform: center;
8101 
8102     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
8103     auto b = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
8104     auto x = a.center;
8105     auto y = b.center;
8106 
8107     KurtosisAccumulator!(double, KurtosisAlgo.online, Summation.naive) v;
8108     v.put(x);
8109     KurtosisAccumulator!(double, KurtosisAlgo.assumeZeroMean, Summation.naive) w;
8110     w.put(y);
8111     v.put(w);
8112     assert(v.centeredSumOfQuarts.approxEqual(622.639052)); //note: different from above due to inconsistent centering
8113     assert(v.centeredSumOfSquares.approxEqual(52.885417)); //note: different from above due to inconsistent centering
8114 }
8115 
8116 // check scaledSumOfCubes/scaledSumOfQuarts/skewness
8117 version(mir_stat_test)
8118 @safe pure nothrow
8119 unittest
8120 {
8121     import mir.math.common: sqrt;
8122     import mir.math.sum: Summation;
8123     import mir.ndslice.slice: sliced;
8124     import mir.test: shouldApprox;
8125 
8126     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
8127               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
8128 
8129     KurtosisAccumulator!(double, KurtosisAlgo.online, Summation.naive) v;
8130     v.put(x);
8131     auto varP = x.variance!"online"(true);
8132     auto varS = x.variance!"online"(false);
8133     v.scaledSumOfCubes(true).shouldApprox == v.centeredSumOfCubes / (varP * varP.sqrt);
8134     v.scaledSumOfCubes(false).shouldApprox == v.centeredSumOfCubes / (varS * varS.sqrt);
8135     v.scaledSumOfQuarts(true).shouldApprox == v.centeredSumOfQuarts / (varP * varP);
8136     v.scaledSumOfQuarts(false).shouldApprox == v.centeredSumOfQuarts / (varS * varS);
8137     v.skewness(true).shouldApprox == x.skewness!"online"(true);
8138     v.skewness(false).shouldApprox == x.skewness!"online"(false);
8139 }
8140 
8141 ///
8142 struct KurtosisAccumulator(T, KurtosisAlgo kurtosisAlgo, Summation summation)
8143     if (isMutable!T && kurtosisAlgo == KurtosisAlgo.twoPass)
8144 {
8145     import mir.math.sum: elementType, Summator;
8146     import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind;
8147     import std.range: isInputRange;
8148 
8149     ///
8150     private MeanAccumulator!(T, summation) meanAccumulator;
8151     ///
8152     alias S = Summator!(T, summation);
8153     ///
8154     private S centeredSummatorOfSquares;
8155     ///
8156     private S centeredSummatorOfCubes; // only included to facilitate adding to online
8157     ///
8158     private S centeredSummatorOfQuarts;
8159 
8160     ///
8161     this(Iterator, size_t N, SliceKind kind)(Slice!(Iterator, N, kind) slice)
8162     {
8163         import mir.functional: naryFun;
8164         import mir.ndslice.topology: vmap, map;
8165         import mir.ndslice.internal: LeftOp;
8166 
8167         meanAccumulator.put(slice.lightScope);
8168 
8169         auto sliceMap = slice.vmap(LeftOp!("-", T)(mean)).map!(naryFun!"a * a", naryFun!"(a * a) * a", naryFun!"(a * a) * (a * a)");
8170         centeredSummatorOfSquares.put(sliceMap.map!"a[0]");
8171         centeredSummatorOfCubes.put(sliceMap.map!"a[1]");
8172         centeredSummatorOfQuarts.put(sliceMap.map!"a[2]");
8173     }
8174 
8175     ///
8176     this(SliceLike)(SliceLike x)
8177         if (isConvertibleToSlice!SliceLike && !isSlice!SliceLike)
8178     {
8179         import mir.ndslice.slice: toSlice;
8180         this(x.toSlice);
8181     }
8182 
8183     ///
8184     this(Range)(Range range)
8185         if (isInputRange!Range && !isConvertibleToSlice!Range && is(elementType!Range : T))
8186     {
8187         import std.algorithm: map;
8188         meanAccumulator.put(range);
8189 
8190         auto centeredRangeMultiplier = range.map!(a => (a - mean)).map!("a * a", "a * a * a", "a * a * a * a");
8191         centeredSummatorOfSquares.put(centeredRangeMultiplier.map!"a[0]");
8192         centeredSummatorOfCubes.put(centeredRangeMultiplier.map!"a[1]");
8193         centeredSummatorOfQuarts.put(centeredRangeMultiplier.map!"a[2]");
8194     }
8195 
8196 const:
8197 
8198     ///
8199     size_t count()()
8200     {
8201         return meanAccumulator.count;
8202     }
8203     ///
8204     F mean(F = T)()
8205     {
8206         return meanAccumulator.mean!F;
8207     }
8208     ///
8209     F variance(F = T)(bool isPopulation)
8210     in
8211     {
8212         assert(count > 1, "SkewnessAccumulator.variance: count must be larger than 1");
8213     }
8214     do
8215     {
8216         return centeredSumOfSquares!F / (count + isPopulation - 1);
8217     }
8218     ///
8219     F centeredSumOfSquares(F = T)()
8220     {
8221         return cast(F) centeredSummatorOfSquares.sum;
8222     }
8223     ///
8224     F centeredSumOfCubes(F = T)()
8225     {
8226         return cast(F) centeredSummatorOfCubes.sum;
8227     }
8228     ///
8229     F centeredSumOfQuarts(F = T)()
8230     {
8231         return cast(F) centeredSummatorOfQuarts.sum;
8232     }
8233     ///
8234     F scaledSumOfCubes(F = T)(bool isPopulation)
8235     {
8236         import mir.math.common: sqrt;
8237         auto var = variance!F(isPopulation);
8238         return centeredSumOfCubes!F / (var * var.sqrt);
8239     }
8240     ///
8241     F scaledSumOfQuarts(F = T)(bool isPopulation)
8242     {
8243         auto var = variance!F(isPopulation);
8244         return centeredSumOfQuarts!F / (var * var);
8245     }
8246     ///
8247     F skewness(F = T)(bool isPopulation)
8248     in
8249     {
8250         assert(count > 2, "KurtosisAccumulator.skewness: count must be larger than two");
8251         assert(centeredSumOfSquares > 0, "KurtosisAccumulator.skewness: variance must be larger than zero");
8252     }
8253     do
8254     {
8255         import mir.math.common: sqrt;
8256         F s = centeredSumOfSquares!F;
8257         return centeredSumOfCubes!F / (s * s.sqrt) * count * sqrt(cast(F) count + isPopulation - 1) /
8258             (count + 2 * isPopulation - 2);
8259         /+ Equivalent to
8260         return scaledSumOfCubes!F(isPopulation) / count *
8261                 (cast(F) count * count / ((count + isPopulation - 1) * (count + 2 * isPopulation - 2)));
8262         +/
8263     }
8264     ///
8265     F kurtosis(F = T)(bool isPopulation, bool isRaw)
8266     in
8267     {
8268         assert(count > 3, "KurtosisAccumulator.kurtosis: count must be larger than three");
8269     }
8270     do
8271     {
8272         F mult1 = cast(F) count * (count + isPopulation - 1) * (count - isPopulation + 1) / ((count + 2 * isPopulation - 2) * (count + 3 * isPopulation - 3));
8273         F mult2 = cast(F) (count + isPopulation - 1) * (count + isPopulation - 1) / ((count + 2 * isPopulation - 2) * (count + 3 * isPopulation - 3));
8274         F s = centeredSumOfSquares!F;
8275         return centeredSumOfQuarts!F / (s * s) * mult1 + 3 * (isRaw - mult2);
8276     }  
8277 }
8278 
8279 /// twoPass
8280 version(mir_stat_test)
8281 @safe pure nothrow
8282 unittest
8283 {
8284     import mir.math.common: approxEqual;
8285     import mir.ndslice.slice: sliced;
8286 
8287     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
8288               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
8289 
8290     auto v = KurtosisAccumulator!(double, KurtosisAlgo.twoPass, Summation.naive)(x);
8291     assert(v.kurtosis(true, true).approxEqual(38.062853 / 12));
8292     assert(v.kurtosis(true, false).approxEqual(38.062853 / 12 - 3.0));
8293     assert(v.kurtosis(false, true).approxEqual(38.062853 / 12 * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)) + 3.0);
8294     assert(v.kurtosis(false, false).approxEqual(38.062853 / 12 * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)));
8295 }
8296 
8297 // check withAsSlice
8298 version(mir_stat_test)
8299 @safe pure nothrow @nogc
8300 unittest
8301 {
8302     import mir.math.sum: Summation;
8303     import mir.rc.array: RCArray;
8304     import mir.test: shouldApprox;
8305 
8306     static immutable a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
8307                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
8308 
8309     auto x = RCArray!double(12);
8310     foreach(i, ref e; x)
8311         e = a[i];
8312 
8313     auto v = KurtosisAccumulator!(double, KurtosisAlgo.twoPass, Summation.naive)(x);
8314     v.scaledSumOfQuarts(true).shouldApprox == 38.062853;
8315 }
8316 
8317 // check dynamic slice
8318 version(mir_stat_test)
8319 @safe pure nothrow
8320 unittest
8321 {
8322     import mir.math.sum: Summation;
8323     import mir.test: shouldApprox;
8324 
8325     double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
8326                   2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
8327 
8328     auto v = KurtosisAccumulator!(double, KurtosisAlgo.twoPass, Summation.naive)(x);
8329     v.scaledSumOfQuarts(true).shouldApprox == 38.062853;
8330 }
8331 
8332 // Test input range
8333 version(mir_stat_test)
8334 @safe pure nothrow
8335 unittest
8336 {
8337     import mir.math.sum: Summation;
8338     import mir.test: shouldApprox;
8339     import std.range: iota;
8340     import std.algorithm: map;
8341 
8342     auto x1 = iota(0, 5);
8343     auto v1 = KurtosisAccumulator!(double, KurtosisAlgo.twoPass, Summation.naive)(x1);
8344     v1.kurtosis(false, true).shouldApprox == 1.8;
8345     auto x2 = x1.map!(a => 2 * a);
8346     auto v2 = KurtosisAccumulator!(double, KurtosisAlgo.twoPass, Summation.naive)(x2);
8347     v2.kurtosis(false, true).shouldApprox == 1.8;
8348 }
8349 
8350 // check scaledSumOfCubes/scaledSumOfQuarts/skewness
8351 version(mir_stat_test)
8352 @safe pure nothrow
8353 unittest
8354 {
8355     import mir.math.common: sqrt;
8356     import mir.math.sum: Summation;
8357     import mir.ndslice.slice: sliced;
8358     import mir.test: shouldApprox;
8359 
8360     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
8361               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
8362 
8363     auto v = KurtosisAccumulator!(double, KurtosisAlgo.twoPass, Summation.naive)(x);
8364     auto varP = x.variance!"twoPass"(true);
8365     auto varS = x.variance!"twoPass"(false);
8366     v.scaledSumOfCubes(true).shouldApprox == v.centeredSumOfCubes / (varP * varP.sqrt);
8367     v.scaledSumOfCubes(false).shouldApprox == v.centeredSumOfCubes / (varS * varS.sqrt);
8368     v.scaledSumOfQuarts(true).shouldApprox == v.centeredSumOfQuarts / (varP * varP);
8369     v.scaledSumOfQuarts(false).shouldApprox == v.centeredSumOfQuarts / (varS * varS);
8370     v.skewness(true).shouldApprox == x.skewness!"twoPass"(true);
8371     v.skewness(false).shouldApprox == x.skewness!"twoPass"(false);
8372 }
8373 
8374 ///
8375 struct KurtosisAccumulator(T, KurtosisAlgo kurtosisAlgo, Summation summation)
8376     if (isMutable!T && kurtosisAlgo == KurtosisAlgo.threePass)
8377 {
8378     import mir.math.sum: elementType, Summator;
8379     import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind;
8380     import std.range: isInputRange;
8381 
8382     ///
8383     private MeanAccumulator!(T, summation) meanAccumulator;
8384     ///
8385     alias S = Summator!(T, summation);
8386     ///
8387     private S centeredSummatorOfSquares;
8388     ///
8389     private S scaledSummatorOfCubes; //only included to facilitate adding to online accumulator
8390     ///
8391     private S scaledSummatorOfQuarts;
8392 
8393     ///
8394     this(Iterator, size_t N, SliceKind kind)(Slice!(Iterator, N, kind) slice)
8395     {
8396         import mir.functional: naryFun;
8397         import mir.ndslice.topology: vmap, map;
8398         import mir.ndslice.internal: LeftOp;
8399         import mir.math.common: sqrt;
8400 
8401         meanAccumulator.put(slice.lightScope);
8402         auto centeredSlice = slice.vmap(LeftOp!("-", T)(mean));
8403         centeredSummatorOfSquares.put(centeredSlice.map!(naryFun!"a * a"));
8404 
8405         assert(variance(true) > 0, "KurtosisAccumulator.this: must divide by positive standard deviation");
8406 
8407         auto sliceMap = centeredSlice.
8408             vmap(LeftOp!("*", T)(1 / variance(true).sqrt)).
8409             map!(naryFun!"(a * a) * a", naryFun!"(a * a) * (a * a)");
8410         scaledSummatorOfCubes.put(sliceMap.map!"a[0]");
8411         scaledSummatorOfQuarts.put(sliceMap.map!"a[1]");
8412     }
8413 
8414     ///
8415     this(SliceLike)(SliceLike x)
8416         if (isConvertibleToSlice!SliceLike && !isSlice!SliceLike)
8417     {
8418         import mir.ndslice.slice: toSlice;
8419         this(x.toSlice);
8420     }
8421 
8422     ///
8423     this(Range)(Range range)
8424         if (isInputRange!Range && !isConvertibleToSlice!Range && is(elementType!Range : T))
8425     {
8426         import mir.math.common: sqrt;
8427         import std.algorithm: map;
8428 
8429         meanAccumulator.put(range);
8430         auto centeredRange = range.map!(a => (a - mean));
8431         centeredSummatorOfSquares.put(centeredRange.map!"a * a");
8432         auto rangeMap = centeredRange.
8433             map!(a => a / variance(true).sqrt).
8434             map!("(a * a) * a", "(a * a) * (a * a)");
8435         scaledSummatorOfCubes.put(rangeMap.map!"a[0]");
8436         scaledSummatorOfQuarts.put(rangeMap.map!"a[1]");
8437     }
8438 
8439 const:
8440 
8441     ///
8442     size_t count()()
8443     {
8444         return meanAccumulator.count;
8445     }
8446     ///
8447     F mean(F = T)()
8448     {
8449         return meanAccumulator.mean!F;
8450     }
8451     ///
8452     F variance(F = T)(bool isPopulation)
8453     in
8454     {
8455         assert(count > 1, "SkewnessAccumulator.variance: count must be larger than 1");
8456     }
8457     do
8458     {
8459         return centeredSumOfSquares!F / (count + isPopulation - 1);
8460     }
8461     ///
8462     F centeredSumOfSquares(F = T)()
8463     {
8464         return cast(F) centeredSummatorOfSquares.sum;
8465     }
8466     ///
8467     F centeredSumOfCubes(F = T)()
8468     {
8469         import mir.math.common: sqrt;
8470         // variance consistent with that used for scaledSumOfQuarts above
8471         auto varP = variance!F(true);
8472         return scaledSumOfCubes!F * varP * varP.sqrt;
8473     }
8474     ///
8475     F centeredSumOfQuarts(F = T)()
8476     {
8477         // variance consistent with that used for scaledSumOfQuarts above
8478         auto varP = variance!F(true);
8479         return scaledSumOfQuarts!F * varP * varP;
8480     }
8481     ///
8482     F scaledSumOfCubes(F = T)()
8483     {
8484         return cast(F) scaledSummatorOfCubes.sum;
8485     }
8486     ///
8487     F scaledSumOfQuarts(F = T)()
8488     {
8489         return cast(F) scaledSummatorOfQuarts.sum;
8490     }
8491     ///
8492     F scaledSumOfCubes(F = T)(bool isPopulation)
8493     {
8494         import mir.math.common: sqrt;
8495         return scaledSumOfCubes!F * (count + isPopulation - 1) * sqrt(cast(F) count + isPopulation - 1) / count / sqrt(cast(F) count);
8496     }
8497     ///
8498     F scaledSumOfQuarts(F = T)(bool isPopulation)
8499     {
8500         return scaledSumOfQuarts!F * (count + isPopulation - 1) * (count + isPopulation - 1) / cast(F) count / cast(F) count;
8501     }
8502     ///
8503     F skewness(F = T)(bool isPopulation)
8504     in
8505     {
8506         assert(count > 2, "KurtosisAccumulator.skewness: count must be larger than two");
8507     }
8508     do
8509     {
8510         // formula for other kurtosis accumulators doesn't work here since we are
8511         // enforcing the the scaledSumOfCubes uses population variance and not that it can switch
8512         import mir.math.common: sqrt;
8513         return scaledSumOfCubes!F / (count + 2 * isPopulation - 2) *
8514                 sqrt(cast(F) (count + isPopulation - 1) / count);
8515         /+ Equivalent to
8516         return scaledSumOfCubes!F / count * 
8517                 sqrt(cast(F) count * (count + isPopulation - 1)) / (count + 2 * isPopulation - 2)
8518         +/
8519     }
8520     ///
8521     F kurtosis(F = T)(bool isPopulation, bool isRaw)
8522     in
8523     {
8524         assert(count > 3, "KurtosisAccumulator.kurtosis: count must be larger than three");
8525     }
8526     do
8527     {
8528         // formula for other kurtosis accumulators doesn't work here since we are
8529         // enforcing the scaling uses population variance and not that it can switch
8530         F mult1 = cast(F) (count + isPopulation - 1) * (count - isPopulation + 1) / (count * (count + 2 * isPopulation - 2) * (count + 3 * isPopulation - 3));
8531         F mult2 = cast(F) (count + isPopulation - 1) * (count + isPopulation - 1) / ((count + 2 * isPopulation - 2) * (count + 3 * isPopulation - 3));
8532 
8533         return scaledSumOfQuarts!F * mult1 + 3 * (isRaw - mult2);
8534     }  
8535 }
8536 
8537 /// threePass
8538 version(mir_stat_test)
8539 @safe pure nothrow
8540 unittest
8541 {
8542     import mir.math.common: approxEqual;
8543     import mir.ndslice.slice: sliced;
8544 
8545     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
8546               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
8547 
8548     auto v = KurtosisAccumulator!(double, KurtosisAlgo.threePass, Summation.naive)(x);
8549     assert(v.kurtosis(true, true).approxEqual(38.062853 / 12));
8550     assert(v.kurtosis(true, false).approxEqual(38.062853 / 12 - 3.0));
8551     assert(v.kurtosis(false, true).approxEqual(38.062853 / 12 * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)) + 3.0);
8552     assert(v.kurtosis(false, false).approxEqual(38.062853 / 12 * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)));
8553 }
8554 
8555 // check withAsSlice
8556 version(mir_stat_test)
8557 @safe pure nothrow @nogc
8558 unittest
8559 {
8560     import mir.math.common: approxEqual, sqrt;
8561     import mir.math.sum: Summation;
8562     import mir.rc.array: RCArray;
8563 
8564     static immutable a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
8565                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
8566 
8567     auto x = RCArray!double(12);
8568     foreach(i, ref e; x)
8569         e = a[i];
8570 
8571     auto v = KurtosisAccumulator!(double, KurtosisAlgo.threePass, Summation.naive)(x);
8572     assert(v.scaledSumOfQuarts.approxEqual(38.062853));
8573 }
8574 
8575 // check dynamic slice
8576 version(mir_stat_test)
8577 @safe pure nothrow
8578 unittest
8579 {
8580     import mir.math.common: approxEqual, sqrt;
8581     import mir.math.sum: Summation;
8582 
8583     double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
8584                   2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
8585 
8586     auto v = KurtosisAccumulator!(double, KurtosisAlgo.threePass, Summation.naive)(x);
8587     assert(v.scaledSumOfQuarts.approxEqual(38.062853));
8588 }
8589 
8590 // Test input range
8591 version(mir_stat_test)
8592 @safe pure nothrow
8593 unittest
8594 {
8595     import mir.math.sum: Summation;
8596     import mir.test: shouldApprox;
8597     import std.range: iota;
8598     import std.algorithm: map;
8599 
8600     auto x1 = iota(0, 5);
8601     auto v1 = KurtosisAccumulator!(double, KurtosisAlgo.threePass, Summation.naive)(x1);
8602     v1.kurtosis(false, true).shouldApprox == 1.8;
8603     auto x2 = x1.map!(a => 2 * a);
8604     auto v2 = KurtosisAccumulator!(double, KurtosisAlgo.threePass, Summation.naive)(x2);
8605     v2.kurtosis(false, true).shouldApprox == 1.8;
8606 }
8607 
8608 // check scaledSumOfCubes/scaledSumOfQuarts/skewness
8609 version(mir_stat_test)
8610 @safe pure nothrow
8611 unittest
8612 {
8613     import mir.math.common: sqrt;
8614     import mir.math.sum: Summation;
8615     import mir.ndslice.slice: sliced;
8616     import mir.test: shouldApprox;
8617 
8618     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
8619               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
8620 
8621     auto v = KurtosisAccumulator!(double, KurtosisAlgo.threePass, Summation.naive)(x);
8622     auto varP = x.variance!"twoPass"(true);
8623     auto varS = x.variance!"twoPass"(false);
8624     v.scaledSumOfCubes(true).shouldApprox == v.centeredSumOfCubes / (varP * varP.sqrt);
8625     v.scaledSumOfCubes(false).shouldApprox == v.centeredSumOfCubes / (varS * varS.sqrt);
8626     v.scaledSumOfQuarts(true).shouldApprox == v.centeredSumOfQuarts / (varP * varP);
8627     v.scaledSumOfQuarts(false).shouldApprox == v.centeredSumOfQuarts / (varS * varS);
8628     v.skewness(true).shouldApprox == x.skewness!"threePass"(true);
8629     v.skewness(false).shouldApprox == x.skewness!"threePass"(false);
8630 }
8631 
8632 ///
8633 struct KurtosisAccumulator(T, KurtosisAlgo kurtosisAlgo, Summation summation)
8634     if (isMutable!T && kurtosisAlgo == KurtosisAlgo.assumeZeroMean)
8635 {
8636     import mir.math.sum: Summator;
8637     import std.traits: isIterable;
8638 
8639     ///
8640     private size_t _count;
8641     ///
8642     alias S = Summator!(T, summation);
8643     ///
8644     private S centeredSummatorOfSquares;
8645     ///
8646     private S centeredSummatorOfCubes;
8647     ///
8648     private S centeredSummatorOfQuarts;
8649 
8650     ///
8651     this(Range)(Range r)
8652         if (isIterable!Range)
8653     {
8654         this.put(r);
8655     }
8656 
8657     ///
8658     this()(T x)
8659     {
8660         this.put(x);
8661     }
8662 
8663     ///
8664     void put(Range)(Range r)
8665         if (isIterable!Range)
8666     {
8667         foreach(x; r)
8668         {
8669             this.put(x);
8670         }
8671     }
8672 
8673     ///
8674     void put()(T x)
8675     {
8676         _count++;
8677         T x2 = x * x;
8678         centeredSummatorOfSquares.put(x2);
8679         centeredSummatorOfCubes.put(x2 * x);
8680         centeredSummatorOfQuarts.put(x2 * x2);
8681     }
8682 
8683     ///
8684     void put(U, Summation sumAlgo)(KurtosisAccumulator!(U, kurtosisAlgo, sumAlgo) v)
8685     {
8686         _count += v.count;
8687         centeredSummatorOfSquares.put(v.centeredSumOfSquares!T);
8688         centeredSummatorOfCubes.put(v.centeredSumOfCubes!T);
8689         centeredSummatorOfQuarts.put(v.centeredSumOfQuarts!T);
8690     }
8691 
8692 const:
8693 
8694     ///
8695     size_t count() @property
8696     {
8697         return _count;
8698     }
8699     ///
8700     F mean(F = T)() @property
8701     {
8702         return cast(F) 0;
8703     }
8704     MeanAccumulator!(T, summation) meanAccumulator()()
8705     {
8706         typeof(return) m = { _count, T(0) };
8707         return m;
8708     }
8709     ///
8710     F variance(F = T)(bool isPopulation) @property
8711     in
8712     {
8713         assert(count > 1, "KurtosisAccumulator.variance: count must be larger than one");
8714     }
8715     do
8716     {
8717         return centeredSumOfSquares!F / (count + isPopulation - 1);
8718     }
8719     ///
8720     F centeredSumOfQuarts(F = T)() @property
8721     {
8722         return cast(F) centeredSummatorOfQuarts.sum;
8723     }
8724     ///
8725     F centeredSumOfCubes(F = T)() @property
8726     {
8727         return cast(F) centeredSummatorOfCubes.sum;
8728     }
8729     ///
8730     F centeredSumOfSquares(F = T)() @property
8731     {
8732         return cast(F) centeredSummatorOfSquares.sum;
8733     }
8734     ///
8735     F scaledSumOfCubes(F = T)(bool isPopulation)
8736     {
8737         import mir.math.common: sqrt;
8738         F var = variance!F(isPopulation);
8739         return centeredSumOfCubes!F/ (var * var.sqrt);
8740     }
8741     ///
8742     F scaledSumOfQuarts(F = T)(bool isPopulation)
8743     {
8744         F var = variance!F(isPopulation);
8745         return centeredSumOfQuarts!F/ (var * var);
8746     }
8747     ///
8748     F skewness(F = T)(bool isPopulation)
8749     in
8750     {
8751         assert(count > 2, "SkewnessAccumulator.skewness: count must be larger than two");
8752         assert(centeredSummatorOfSquares.sum > 0, "SkewnessAccumulator.skewness: variance must be larger than zero");
8753     }
8754     do
8755     {
8756         import mir.math.common: sqrt;
8757         F s = centeredSumOfSquares!F;
8758         return centeredSumOfCubes!F / (s * s.sqrt) * count * sqrt(cast(F) count + isPopulation - 1) /
8759             (count + 2 * isPopulation - 2);
8760         /+ Equivalent to
8761         return scaledSumOfCubes!F(isPopulation) / count *
8762                 (cast(F) count * count / ((count + isPopulation - 1) * (count + 2 * isPopulation - 2)));
8763         +/
8764     }
8765     ///
8766     F kurtosis(F = T)(bool isPopulation, bool isRaw)
8767     in
8768     {
8769         assert(count > 3, "KurtosisAccumulator.kurtosis: count must be larger than three");
8770         assert(variance(true) > 0, "KurtosisAccumulator.kurtosis: variance must be larger than zero");
8771     }
8772     do
8773     {
8774         F mult1 = cast(F) count * (count + isPopulation - 1) * (count - isPopulation + 1) / ((count + 2 * isPopulation - 2) * (count + 3 * isPopulation - 3));
8775         F mult2 = cast(F) (count + isPopulation - 1) * (count + isPopulation - 1) / ((count + 2 * isPopulation - 2) * (count + 3 * isPopulation - 3));
8776         F s = centeredSumOfSquares!F;
8777         return centeredSumOfQuarts!F / (s * s) * mult1 + 3 * (isRaw - mult2);
8778     }
8779 }
8780 
8781 /// assumeZeroMean
8782 version(mir_stat_test)
8783 @safe pure nothrow
8784 unittest
8785 {
8786     import mir.math.common: approxEqual, pow;
8787     import mir.ndslice.slice: sliced;
8788     import mir.stat.transform: center;
8789 
8790     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
8791               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
8792     auto x = a.center;
8793 
8794     KurtosisAccumulator!(double, KurtosisAlgo.assumeZeroMean, Summation.naive) v;
8795     v.put(x);
8796     assert(v.kurtosis(true, true).approxEqual((792.784119 / 12) / pow(54.765625 / 12, 2.0)));
8797     assert(v.kurtosis(true, false).approxEqual((792.784119 / 12) / pow(54.765625 / 12, 2.0) - 3.0));
8798     assert(v.kurtosis(false, false).approxEqual(792.784119 / pow(54.765625 / 11, 2.0) * (12.0 * 13.0) / (11.0 * 10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)));
8799     assert(v.kurtosis(false, true).approxEqual(792.784119 / pow(54.765625 / 11, 2.0) * (12.0 * 13.0) / (11.0 * 10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0) + 3.0));
8800 
8801     v.put(4.0);
8802     assert(v.kurtosis(true, true).approxEqual((1048.784119 / 13) / pow(70.765625 / 13, 2.0)));
8803     assert(v.kurtosis(true, false).approxEqual((1048.784119 / 13) / pow(70.765625 / 13, 2.0) - 3.0));
8804     assert(v.kurtosis(false, false).approxEqual(1048.784119 / pow(70.765625 / 12, 2.0) * (13.0 * 14.0) / (12.0 * 11.0 * 10.0) - 3.0 * (12.0 * 12.0) / (11.0 * 10.0)));
8805     assert(v.kurtosis(false, true).approxEqual(1048.784119 / pow(70.765625 / 12, 2.0) * (13.0 * 14.0) / (12.0 * 11.0 * 10.0) - 3.0 * (12.0 * 12.0) / (11.0 * 10.0) + 3.0));
8806 }
8807 
8808 // Can put slice
8809 version(mir_stat_test)
8810 @safe pure nothrow
8811 unittest
8812 {
8813     import mir.math.common: approxEqual, pow;
8814     import mir.ndslice.slice: sliced;
8815     import mir.stat.transform: center;
8816 
8817     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
8818               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
8819     auto b = a.center;
8820     auto x = b[0 .. 6];
8821     auto y = b[6 .. $];
8822 
8823     KurtosisAccumulator!(double, KurtosisAlgo.assumeZeroMean, Summation.naive) v;
8824     v.put(x);
8825     assert(v.centeredSumOfQuarts.approxEqual(52.44613647));
8826     assert(v.centeredSumOfSquares.approxEqual(13.4921875));
8827 
8828     v.put(y);
8829     assert(v.centeredSumOfQuarts.approxEqual(792.784119));
8830     assert(v.centeredSumOfSquares.approxEqual(54.765625));
8831 }
8832 
8833 // Can put KurtosisAccumulator
8834 version(mir_stat_test)
8835 @safe pure nothrow
8836 unittest
8837 {
8838     import mir.math.common: approxEqual, pow;
8839     import mir.ndslice.slice: sliced;
8840     import mir.stat.transform: center;
8841 
8842     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
8843               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
8844     auto b = a.center;
8845     auto x = b[0 .. 6];
8846     auto y = b[6 .. $];
8847 
8848     KurtosisAccumulator!(double, KurtosisAlgo.assumeZeroMean, Summation.naive) v;
8849     v.put(x);
8850     assert(v.centeredSumOfQuarts.approxEqual(52.44613647));
8851     assert(v.centeredSumOfSquares.approxEqual(13.4921875));
8852 
8853     KurtosisAccumulator!(double, KurtosisAlgo.assumeZeroMean, Summation.naive) w;
8854     w.put(y);
8855     v.put(w);
8856     assert(v.centeredSumOfQuarts.approxEqual(792.784119));
8857     assert(v.centeredSumOfSquares.approxEqual(54.765625));
8858 }
8859 
8860 
8861 // check scaledSumOfCubes/scaledSumOfQuarts/skewness
8862 version(mir_stat_test)
8863 @safe pure nothrow
8864 unittest
8865 {
8866     import mir.math.common: sqrt;
8867     import mir.math.sum: Summation;
8868     import mir.ndslice.slice: sliced;
8869     import mir.stat.transform: center;
8870     import mir.test: shouldApprox;
8871 
8872     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
8873               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
8874     auto x = a.center;
8875 
8876     auto v = KurtosisAccumulator!(double, KurtosisAlgo.assumeZeroMean, Summation.naive)(x);
8877     auto varP = x.variance!"assumeZeroMean"(true);
8878     auto varS = x.variance!"assumeZeroMean"(false);
8879     v.scaledSumOfCubes(true).shouldApprox == v.centeredSumOfCubes / (varP * varP.sqrt);
8880     v.scaledSumOfCubes(false).shouldApprox == v.centeredSumOfCubes / (varS * varS.sqrt);
8881     v.scaledSumOfQuarts(true).shouldApprox == v.centeredSumOfQuarts / (varP * varP);
8882     v.scaledSumOfQuarts(false).shouldApprox == v.centeredSumOfQuarts / (varS * varS);
8883     v.skewness(true).shouldApprox == x.skewness!"assumeZeroMean"(true);
8884     v.skewness(false).shouldApprox == x.skewness!"assumeZeroMean"(false);
8885 }
8886 
8887 ///
8888 struct KurtosisAccumulator(T, KurtosisAlgo kurtosisAlgo, Summation summation)
8889     if (isMutable!T && kurtosisAlgo == KurtosisAlgo.hybrid)
8890 {
8891     import mir.math.sum: elementType, Summator;
8892     import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind;
8893     import std.range: isInputRange;
8894     import std.traits: isIterable;
8895 
8896     ///
8897     private MeanAccumulator!(T, summation) meanAccumulator;
8898     ///
8899     alias S = Summator!(T, summation);
8900     ///
8901     private S centeredSummatorOfSquares;
8902     ///
8903     private S centeredSummatorOfCubes;
8904     ///
8905     private S centeredSummatorOfQuarts;
8906 
8907     ///
8908     this(Iterator, size_t N, SliceKind kind)(Slice!(Iterator, N, kind) slice)
8909     {
8910         import mir.functional: naryFun;
8911         import mir.ndslice.topology: vmap, map;
8912         import mir.ndslice.internal: LeftOp;
8913 
8914         meanAccumulator.put(slice.lightScope);
8915 
8916         auto sliceMap = slice.vmap(LeftOp!("-", T)(mean)).map!(naryFun!"a * a", naryFun!"(a * a) * a", naryFun!"(a * a) * (a * a)");
8917         centeredSummatorOfSquares.put(sliceMap.map!"a[0]");
8918         centeredSummatorOfCubes.put(sliceMap.map!"a[1]");
8919         centeredSummatorOfQuarts.put(sliceMap.map!"a[2]");
8920     }
8921 
8922     ///
8923     this(SliceLike)(SliceLike x)
8924         if (isConvertibleToSlice!SliceLike && !isSlice!SliceLike)
8925     {
8926         import mir.ndslice.slice: toSlice;
8927         this(x.toSlice);
8928     }
8929 
8930     ///
8931     this(Range)(Range range)
8932         if (isIterable!Range && !isConvertibleToSlice!Range)
8933     {
8934         static if (isInputRange!Range && is(elementType!Range : T)) {
8935             import std.algorithm: map;
8936             meanAccumulator.put(range);
8937 
8938             auto centeredRangeMultiplier = range.map!(a => (a - mean)).map!("a * a", "a * a * a", "a * a * a * a");
8939             centeredSummatorOfSquares.put(centeredRangeMultiplier.map!"a[0]");
8940             centeredSummatorOfCubes.put(centeredRangeMultiplier.map!"a[1]");
8941             centeredSummatorOfQuarts.put(centeredRangeMultiplier.map!"a[2]");
8942         } else {
8943             this.put(range);
8944         }
8945     }
8946 
8947     ///
8948     this()(T x)
8949     {
8950         this.put(x);
8951     }
8952 
8953     ///
8954     void put(Range)(Range r)
8955         if (isIterable!Range)
8956     {
8957         static if (isInputRange!Range && is(elementType!Range : T)) {
8958             auto v = typeof(this)(r);
8959             this.put(v);
8960         } else {
8961             foreach(x; r)
8962             {
8963                 this.put(x);
8964             }
8965         }
8966     }
8967 
8968     ///
8969     void put()(T x)
8970     {
8971         T deltaOld = x;
8972         if (count > 0) {
8973             deltaOld -= mean;
8974         }
8975         meanAccumulator.put(x);
8976         T deltaNew = x - mean;
8977         centeredSummatorOfQuarts.put(deltaOld * deltaOld * deltaOld * deltaOld * ((count - 1) * (count * count - 3 * count + 3)) / (count * count * count) +
8978                                 6 * deltaOld * deltaOld * centeredSumOfSquares!T / (count * count) -
8979                                 4 * deltaOld * centeredSumOfCubes!T / count);
8980         centeredSummatorOfCubes.put(deltaOld * deltaOld * deltaOld * (count - 1) * (count - 2) / (count * count) -
8981                                3 * deltaOld * centeredSumOfSquares!T / count);
8982         centeredSummatorOfSquares.put(deltaOld * deltaNew);
8983     }
8984 
8985     ///
8986     void put(U, KurtosisAlgo kurtAlgo, Summation sumAlgo)(KurtosisAccumulator!(U, kurtAlgo, sumAlgo) v)
8987     {
8988         size_t oldCount = count;
8989         T delta = v.mean;
8990         if (oldCount > 0) {
8991             delta -= mean;
8992         }
8993         meanAccumulator.put!T(v.meanAccumulator);
8994         centeredSummatorOfQuarts.put(v.centeredSumOfQuarts!T + 
8995                                delta * delta * delta * delta * ((v.count * oldCount) * (oldCount * oldCount - v.count * oldCount + v.count * v.count)) / (count * count * count) +
8996                                6 * delta * delta * ((oldCount * oldCount) * v.centeredSumOfSquares!T + (v.count * v.count) * centeredSumOfSquares!T) / (count * count) +
8997                                4 * delta * (oldCount * v.centeredSumOfCubes!T - v.count * centeredSumOfCubes!T) / count);
8998         centeredSummatorOfCubes.put(v.centeredSumOfCubes!T + 
8999                                delta * delta * delta * v.count * oldCount * (oldCount - v.count) / (count * count) +
9000                                3 * delta * (oldCount * v.centeredSumOfSquares!T - v.count * centeredSumOfSquares!T) / count);
9001         centeredSummatorOfSquares.put(v.centeredSumOfSquares!T + delta * delta * v.count * oldCount / count);
9002     }
9003 
9004 const:
9005 
9006     ///
9007     size_t count()
9008     {
9009         return meanAccumulator.count;
9010     }
9011     ///
9012     F centeredSumOfQuarts(F = T)()
9013     {
9014         return cast(F) centeredSummatorOfQuarts.sum;
9015     }
9016     ///
9017     F centeredSumOfCubes(F = T)()
9018     {
9019         return cast(F) centeredSummatorOfCubes.sum;
9020     }
9021     ///
9022     F centeredSumOfSquares(F = T)()
9023     {
9024         return cast(F) centeredSummatorOfSquares.sum;
9025     }
9026     ///
9027     F scaledSumOfCubes(F = T)(bool isPopulation)
9028     {
9029         import mir.math.common: sqrt;
9030         F var = variance!F(isPopulation);
9031         return centeredSumOfCubes!F/ (var * var.sqrt);
9032     }
9033     ///
9034     F scaledSumOfQuarts(F = T)(bool isPopulation)
9035     {
9036         F var = variance!F(isPopulation);
9037         return centeredSumOfQuarts!F/ (var * var);
9038     }
9039     ///
9040     F mean(F = T)()
9041     {
9042         return meanAccumulator.mean!F;
9043     }
9044     ///
9045     F variance(F = T)(bool isPopulation)
9046     in
9047     {
9048         assert(count > 1, "KurtosisAccumulator.variance: count must be larger than one");
9049     }
9050     do
9051     {
9052         return centeredSumOfSquares!F / (count + isPopulation - 1);
9053     }
9054     ///
9055     F skewness(F = T)(bool isPopulation)
9056     in
9057     {
9058         assert(count > 2, "SkewnessAccumulator.skewness: count must be larger than two");
9059         assert(centeredSummatorOfSquares.sum > 0, "SkewnessAccumulator.skewness: variance must be larger than zero");
9060     }
9061     do
9062     {
9063         import mir.math.common: sqrt;
9064         F s = centeredSumOfSquares!F;
9065         return centeredSumOfCubes!F / (s * s.sqrt) * count * sqrt(cast(F) count + isPopulation - 1) /
9066             (count + 2 * isPopulation - 2);
9067         /+ Equivalent to
9068         return scaledSumOfCubes!F(isPopulation) / count *
9069                 (cast(F) count * count / ((count + isPopulation - 1) * (count + 2 * isPopulation - 2)));
9070         +/
9071     }
9072     ///
9073     F kurtosis(F = T)(bool isPopulation, bool isRaw)
9074     in
9075     {
9076         assert(count > 3, "KurtosisAccumulator.kurtosis: count must be larger than three");
9077         assert(variance(true) > 0, "KurtosisAccumulator.kurtosis: variance must be larger than zero");
9078     }
9079     do
9080     {
9081         F mult1 = cast(F) count * (count + isPopulation - 1) * (count - isPopulation + 1) / ((count + 2 * isPopulation - 2) * (count + 3 * isPopulation - 3));
9082         F mult2 = cast(F) (count + isPopulation - 1) * (count + isPopulation - 1) / ((count + 2 * isPopulation - 2) * (count + 3 * isPopulation - 3));
9083         F s = centeredSumOfSquares!F;
9084         return centeredSumOfQuarts!F / (s * s) * mult1 + 3 * (isRaw - mult2);
9085     }
9086 }
9087 
9088 /// hybrid
9089 version(mir_stat_test)
9090 @safe pure nothrow
9091 unittest
9092 {
9093     import mir.math.common: approxEqual, pow;
9094     import mir.ndslice.slice: sliced;
9095     import mir.test: shouldApprox;
9096 
9097     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
9098               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
9099 
9100     auto v = KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive)(x);
9101     v.kurtosis(true, true).shouldApprox == (792.784119 / 12) / pow(54.765625 / 12, 2.0);
9102     v.kurtosis(true, false).shouldApprox == (792.784119 / 12) / pow(54.765625 / 12, 2.0) - 3;
9103     v.kurtosis(false, false).shouldApprox == (792.784119 / 12) / pow(54.765625 / 12, 2.0) * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0);
9104     v.kurtosis(false, true).shouldApprox == (792.784119 / 12) / pow(54.765625 / 12, 2.0) * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0) + 3;
9105 
9106     v.put(4.0);
9107     v.kurtosis(true, true).shouldApprox == (745.608180 / 13) / pow(57.019231 / 13, 2.0);
9108     v.kurtosis(true, false).shouldApprox == (745.608180 / 13) / pow(57.019231 / 13, 2.0) - 3;
9109     v.kurtosis(false, false).shouldApprox == (745.608180 / 13) / pow(57.019231 / 13, 2.0) * (12.0 * 14.0) / (11.0 * 10.0) - 3.0 * (12.0 * 12.0) / (11.0 * 10.0);
9110     v.kurtosis(false, true).shouldApprox == (745.608180 / 13) / pow(57.019231 / 13, 2.0) * (12.0 * 14.0) / (11.0 * 10.0) - 3.0 * (12.0 * 12.0) / (11.0 * 10.0) + 3;
9111 }
9112 
9113 // check withAsSlice
9114 version(mir_stat_test)
9115 @safe pure nothrow @nogc
9116 unittest
9117 {
9118     import mir.math.sum: Summation;
9119     import mir.rc.array: RCArray;
9120     import mir.test: shouldApprox;
9121 
9122     static immutable a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
9123                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
9124 
9125     auto x = RCArray!double(12);
9126     foreach(i, ref e; x)
9127         e = a[i];
9128 
9129     auto v = KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive)(x);
9130     v.scaledSumOfQuarts(true).shouldApprox == 38.062853;
9131 }
9132 
9133 // check dynamic slice
9134 version(mir_stat_test)
9135 @safe pure nothrow
9136 unittest
9137 {
9138     import mir.math.sum: Summation;
9139     import mir.test: shouldApprox;
9140 
9141     double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
9142                   2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
9143 
9144     auto v = KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive)(x);
9145     v.scaledSumOfQuarts(true).shouldApprox == 38.062853;
9146 }
9147 
9148 // Test input range
9149 version(mir_stat_test)
9150 @safe pure nothrow
9151 unittest
9152 {
9153     import mir.math.sum: Summation;
9154     import mir.test: shouldApprox;
9155     import std.algorithm: map;
9156     import std.range: chunks, iota;
9157 
9158     auto x1 = iota(0, 5);
9159     auto v1 = KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive)(x1);
9160     v1.kurtosis(false, true).shouldApprox == 1.8;
9161     auto x2 = x1.map!(a => 2 * a);
9162     auto v2 = KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive)(x2);
9163     v2.kurtosis(false, true).shouldApprox == 1.8;
9164     KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive) v3;
9165     v3.put(x1.chunks(1));
9166     v3.kurtosis(false, true).shouldApprox == 1.8;
9167     auto v4 = KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive)(x1.chunks(1));
9168     v4.kurtosis(false, true).shouldApprox == 1.8;
9169 }
9170 
9171 // Can put slice
9172 version(mir_stat_test)
9173 @safe pure nothrow
9174 unittest
9175 {
9176     import mir.math.common: approxEqual;
9177     import mir.ndslice.slice: sliced;
9178 
9179     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
9180     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
9181 
9182     KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive) v;
9183     v.put(x);
9184     assert(v.centeredSumOfQuarts.approxEqual(46.944607));
9185     assert(v.centeredSumOfSquares.approxEqual(12.552083));
9186 
9187     v.put(y);
9188     assert(v.centeredSumOfQuarts.approxEqual(792.784119));
9189     assert(v.centeredSumOfSquares.approxEqual(54.765625));
9190 }
9191 
9192 // Can put KurtosisAccumulator
9193 version(mir_stat_test)
9194 @safe pure nothrow
9195 unittest
9196 {
9197     import mir.math.common: approxEqual;
9198     import mir.ndslice.slice: sliced;
9199 
9200     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
9201     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
9202 
9203     auto v = KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive)(x);
9204     assert(v.centeredSumOfQuarts.approxEqual(46.944607));
9205     assert(v.centeredSumOfSquares.approxEqual(12.552083));
9206 
9207     auto w = KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive)(y);
9208     v.put(w);
9209     assert(v.centeredSumOfQuarts.approxEqual(792.784119));
9210     assert(v.centeredSumOfSquares.approxEqual(54.765625));
9211 }
9212 
9213 // Can put KurtosisAccumulator (naive)
9214 version(mir_stat_test)
9215 @safe pure nothrow
9216 unittest
9217 {
9218     import mir.math.common: approxEqual;
9219     import mir.ndslice.slice: sliced;
9220 
9221     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
9222     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
9223 
9224     auto v = KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive)(x);
9225     assert(v.centeredSumOfQuarts.approxEqual(46.944607));
9226     assert(v.centeredSumOfSquares.approxEqual(12.552083));
9227 
9228     auto w = KurtosisAccumulator!(double, KurtosisAlgo.naive, Summation.naive)(y);
9229     v.put(w);
9230     assert(v.centeredSumOfQuarts.approxEqual(792.784119));
9231     assert(v.centeredSumOfSquares.approxEqual(54.765625));
9232 }
9233 
9234 // Can put KurtosisAccumulator (online)
9235 version(mir_stat_test)
9236 @safe pure nothrow
9237 unittest
9238 {
9239     import mir.math.common: approxEqual;
9240     import mir.ndslice.slice: sliced;
9241 
9242     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
9243     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
9244 
9245     auto v = KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive)(x);
9246     assert(v.centeredSumOfQuarts.approxEqual(46.944607));
9247     assert(v.centeredSumOfSquares.approxEqual(12.552083));
9248 
9249     auto w = KurtosisAccumulator!(double, KurtosisAlgo.online, Summation.naive)(y);
9250     v.put(w);
9251     assert(v.centeredSumOfQuarts.approxEqual(792.784119));
9252     assert(v.centeredSumOfSquares.approxEqual(54.765625));
9253 }
9254 
9255 // Can put KurtosisAccumulator (twoPass)
9256 version(mir_stat_test)
9257 @safe pure nothrow
9258 unittest
9259 {
9260     import mir.math.common: approxEqual;
9261     import mir.ndslice.slice: sliced;
9262 
9263     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
9264     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
9265 
9266     auto v = KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive)(x);
9267     assert(v.centeredSumOfQuarts.approxEqual(46.944607));
9268     assert(v.centeredSumOfSquares.approxEqual(12.552083));
9269 
9270     auto w = KurtosisAccumulator!(double, KurtosisAlgo.twoPass, Summation.naive)(y);
9271     v.put(w);
9272     assert(v.centeredSumOfQuarts.approxEqual(792.784119));
9273     assert(v.centeredSumOfSquares.approxEqual(54.765625));
9274 }
9275 
9276 // Can put KurtosisAccumulator (threePass)
9277 version(mir_stat_test)
9278 @safe pure nothrow
9279 unittest
9280 {
9281     import mir.math.common: approxEqual;
9282     import mir.ndslice.slice: sliced;
9283 
9284     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
9285     auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
9286 
9287     auto v = KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive)(x);
9288     assert(v.centeredSumOfQuarts.approxEqual(46.944607));
9289     assert(v.centeredSumOfSquares.approxEqual(12.552083));
9290 
9291     auto w = KurtosisAccumulator!(double, KurtosisAlgo.threePass, Summation.naive)(y);
9292     v.put(w);
9293     assert(v.centeredSumOfQuarts.approxEqual(792.784119));
9294     assert(v.centeredSumOfSquares.approxEqual(54.765625));
9295 }
9296 
9297 // Can put KurtosisAccumulator (assumeZeroMean)
9298 version(mir_stat_test)
9299 @safe pure nothrow
9300 unittest
9301 {
9302     import mir.math.common: approxEqual;
9303     import mir.ndslice.slice: sliced;
9304     import mir.stat.transform: center;
9305 
9306     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced;
9307     auto b = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
9308     auto x = a.center;
9309     auto y = b.center;
9310 
9311     auto v = KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive)(x);
9312     auto w = KurtosisAccumulator!(double, KurtosisAlgo.assumeZeroMean, Summation.naive)(y);
9313     v.put(w);
9314     assert(v.centeredSumOfQuarts.approxEqual(622.639052)); //note: different from above due to inconsistent centering
9315     assert(v.centeredSumOfSquares.approxEqual(52.885417)); //note: different from above due to inconsistent centering
9316 }
9317 
9318 // check scaledSumOfCubes/scaledSumOfQuarts/skewness
9319 version(mir_stat_test)
9320 @safe pure nothrow
9321 unittest
9322 {
9323     import mir.math.common: sqrt;
9324     import mir.math.sum: Summation;
9325     import mir.ndslice.slice: sliced;
9326     import mir.test: shouldApprox;
9327 
9328     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
9329               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
9330 
9331     KurtosisAccumulator!(double, KurtosisAlgo.hybrid, Summation.naive) v;
9332     v.put(x);
9333     auto varP = x.variance!"twoPass"(true);
9334     auto varS = x.variance!"twoPass"(false);
9335     v.scaledSumOfCubes(true).shouldApprox == v.centeredSumOfCubes / (varP * varP.sqrt);
9336     v.scaledSumOfCubes(false).shouldApprox == v.centeredSumOfCubes / (varS * varS.sqrt);
9337     v.scaledSumOfQuarts(true).shouldApprox == v.centeredSumOfQuarts / (varP * varP);
9338     v.scaledSumOfQuarts(false).shouldApprox == v.centeredSumOfQuarts / (varS * varS);
9339     v.skewness(true).shouldApprox == x.skewness!"hybrid"(true);
9340     v.skewness(false).shouldApprox == x.skewness!"hybrid"(false);
9341 }
9342 
9343 /++
9344 Calculates the kurtosis of the input
9345 
9346 By default, if `F` is not floating point type, then the result will have a
9347 `double` type if `F` is implicitly convertible to a floating point type.
9348 
9349 Params:
9350     F = controls type of output
9351     kurtosisAlgo = algorithm for calculating kurtosis (default: KurtosisAlgo.hybrid)
9352     summation = algorithm for calculating sums (default: Summation.appropriate)
9353 
9354 Returns:
9355     The kurtosis of the input, must be floating point
9356 
9357 See_also:
9358     $(LREF KurtosisAlgo)
9359 +/
9360 template kurtosis(
9361     F, 
9362     KurtosisAlgo kurtosisAlgo = KurtosisAlgo.hybrid, 
9363     Summation summation = Summation.appropriate)
9364 {
9365     import std.traits: isIterable;
9366 
9367     /++
9368     Params:
9369         r = range, must be finite iterable
9370         isPopulation = true if population kurtosis, false if sample kurtosis (default)
9371         isRaw = true if raw kurtosis, false if excess kurtosis (default)
9372     +/
9373     @fmamath stdevType!F kurtosis(Range)(Range r, bool isPopulation = false, bool isRaw = false)
9374         if (isIterable!Range)
9375     {
9376         import core.lifetime: move;
9377         alias G = typeof(return);
9378         auto kurtosisAccumulator = KurtosisAccumulator!(G, kurtosisAlgo, ResolveSummationType!(summation, Range, G))(r.move);
9379         return kurtosisAccumulator.kurtosis(isPopulation, isRaw);
9380     }
9381 
9382     /++
9383     Params:
9384         ar = values
9385     +/
9386     @fmamath stdevType!F kurtosis(scope const F[] ar...)
9387     {
9388         alias G = typeof(return);
9389         auto kurtosisAccumulator = KurtosisAccumulator!(G, kurtosisAlgo, ResolveSummationType!(summation, const(G)[], G))(ar);
9390         return kurtosisAccumulator.kurtosis(false, false);
9391     }
9392 }
9393 
9394 /// ditto
9395 template kurtosis(
9396     KurtosisAlgo kurtosisAlgo = KurtosisAlgo.hybrid, 
9397     Summation summation = Summation.appropriate)
9398 {
9399     import std.traits: isIterable;
9400 
9401     /++
9402     Params:
9403         r = range, must be finite iterable
9404         isPopulation = true if population kurtosis, false if sample kurtosis (default)
9405         isRaw = true if raw kurtosis, false if excess kurtosis (default)
9406     +/
9407     @fmamath stdevType!Range kurtosis(Range)(Range r, bool isPopulation = false, bool isRaw = false)
9408         if (isIterable!Range)
9409     {
9410         import core.lifetime: move;
9411         alias F = typeof(return);
9412         return .kurtosis!(F, kurtosisAlgo, summation)(r.move, isPopulation, isRaw);
9413     }
9414 
9415     /++
9416     Params:
9417         ar = values
9418     +/
9419     @fmamath stdevType!T kurtosis(T)(scope const T[] ar...)
9420     {
9421         alias F = typeof(return);
9422         return .kurtosis!(F, kurtosisAlgo, summation)(ar);
9423     }
9424 }
9425 
9426 /// ditto
9427 template kurtosis(F, string kurtosisAlgo, string summation = "appropriate")
9428 {
9429     mixin("alias kurtosis = .kurtosis!(F, KurtosisAlgo." ~ kurtosisAlgo ~ ", Summation." ~ summation ~ ");");
9430 }
9431 
9432 /// ditto
9433 template kurtosis(string kurtosisAlgo, string summation = "appropriate")
9434 {
9435     mixin("alias kurtosis = .kurtosis!(KurtosisAlgo." ~ kurtosisAlgo ~ ", Summation." ~ summation ~ ");");
9436 }
9437 
9438 /// Simple example
9439 version(mir_stat_test)
9440 @safe pure nothrow
9441 unittest
9442 {
9443     import mir.math.common: approxEqual, pow;
9444     import mir.ndslice.slice: sliced;
9445 
9446     assert(kurtosis([1.0, 2, 3, 4]).approxEqual(-1.2));
9447 
9448     assert(kurtosis([1.0, 2, 4, 5]).approxEqual((34.0 / 4) / pow(10.0 / 4, 2.0) * (3.0 * 5.0) / (2.0 * 1.0) - 3.0 * (3.0 * 3.0) / (2.0 * 1.0)));
9449     // population excess kurtosis
9450     assert(kurtosis([1.0, 2, 4, 5], true).approxEqual((34.0 / 4) / pow(10.0 / 4, 2.0) - 3.0));
9451     // sample raw kurtosis
9452     assert(kurtosis([1.0, 2, 4, 5], false, true).approxEqual((34.0 / 4) / pow(10.0 / 4, 2.0) * (3.0 * 5.0) / (2.0 * 1.0) - 3.0 * (3.0 * 3.0) / (2.0 * 1.0) + 3.0));
9453     // population raw kurtosis
9454     assert(kurtosis([1.0, 2, 4, 5], true, true).approxEqual((34.0 / 4) / pow(10.0 / 4, 2.0)));
9455 
9456     assert(kurtosis!float([0, 1, 2, 3, 4, 6].sliced(3, 2)).approxEqual(-0.2999999));
9457 
9458     static assert(is(typeof(kurtosis!float([1, 2, 3])) == float));
9459 }
9460 
9461 /// Kurtosis of vector
9462 version(mir_stat_test)
9463 @safe pure nothrow
9464 unittest
9465 {
9466     import mir.math.common: approxEqual, pow;
9467 
9468     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
9469               2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
9470 
9471     assert(x.kurtosis.approxEqual((792.784119 / 12) / pow(54.765625 / 12, 2.0) * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)));
9472 }
9473 
9474 /// Kurtosis of matrix
9475 version(mir_stat_test)
9476 @safe pure
9477 unittest
9478 {
9479     import mir.math.common: approxEqual, pow;
9480     import mir.ndslice.fuse: fuse;
9481 
9482     auto x = [
9483         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
9484         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
9485     ].fuse;
9486 
9487     assert(x.kurtosis.approxEqual((792.784119 / 12) / pow(54.765625 / 12, 2.0) * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)));
9488 }
9489 
9490 /// Column kurtosis of matrix
9491 version(mir_stat_test)
9492 @safe pure
9493 unittest
9494 {
9495     import mir.algorithm.iteration: all;
9496     import mir.math.common: approxEqual, pow;
9497     import mir.ndslice.fuse: fuse;
9498     import mir.ndslice.topology: alongDim, byDim, map;
9499 
9500     auto x = [
9501         [0.0,  1.0,  1.5, 2.0], 
9502         [3.5, 4.25,  2.0, 7.5],
9503         [5.0,  1.0,  1.5, 0.0],
9504         [1.5,  4.5, 4.75, 0.5]
9505     ].fuse;
9506     auto result = [-2.067182, -5.918089, 3.504056, 2.690240];
9507 
9508     // Use byDim or alongDim with map to compute kurtosis of row/column.
9509     assert(x.byDim!1.map!kurtosis.all!approxEqual(result));
9510     assert(x.alongDim!0.map!kurtosis.all!approxEqual(result));
9511 
9512     // FIXME
9513     // Without using map, computes the kurtosis of the whole slice
9514     // assert(x.byDim!1.kurtosis == x.sliced.kurtosis);
9515     // assert(x.alongDim!0.kurtosis == x.sliced.kurtosis);
9516 }
9517 
9518 /// Can also set algorithm type
9519 version(mir_stat_test)
9520 @safe pure nothrow
9521 unittest
9522 {
9523     import mir.math.common: approxEqual, pow;
9524     import mir.ndslice.slice: sliced;
9525 
9526     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
9527               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
9528 
9529     auto x = a + 100_000_000_000;
9530 
9531     // The online algorithm is numerically unstable in this case
9532     auto y = x.kurtosis!"online";
9533     assert(!y.approxEqual((792.78411865 / 12) / pow(54.76562500 / 12, 2.0) * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)));
9534 
9535     // The naive algorithm has an assert error in this case because standard
9536     // deviation is calculated naively as zero. The kurtosis formula would then
9537     // be dividing by zero. 
9538     //auto z0 = x.kurtosis!(real, "naive");
9539 
9540     // The two-pass algorithm is also numerically unstable in this case
9541     auto z1 = x.kurtosis!"twoPass";
9542     assert(!z1.approxEqual(38.062853 / 12 * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)) + 3.0);
9543     assert(!z1.approxEqual(y));
9544 
9545     // However, the three-pass algorithm is numerically stable in this case
9546     auto z2 = x.kurtosis!"threePass";
9547     assert(z2.approxEqual(38.062853 / 12 * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)) + 3.0);
9548     assert(!z2.approxEqual(y));
9549 
9550     // And the assumeZeroMean algorithm provides the incorrect answer, as expected
9551     auto z3 = x.kurtosis!"assumeZeroMean";
9552     assert(!z3.approxEqual(y));
9553 }
9554 
9555 // Alt version with x a hundred of above's value
9556 version(mir_stat_test)
9557 @safe pure nothrow
9558 unittest
9559 {
9560     import mir.math.common: approxEqual, pow;
9561     import mir.ndslice.slice: sliced;
9562 
9563     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
9564               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
9565 
9566     auto x = a + 1_000_000_000;
9567 
9568     // The online algorithm is numerically stable in this case
9569     auto y = x.kurtosis!"online";
9570     assert(y.approxEqual((792.78411865 / 12) / pow(54.76562500 / 12, 2.0) * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)));
9571 
9572     // The naive algorithm has an assert error in this case because standard
9573     // deviation is calculated naively as zero. The kurtosis formula would then
9574     // be dividing by zero. 
9575     //auto z0 = x.kurtosis!(real, "naive");
9576 
9577     // The two-pass algorithm is  numerically stable in this case
9578     auto z1 = x.kurtosis!"twoPass";
9579     assert(z1.approxEqual(38.062853 / 12 * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)) + 3.0);
9580     assert(z1.approxEqual(y));
9581 
9582     // However, the three-pass algorithm is numerically stable in this case
9583     auto z2 = x.kurtosis!"threePass";
9584     assert(z2.approxEqual(38.062853 / 12 * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)) + 3.0);
9585     assert(z2.approxEqual(y));
9586 
9587     // And the assumeZeroMean algorithm provides the incorrect answer, as expected
9588     auto z3 = x.kurtosis!"assumeZeroMean";
9589     assert(!z3.approxEqual(y));
9590 }
9591 
9592 /// Can also set algorithm or output type
9593 version(mir_stat_test)
9594 @safe pure nothrow
9595 unittest
9596 {
9597     import mir.math.common: approxEqual;
9598     import mir.ndslice.slice: sliced;
9599     import mir.ndslice.topology: repeat;
9600 
9601     // Set population/sample kurtosis, excess/raw kurtosis, kurtosis algorithm,
9602     // sum algorithm or output type
9603 
9604     auto a = [1.0, 1e72, 1, -1e72].sliced;
9605     auto x = a * 10_000;
9606 
9607     /++
9608     Due to Floating Point precision, when centering `x`, subtracting the mean 
9609     from the second and fourth numbers has no effect. Further, after centering 
9610     and taking `x` to the fourth power, the first and third numbers in the slice
9611     have precision too low to be included in the centered sum of cubes. 
9612     +/
9613     assert(x.kurtosis.approxEqual(1.5));
9614     assert(x.kurtosis(false).approxEqual(1.5));
9615     assert(x.kurtosis(true).approxEqual(-1.0));
9616     assert(x.kurtosis(true, true).approxEqual(2.0));
9617     assert(x.kurtosis(false, true).approxEqual(4.5));
9618 
9619     assert(x.kurtosis!("online").approxEqual(1.5));
9620     assert(x.kurtosis!("online", "kbn").approxEqual(1.5));
9621     assert(x.kurtosis!("online", "kb2").approxEqual(1.5));
9622     assert(x.kurtosis!("online", "precise").approxEqual(1.5));
9623     assert(x.kurtosis!(double, "online", "precise").approxEqual(1.5));
9624     assert(x.kurtosis!(double, "online", "precise")(true).approxEqual(-1.0));
9625     assert(x.kurtosis!(double, "online", "precise")(true, true).approxEqual(2.0));
9626 
9627     auto y = [uint.max - 3, uint.max - 2, uint.max - 1, uint.max].sliced;
9628     auto z = y.kurtosis!(ulong, "threePass");
9629     assert(z.approxEqual(-1.2));
9630     static assert(is(typeof(z) == double));
9631 }
9632 
9633 /++
9634 For integral slices, can pass output type as template parameter to ensure output
9635 type is correct.
9636 +/
9637 version(mir_stat_test)
9638 @safe pure nothrow
9639 unittest
9640 {
9641     import mir.math.common: approxEqual;
9642     import mir.ndslice.slice: sliced;
9643 
9644     auto x = [0, 1, 1, 2, 4, 4,
9645               2, 7, 5, 1, 2, 0].sliced;
9646 
9647     auto y = x.kurtosis;
9648     assert(y.approxEqual(0.223394));
9649     static assert(is(typeof(y) == double));
9650 
9651     assert(x.kurtosis!float.approxEqual(0.223394));
9652 }
9653 
9654 /++
9655 Kurtosis works for other user-defined types (provided they can be converted to a
9656 floating point)
9657 +/
9658 version(mir_stat_test)
9659 @safe pure nothrow
9660 unittest
9661 {
9662     import mir.math.common: approxEqual;
9663 
9664     static struct Foo {
9665         float x;
9666         alias x this;
9667     }
9668 
9669     Foo[] foo = [Foo(1f), Foo(2f), Foo(3f), Foo(4f)];
9670     assert(foo.kurtosis.approxEqual(-1.2f));
9671 }
9672 
9673 /// Compute kurtosis along specified dimention of tensors
9674 version(mir_stat_test)
9675 @safe pure
9676 unittest
9677 {
9678     import mir.algorithm.iteration: all;
9679     import mir.math.common: approxEqual;
9680     import mir.ndslice.fuse: fuse;
9681     import mir.ndslice.topology: as, iota, alongDim, map, repeat;
9682 
9683     auto x = [
9684         [0.0,  1,  3,  5],
9685         [3.0,  4,  5,  7],
9686         [6.0,  7, 10, 11],
9687         [9.0, 12, 15, 12]
9688     ].fuse;
9689 
9690     assert(x.kurtosis.approxEqual(-0.770040));
9691 
9692     auto m0 = [-1.200000, -0.152893, -1.713859, -3.869005];
9693     assert(x.alongDim!0.map!kurtosis.all!approxEqual(m0));
9694     assert(x.alongDim!(-2).map!kurtosis.all!approxEqual(m0));
9695 
9696     auto m1 = [-1.699512, 0.342857, -4.339100, 1.500000];
9697     assert(x.alongDim!1.map!kurtosis.all!approxEqual(m1));
9698     assert(x.alongDim!(-1).map!kurtosis.all!approxEqual(m1));
9699 
9700     assert(iota(4, 5, 6, 7).as!double.alongDim!0.map!kurtosis.all!approxEqual(repeat(-1.2, 5, 6, 7)));
9701 }
9702 
9703 /// Arbitrary kurtosis
9704 version(mir_stat_test)
9705 @safe pure nothrow @nogc
9706 unittest
9707 {
9708     import mir.math.common: approxEqual;
9709 
9710     assert(kurtosis(1.0, 2, 3, 4).approxEqual(-1.2));
9711     assert(kurtosis!float(1, 2, 3, 4).approxEqual(-1.2f));
9712 }
9713 
9714 // Check kurtosis vector UFCS
9715 version(mir_stat_test)
9716 @safe pure nothrow
9717 unittest
9718 {
9719     import mir.math.common: approxEqual;
9720 
9721     assert([1.0, 2, 3, 4].kurtosis.approxEqual(-1.2));
9722 }
9723 
9724 // Double-check correct output types
9725 version(mir_stat_test)
9726 @safe pure nothrow
9727 unittest
9728 {
9729     import mir.algorithm.iteration: all;
9730     import mir.math.common: approxEqual;
9731     import mir.ndslice.topology: iota, alongDim, map;
9732 
9733     auto x = iota([4, 4], 1);
9734     auto y = x.alongDim!1.map!kurtosis;
9735     assert(y.all!approxEqual([-1.2, -1.2, -1.2, -1.2]));
9736     static assert(is(stdevType!(typeof(y)) == double));
9737 }
9738 
9739 // @nogc kurtosis test
9740 version(mir_stat_test)
9741 @safe pure @nogc nothrow
9742 unittest
9743 {
9744     import mir.math.common: approxEqual, pow;
9745     import mir.ndslice.slice: sliced;
9746 
9747     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
9748                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
9749 
9750     assert(x.sliced.kurtosis.approxEqual((792.784119 / 12) / pow(54.765625 / 12, 2.0) * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)));
9751     assert(x.sliced.kurtosis!float.approxEqual((792.784119 / 12) / pow(54.765625 / 12, 2.0) * (11.0 * 13.0) / (10.0 * 9.0) - 3.0 * (11.0 * 11.0) / (10.0 * 9.0)));
9752 }
9753 
9754 // Test all using values
9755 version(mir_stat_test)
9756 @safe pure nothrow
9757 unittest
9758 {
9759     import mir.math.common: approxEqual, pow;
9760     import mir.stat.transform: center;
9761 
9762     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
9763               2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
9764 
9765     assert(x.kurtosis.approxEqual(1.006470));
9766     assert(x.kurtosis(false, true).approxEqual(4.006470));
9767     assert(x.kurtosis(true).approxEqual(0.171904));
9768     assert(x.kurtosis(true, true).approxEqual(3.171904));
9769 
9770     assert(x.kurtosis!"naive".approxEqual(1.006470));
9771     assert(x.kurtosis!"naive"(false, true).approxEqual(4.006470));
9772     assert(x.kurtosis!"naive"(true).approxEqual(0.171904));
9773     assert(x.kurtosis!"naive"(true, true).approxEqual(3.171904));
9774 
9775     assert(x.kurtosis!"online".approxEqual(1.006470));
9776     assert(x.kurtosis!"online"(false, true).approxEqual(4.006470));
9777     assert(x.kurtosis!"online"(true).approxEqual(0.171904));
9778     assert(x.kurtosis!"online"(true, true).approxEqual(3.171904));
9779 
9780     assert(x.kurtosis!"twoPass".approxEqual(1.006470));
9781     assert(x.kurtosis!"twoPass"(false, true).approxEqual(4.006470));
9782     assert(x.kurtosis!"twoPass"(true).approxEqual(0.171904));
9783     assert(x.kurtosis!"twoPass"(true, true).approxEqual(3.171904));
9784 
9785     assert(x.kurtosis!"threePass".approxEqual(1.006470));
9786     assert(x.kurtosis!"threePass"(false, true).approxEqual(4.006470));
9787     assert(x.kurtosis!"threePass"(true).approxEqual(0.171904));
9788     assert(x.kurtosis!"threePass"(true, true).approxEqual(3.171904));
9789 
9790     auto y = x.center;
9791     assert(y.kurtosis!"assumeZeroMean".approxEqual(1.006470));
9792     assert(y.kurtosis!"assumeZeroMean"(false, true).approxEqual(4.006470));
9793     assert(y.kurtosis!"assumeZeroMean"(true).approxEqual(0.171904));
9794     assert(y.kurtosis!"assumeZeroMean"(true, true).approxEqual(3.171904));
9795 }
9796 
9797 // compile with dub test --build=unittest-perf --config=unittest-perf --compiler=ldc2
9798 version(mir_stat_test_kurt_performance)
9799 unittest
9800 {
9801     import mir.math.sum: Summation;
9802     import mir.math.internal.benchmark;
9803     import std.stdio: writeln;
9804     import std.traits: EnumMembers;
9805 
9806     template staticMap(alias fun, alias S, args...)
9807     {
9808         import std.meta: AliasSeq;
9809         alias staticMap = AliasSeq!();
9810         static foreach (arg; args)
9811             staticMap = AliasSeq!(staticMap, fun!(double, arg, S));
9812     }
9813 
9814     size_t n = 10_000;
9815     size_t m = 1_000;
9816 
9817     alias S = Summation.fast;
9818     alias E = EnumMembers!KurtosisAlgo;
9819     alias fs = staticMap!(kurtosis, S, E);
9820     double[fs.length] output;
9821 
9822     auto e = [E];
9823     auto time = benchmarkRandom!(fs)(n, m, output);
9824     writeln("Kurtosis performance test");
9825     foreach (size_t i; 0 .. fs.length) {
9826         writeln("Function ", i + 1, ", Algo: ", e[i], ", Output: ", output[i], ", Elapsed time: ", time[i]);
9827     }
9828     writeln();
9829 }
9830 
9831 ///
9832 struct EntropyAccumulator(T, Summation summation)
9833 {
9834     import mir.math.internal.xlogy: xlog;
9835     import mir.primitives: hasShape;
9836     import std.traits: isIterable;
9837 
9838     ///
9839     Summator!(T, summation) summator;
9840     ///
9841     F entropy(F = T)() const @safe @property pure nothrow @nogc
9842     {
9843         return cast(F) summator.sum;
9844     }
9845 
9846     ///
9847     void put(Range)(Range r)
9848         if (isIterable!Range)
9849     {
9850         static if (hasShape!Range)
9851         {
9852             import mir.ndslice.topology: as, map;
9853 
9854             summator.put(r.as!T.map!xlog);
9855         }
9856         else
9857         {
9858             foreach(x; r)
9859             {
9860                 summator.put(xlog(cast(T)x));
9861             }
9862         }
9863     }
9864 
9865     ///
9866     void put()(T x)
9867     {
9868         summator.put(xlog(x));
9869     }
9870 
9871     ///
9872     void put(U)(EntropyAccumulator!(U, summation) e)
9873     {
9874         summator.put(e.summator.sum);
9875     }
9876 }
9877 
9878 /// test basic functionality
9879 version(mir_stat_test_uni)
9880 @safe pure nothrow
9881 unittest
9882 {
9883     import mir.math.common: approxEqual;
9884     import mir.ndslice.slice: sliced;
9885 
9886     EntropyAccumulator!(double, Summation.pairwise) x;
9887     x.put([0.1, 0.2, 0.3].sliced);
9888     assert(x.entropy.approxEqual(-0.913338));
9889     x.put(0.4);
9890     assert(x.entropy.approxEqual(-1.279854));
9891 }
9892 
9893 // test floats
9894 version(mir_stat_test_uni)
9895 @safe pure nothrow
9896 unittest
9897 {
9898     import mir.math.common: approxEqual;
9899     import mir.ndslice.slice: sliced;
9900 
9901     EntropyAccumulator!(float, Summation.pairwise) x;
9902     x.put([0.1, 0.2, 0.3].sliced);
9903     assert(x.entropy.approxEqual(-0.913338));
9904     x.put(0.4);
9905     assert(x.entropy.approxEqual(-1.279854));
9906 }
9907 
9908 // test put EntropyAccumulator
9909 version(mir_stat_test_uni)
9910 @safe pure nothrow
9911 unittest
9912 {
9913     import mir.math.common: approxEqual;
9914     import mir.ndslice.slice: sliced;
9915 
9916     auto a = [1.0, 2, 3,  4,  5,  6].sliced;
9917     auto b = [7.0, 8, 9, 10, 11, 12].sliced;
9918 
9919     auto x = a / 78.0;
9920     auto y = b / 78.0;
9921 
9922     EntropyAccumulator!(double, Summation.pairwise) m0;
9923     m0.put(x);
9924     assert(m0.entropy.approxEqual(-0.800844));
9925     EntropyAccumulator!(double, Summation.pairwise) m1;
9926     m1.put(y);
9927     assert(m1.entropy.approxEqual(-1.526653));
9928     m0.put(m1);
9929     assert(m0.entropy.approxEqual(-2.327497));
9930 }
9931 
9932 /++
9933 If `T` is a floating point type, this is an alias to the unqualified type.
9934 If `T` is not a floating point type, this will alias a `double` type if `T`
9935 is summable and implicitly convertible to a floating point type.
9936 +/
9937 package(mir)
9938 template entropyType(T)
9939 {
9940     import mir.math.sum: sumType;
9941 
9942     alias U = sumType!T;
9943     alias entropyType = statType!(U, false);
9944 }
9945 
9946 /++
9947 Computes the entropy of the input.
9948 By default, if `F` is not a floating point type, then the result will have a
9949 `double` type if `F` is implicitly convertible to a floating point type.
9950 Params:
9951     F = controls type of output
9952     summation = algorithm for summing the individual entropy values (default: Summation.appropriate)
9953 Returns:
9954     The entropy of all the elements in the input, must be floating point type
9955 See_also: 
9956     $(MATHREF sum, Summation)
9957 +/
9958 template entropy(F, Summation summation = Summation.appropriate)
9959 {
9960     import core.lifetime: move;
9961     import std.traits: isIterable;
9962 
9963     /++
9964     Params:
9965         r = range, must be finite iterable
9966     +/
9967     @fmamath entropyType!Range entropy(Range)(Range r)
9968         if (isIterable!Range)
9969     {
9970         alias G = typeof(return);
9971         EntropyAccumulator!(G, ResolveSummationType!(summation, Range, G)) entropyAccumulator;
9972         entropyAccumulator.put(r.move);
9973         return entropyAccumulator.entropy;
9974     }
9975 
9976     /++
9977     Params:
9978         ar = values
9979     +/
9980     @fmamath entropyType!F entropy(scope const F[] ar...)
9981     {
9982         alias G = typeof(return);
9983         EntropyAccumulator!(G, ResolveSummationType!(summation, const(G)[], G)) entropyAccumulator;
9984         entropyAccumulator.put(ar);
9985         return entropyAccumulator.entropy;
9986     }
9987 }
9988 
9989 ///
9990 template entropy(Summation summation = Summation.appropriate)
9991 {
9992     import core.lifetime: move;
9993     import std.traits: isIterable;
9994 
9995     /++
9996     Params:
9997         r = range, must be finite iterable
9998     +/
9999     @fmamath entropyType!Range entropy(Range)(Range r)
10000         if (isIterable!Range)
10001     {
10002         alias F = typeof(return);
10003         return .entropy!(F, summation)(r.move);
10004     }
10005 
10006     /++
10007     Params:
10008         ar = values
10009     +/
10010     @fmamath entropyType!T entropy(T)(scope const T[] ar...)
10011     {
10012         alias F = typeof(return);
10013         return .entropy!(F, summation)(ar);
10014     }
10015 }
10016 
10017 /// ditto
10018 template entropy(F, string summation)
10019 {
10020     mixin("alias entropy = .entropy!(F, Summation." ~ summation ~ ");");
10021 }
10022 
10023 /// ditto
10024 template entropy(string summation)
10025 {
10026     mixin("alias entropy = .entropy!(Summation." ~ summation ~ ");");
10027 }
10028 
10029 ///
10030 version(mir_stat_test_uni)
10031 @safe pure nothrow
10032 unittest
10033 {
10034     import mir.math.common: approxEqual;
10035     import mir.ndslice.slice: sliced;
10036 
10037     assert(entropy([0.166667, 0.333333, 0.50]).approxEqual(-1.011404));
10038 
10039     assert(entropy!float([0.05, 0.1, 0.15, 0.2, 0.25, 0.25].sliced(3, 2)).approxEqual(-1.679648));
10040 
10041     static assert(is(typeof(entropy!float([0.166667, 0.333333, 0.50])) == float));
10042 }
10043 
10044 /// Entropy of vector
10045 version(mir_stat_test_uni)
10046 @safe pure nothrow
10047 unittest
10048 {
10049     import mir.math.common: approxEqual;
10050     import mir.ndslice.slice: sliced;
10051 
10052     double[] a = [1.0, 2, 3,  4,  5,  6, 7, 8, 9, 10, 11, 12];
10053     a[] /= 78.0;
10054 
10055     auto x = a.sliced;
10056     assert(x.entropy.approxEqual(-2.327497));
10057 }
10058 
10059 /// Entropy of matrix
10060 version(mir_stat_test_uni)
10061 @safe pure
10062 unittest
10063 {
10064     import mir.math.common: approxEqual;
10065     import mir.ndslice.fuse: fuse;
10066 
10067     double[] a = [1.0, 2, 3,  4,  5,  6, 7, 8, 9, 10, 11, 12];
10068     a[] /= 78.0;
10069 
10070     auto x = a.fuse;
10071     assert(x.entropy.approxEqual(-2.327497));
10072 }
10073 
10074 /// Column entropy of matrix
10075 version(mir_stat_test_uni)
10076 @safe pure
10077 unittest
10078 {
10079     import mir.algorithm.iteration: all;
10080     import mir.math.common: approxEqual;
10081     import mir.ndslice.fuse: fuse;
10082     import mir.ndslice.topology: alongDim, byDim, map;
10083 
10084     double[][] a = [
10085         [1.0, 2, 3,  4,  5,  6], 
10086         [7.0, 8, 9, 10, 11, 12]
10087     ];
10088     a[0][] /= 78.0;
10089     a[1][] /= 78.0;
10090 
10091     auto x = a.fuse;
10092     auto result = [-0.272209, -0.327503, -0.374483, -0.415678, -0.452350, -0.485273];
10093 
10094     // Use byDim or alongDim with map to compute entropy of row/column.
10095     assert(x.byDim!1.map!entropy.all!approxEqual(result));
10096     assert(x.alongDim!0.map!entropy.all!approxEqual(result));
10097 
10098     // FIXME
10099     // Without using map, computes the entropy of the whole slice
10100     // assert(x.byDim!1.entropy == x.sliced.entropy);
10101     // assert(x.alongDim!0.entropy == x.sliced.entropy);
10102 }
10103 
10104 /// Can also set algorithm or output type
10105 version(mir_stat_test_uni)
10106 @safe pure nothrow
10107 unittest
10108 {
10109     import mir.math.common: approxEqual;
10110     import mir.ndslice.slice: sliced;
10111     import mir.ndslice.topology: repeat;
10112 
10113     auto a = [1, 1e100, 1, 1e100].sliced;
10114 
10115     auto x = a * 10_000;
10116 
10117     assert(x.entropy!"kbn".approxEqual(4.789377e106));
10118     assert(x.entropy!"kb2".approxEqual(4.789377e106));
10119     assert(x.entropy!"precise".approxEqual(4.789377e106));
10120     assert(x.entropy!(double, "precise").approxEqual(4.789377e106));
10121 }
10122 
10123 /++
10124 For integral slices, pass output type as template parameter to ensure output
10125 type is correct.
10126 +/
10127 version(mir_stat_test_uni)
10128 @safe pure nothrow
10129 unittest
10130 {
10131     import mir.math.common: approxEqual;
10132     import mir.ndslice.slice: sliced;
10133 
10134     auto x = [3, 1, 1, 2, 4, 4,
10135               2, 7, 5, 1, 2, 3].sliced;
10136 
10137     auto y = x.entropy;
10138     assert(y.approxEqual(43.509472));
10139     static assert(is(typeof(y) == double));
10140 
10141     assert(x.entropy!float.approxEqual(43.509472f));
10142 }
10143 
10144 /// Arbitrary entropy
10145 version(mir_stat_test_uni)
10146 @safe pure nothrow @nogc
10147 unittest
10148 {
10149     import mir.math.common: approxEqual;
10150 
10151     assert(entropy(0.25, 0.25, 0.25, 0.25).approxEqual(-1.386294));
10152     assert(entropy!float(0.25, 0.25, 0.25, 0.25).approxEqual(-1.386294));
10153 }
10154 
10155 // Dynamic array / UFCS
10156 version(mir_stat_test_uni)
10157 @safe pure nothrow
10158 unittest
10159 {
10160     import mir.math.common: approxEqual;
10161 
10162     assert(entropy([0.25, 0.25, 0.25, 0.25]).approxEqual(-1.386294));
10163     assert([0.25, 0.25, 0.25, 0.25].entropy.approxEqual(-1.386294));
10164 }
10165 
10166 // Check type of alongDim result
10167 version(mir_stat_test_uni)
10168 @safe pure nothrow
10169 unittest
10170 {
10171     import mir.algorithm.iteration: all;
10172     import mir.math.common: approxEqual;
10173     import mir.ndslice.topology: iota, alongDim, map;
10174 
10175     auto x = iota([2, 2], 1);
10176     auto y = x.alongDim!1.map!entropy;
10177     assert(y.all!approxEqual([1.386294, 8.841014]));
10178     static assert(is(entropyType!(typeof(y)) == double));
10179 }
10180 
10181 // @nogc test
10182 version(mir_stat_test_uni)
10183 @safe pure @nogc nothrow
10184 unittest
10185 {
10186     import mir.math.common: approxEqual;
10187     import mir.ndslice.slice: sliced;
10188 
10189     static immutable x = [1.0 / 78,  2.0 / 78,  3.0 / 78,  4.0 / 78,
10190                           5.0 / 78,  6.0 / 78,  7.0 / 78,  8.0 / 78,
10191                           9.0 / 78, 10.0 / 78, 11.0 / 78, 12.0 / 78];
10192 
10193     assert(x.sliced.entropy.approxEqual(-2.327497));
10194     assert(x.sliced.entropy!float.approxEqual(-2.327497));
10195 }
10196 
10197 /++
10198 Calculates the coefficient of variation of the input.
10199 
10200 The coefficient of variation is calculated by dividing either the population or
10201 sample (default) standard deviation by the mean of the input. According to
10202 wikipedia, "the coefficient of variation should be computed computed for data
10203 measured on a ratio scale, that is, scales that have a meaningful zero and hence
10204 allow for relative comparison of two measurements." In addition, for "small- and
10205 moderately-sized datasets", the coefficient of variation is biased, even when
10206 using the sample standard deviation.
10207 
10208 By default, if `F` is not floating point type, then the result will have a
10209 `double` type if `F` is implicitly convertible to a floating point type.
10210 
10211 Params:
10212     F = controls type of output
10213     varianceAlgo = algorithm for calculating variance (default: VarianceAlgo.hybrid)
10214     summation = algorithm for calculating sums (default: Summation.appropriate)
10215 
10216 Returns:
10217     The coefficient of varition of the input, must be floating point type
10218 
10219 See_also:
10220     $(WEB en.wikipedia.org/wiki/Coefficient_of_variation, Coefficient of variation)
10221 +/
10222 template coefficientOfVariation(
10223     F, 
10224     VarianceAlgo varianceAlgo = VarianceAlgo.hybrid, 
10225     Summation summation = Summation.appropriate)
10226 {
10227     import mir.math.common: sqrt;
10228     import mir.math.sum: ResolveSummationType;
10229     import std.traits: isIterable;
10230 
10231     /++
10232     Params:
10233         r = range, must be finite iterable
10234         isPopulation = true if population variance, false if sample variance (default)
10235     +/
10236     @fmamath stdevType!F coefficientOfVariation(Range)(Range r, bool isPopulation = false)
10237         if (isIterable!Range)
10238     {
10239         import core.lifetime: move;
10240 
10241         alias G = typeof(return);
10242         auto varianceAccumulator = VarianceAccumulator!(G, varianceAlgo, ResolveSummationType!(summation, Range, G))(r.move);
10243         assert(varianceAccumulator.mean!G > 0, "coefficientOfVariation: mean must be larger than zero");
10244         return varianceAccumulator.variance!G(isPopulation).sqrt / varianceAccumulator.mean!G;
10245     }
10246 
10247     /++
10248     Params:
10249         ar = values
10250     +/
10251     @fmamath stdevType!F coefficientOfVariation(scope const F[] ar...)
10252     {
10253         alias G = typeof(return);
10254         auto varianceAccumulator = VarianceAccumulator!(G, varianceAlgo, ResolveSummationType!(summation, const(G)[], G))(ar);
10255         assert(varianceAccumulator.mean!G > 0, "coefficientOfVariation: mean must be larger than zero");
10256         return varianceAccumulator.variance!G(false).sqrt / varianceAccumulator.mean!G;
10257     }
10258 }
10259 
10260 /// ditto
10261 template coefficientOfVariation(
10262     VarianceAlgo varianceAlgo = VarianceAlgo.hybrid, 
10263     Summation summation = Summation.appropriate)
10264 {
10265     import std.traits: isIterable;
10266 
10267     /++
10268     Params:
10269         r = range, must be finite iterable
10270         isPopulation = true if population variance, false if sample variance (default)
10271     +/
10272     @fmamath stdevType!Range coefficientOfVariation(Range)(Range r, bool isPopulation = false)
10273         if (isIterable!Range)
10274     {
10275         import core.lifetime: move;
10276 
10277         alias F = typeof(return);
10278         return .coefficientOfVariation!(F, varianceAlgo, summation)(r.move, isPopulation);
10279     }
10280 
10281     /++
10282     Params:
10283         ar = values
10284     +/
10285     @fmamath stdevType!T coefficientOfVariation(T)(scope const T[] ar...)
10286     {
10287         alias F = typeof(return);
10288         return .coefficientOfVariation!(F, varianceAlgo, summation)(ar);
10289     }
10290 }
10291 
10292 ///
10293 template coefficientOfVariation(F, string varianceAlgo, string summation = "appropriate")
10294 {
10295     mixin("alias coefficientOfVariation = .coefficientOfVariation!(F, VarianceAlgo." ~ varianceAlgo ~ ", Summation." ~ summation ~ ");");
10296 }
10297 
10298 /// ditto
10299 template coefficientOfVariation(string varianceAlgo, string summation = "appropriate")
10300 {
10301     mixin("alias coefficientOfVariation = .coefficientOfVariation!(VarianceAlgo." ~ varianceAlgo ~ ", Summation." ~ summation ~ ");");
10302 }
10303 
10304 ///
10305 version(mir_stat_test)
10306 @safe pure nothrow
10307 unittest
10308 {
10309     import mir.math.common: approxEqual;
10310     import mir.ndslice.slice: sliced;
10311 
10312     assert(coefficientOfVariation([1.0, 2, 3]).approxEqual(1.0 / 2.0));
10313     assert(coefficientOfVariation([1.0, 2, 3], true).approxEqual(0.816497 / 2.0));
10314 
10315     assert(coefficientOfVariation!float([0, 1, 2, 3, 4, 5].sliced(3, 2)).approxEqual(1.870829 / 2.5));
10316 
10317     static assert(is(typeof(coefficientOfVariation!float([1, 2, 3])) == float));
10318 }
10319 
10320 /// Coefficient of variation of vector
10321 version(mir_stat_test)
10322 @safe pure nothrow
10323 unittest
10324 {
10325     import mir.math.common: approxEqual;
10326     import mir.ndslice.slice: sliced;
10327 
10328     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
10329               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
10330 
10331     assert(x.coefficientOfVariation.approxEqual(2.231299 / 2.437500));
10332 }
10333 
10334 /// Coefficient of variation of matrix
10335 version(mir_stat_test)
10336 @safe pure
10337 unittest
10338 {
10339     import mir.math.common: approxEqual;
10340     import mir.ndslice.fuse: fuse;
10341 
10342     auto x = [
10343         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
10344         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
10345     ].fuse;
10346 
10347     assert(x.coefficientOfVariation.approxEqual(2.231299 / 2.437500));
10348 }
10349 
10350 /// Can also set algorithm type
10351 version(mir_stat_test)
10352 @safe pure nothrow
10353 unittest
10354 {
10355     import mir.math.common: approxEqual;
10356     import mir.ndslice.slice: sliced;
10357 
10358     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
10359               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
10360 
10361     auto x = a + 1_000_000_000;
10362 
10363     auto y = x.coefficientOfVariation;
10364     assert(y.approxEqual(2.231299 / 1_000_000_002.437500));
10365 
10366     // The naive variance algorithm is numerically unstable in this case, but
10367     // the difference is small as coefficientOfVariation is a ratio
10368     auto z0 = x.coefficientOfVariation!"naive";
10369     assert(!z0.approxEqual(y, 0x1p-20f, 0x1p-30f));
10370 
10371     // But the two-pass algorithm provides a consistent answer
10372     auto z1 = x.coefficientOfVariation!"twoPass";
10373     assert(z1.approxEqual(y));
10374 }
10375 
10376 /// Can also set algorithm or output type
10377 version(mir_stat_test)
10378 //@safe pure nothrow
10379 unittest
10380 {
10381     import mir.math.common: approxEqual;
10382     import mir.ndslice.slice: sliced;
10383 
10384     // Set population standard deviation, standardDeviation algorithm, sum algorithm or output type
10385 
10386     auto a = [1.0, 1e100, 1, -1e100].sliced;
10387     auto x = a * 10_000;
10388 
10389     bool populationTrue = true;
10390 
10391     /++
10392     For this case, failing to use a summation algorithm results in an assert
10393     error because the mean is zero due to floating point precision issues.
10394     +/
10395     //assert(x.coefficientOfVariation!("online").approxEqual(8.164966e103 / 0.0));
10396 
10397     /++
10398     Due to Floating Point precision, when centering `x`, subtracting the mean 
10399     from the second and fourth numbers has no effect. Further, after centering 
10400     and squaring `x`, the first and third numbers in the slice have precision 
10401     too low to be included in the centered sum of squares. 
10402     +/
10403     assert(x.coefficientOfVariation!("online", "kbn").approxEqual(8.164966e103 / 5000.0));
10404     assert(x.coefficientOfVariation!("online", "kb2").approxEqual(8.164966e103 / 5000.0));
10405     assert(x.coefficientOfVariation!("online", "precise").approxEqual(8.164966e103 / 5000.0));
10406     assert(x.coefficientOfVariation!(double, "online", "precise").approxEqual(8.164966e103 / 5000.0));
10407     assert(x.coefficientOfVariation!(double, "online", "precise")(populationTrue).approxEqual(7.071068e103 / 5000.0));
10408 
10409 
10410     auto y = [uint.max - 2, uint.max - 1, uint.max].sliced;
10411     auto z = y.coefficientOfVariation!ulong;
10412     assert(z == (1.0 / (cast(double) uint.max - 1)));
10413     static assert(is(typeof(z) == double));
10414     assert(y.coefficientOfVariation!(ulong, "online") == (1.0 / (cast(double) uint.max - 1)));
10415 }
10416 
10417 /++
10418 For integral slices, pass output type as template parameter to ensure output
10419 type is correct.
10420 +/
10421 version(mir_stat_test)
10422 @safe pure nothrow
10423 unittest
10424 {
10425     import mir.math.common: approxEqual;
10426     import mir.ndslice.slice: sliced;
10427 
10428     auto x = [0, 1, 1, 2, 4, 4,
10429               2, 7, 5, 1, 2, 0].sliced;
10430 
10431     auto y = x.coefficientOfVariation;
10432     assert(y.approxEqual(2.151462f / 2.416667));
10433     static assert(is(typeof(y) == double));
10434 
10435     assert(x.coefficientOfVariation!float.approxEqual(2.151462f / 2.416667));
10436 }
10437 
10438 /++
10439 coefficientOfVariation works for other user-defined types (provided they
10440 can be converted to a floating point)
10441 +/
10442 version(mir_stat_test)
10443 @safe pure nothrow
10444 unittest
10445 {
10446     import mir.math.common: approxEqual;
10447 
10448     static struct Foo {
10449         float x;
10450         alias x this;
10451     }
10452 
10453     Foo[] foo = [Foo(1f), Foo(2f), Foo(3f)];
10454     assert(foo.coefficientOfVariation.approxEqual(1f / 2f));
10455 }
10456 
10457 /// Arbitrary coefficientOfVariation
10458 version(mir_stat_test)
10459 @safe pure nothrow @nogc
10460 unittest
10461 {
10462     import mir.math.common: approxEqual;
10463 
10464     assert(coefficientOfVariation(1.0, 2, 3).approxEqual(1.0 / 2.0));
10465     assert(coefficientOfVariation!float(1, 2, 3).approxEqual(1f / 2f));
10466 }
10467 
10468 // Dynamic array / UFCS
10469 version(mir_stat_test)
10470 @safe pure nothrow
10471 unittest
10472 {
10473     import mir.math.common: approxEqual;
10474 
10475     assert(coefficientOfVariation([1.0, 2, 3, 4]).approxEqual(1.290994 / 2.50));
10476     assert([1.0, 2, 3, 4].coefficientOfVariation.approxEqual(1.290994 / 2.50));
10477 }
10478 
10479 // Check type of alongDim result
10480 version(mir_stat_test)
10481 @safe pure nothrow
10482 unittest
10483 {
10484     import mir.algorithm.iteration: all;
10485     import mir.math.common: approxEqual;
10486     import mir.ndslice.topology: iota, alongDim, map;
10487 
10488     auto x = iota([2, 2], 1);
10489     auto y = x.alongDim!1.map!coefficientOfVariation;
10490     assert(y.all!approxEqual([0.707107 / 1.50, 0.707107 / 3.50]));
10491     static assert(is(meanType!(typeof(y)) == double));
10492 }
10493 
10494 // @nogc test
10495 version(mir_stat_test)
10496 @safe pure @nogc nothrow
10497 unittest
10498 {
10499     import mir.math.common: approxEqual;
10500     import mir.ndslice.slice: sliced;
10501 
10502     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
10503                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
10504 
10505     assert(x.sliced.coefficientOfVariation.approxEqual(2.231299 / 2.437500));
10506     assert(x.sliced.coefficientOfVariation!float.approxEqual(2.231299 / 2.437500));
10507 }
10508 
10509 ///
10510 struct MomentAccumulator(T, size_t N, Summation summation)
10511     if (N > 0 && isMutable!T)
10512 {
10513     import std.traits: isIterable;
10514 
10515     ///
10516     Summator!(T, summation) summator;
10517 
10518     ///
10519     size_t count;
10520 
10521     ///
10522     F moment(F = T)() const @safe @property pure nothrow @nogc
10523     {
10524         return cast(F) summator.sum / cast(F) count;
10525     }
10526 
10527     ///
10528     F sumOfPower(F = T)() const @safe @property pure nothrow @nogc
10529     {
10530         return cast(F) summator.sum;
10531     }
10532 
10533     ///
10534     void put(Range)(Range r)
10535         if (isIterable!Range)
10536     {
10537         import mir.math.internal.powi: powi;
10538         import mir.primitives: hasShape;
10539 
10540         static if (hasShape!Range)
10541         {
10542             import mir.ndslice.topology: map;
10543             import mir.primitives: elementCount;
10544 
10545             count += r.elementCount;
10546             summator.put(r.map!(a => a.powi(N)));
10547         }
10548         else
10549         {
10550             foreach(x; r)
10551             {
10552                 put(x);
10553             }
10554         }
10555     }
10556 
10557     ///
10558     void put(Range)(Range r, T m)
10559         if (isIterable!Range)
10560     {
10561         import mir.math.internal.powi: powi;
10562         import mir.primitives: hasShape;
10563 
10564         static if (hasShape!Range)
10565         {
10566             import mir.ndslice.internal: LeftOp;
10567             import mir.ndslice.topology: vmap, map;
10568             import mir.primitives: elementCount;
10569 
10570             count += r.elementCount;
10571             static if (N == 1)
10572             {
10573                 summator.put(r.vmap(LeftOp!("-", T)(m))
10574                     );
10575             } else static if (N == 2) {
10576                 summator.put(r.vmap(LeftOp!("-", T)(m)).map!"a * a"
10577                     );
10578             } else {
10579                 summator.put(r.vmap(LeftOp!("-", T)(m)).
10580                                map!(a => a.powi(N))
10581                     );
10582             }
10583         }
10584         else
10585         {
10586             foreach(x; r)
10587             {
10588                 put(x, m);
10589             }
10590         }
10591     }
10592 
10593     ///
10594     void put(Range)(Range r, T m, T s)
10595         if (isIterable!Range)
10596     {
10597         import mir.math.internal.powi: powi;
10598         import mir.primitives: hasShape;
10599 
10600         static if (hasShape!Range)
10601         {
10602             import mir.ndslice.internal: LeftOp;
10603             import mir.ndslice.topology: vmap, map;
10604             import mir.primitives: elementCount;
10605 
10606             count += r.elementCount;
10607             static if (N == 1)
10608             {
10609                 summator.put(r.vmap(LeftOp!("-", T)(m)).
10610                                vmap(LeftOp!("*", T)(1 / s))
10611                     );
10612             } else static if (N == 2) {
10613                 summator.put(r.vmap(LeftOp!("-", T)(m)).
10614                                vmap(LeftOp!("*", T)(1 / s)).
10615                                map!"a * a"
10616                     );
10617             } else {
10618                 summator.put(r.vmap(LeftOp!("-", T)(m)).
10619                                vmap(LeftOp!("*", T)(1 / s)).
10620                                map!(a => a.powi(N))
10621                     );
10622             }
10623 
10624         }
10625         else
10626         {
10627             foreach(x; r)
10628             {
10629                 put(x, m, s);
10630             }
10631         }
10632     }
10633 
10634     ///
10635     void put()(T x)
10636     {
10637         import mir.math.internal.powi;
10638 
10639         count++;
10640         summator.put(x.powi(N));
10641     }
10642 
10643     ///
10644     void put()(MomentAccumulator!(T, N, summation) m)
10645     {
10646         count += m.count;
10647         summator.put(m.summator.sum);
10648     }
10649 
10650     ///
10651     this(Range)(Range r)
10652         if (isIterable!Range)
10653     {
10654         import core.lifetime: move;
10655         this.put(r.move);
10656     }
10657 
10658     ///
10659     this(Range)(Range r, T m)
10660         if (isIterable!Range)
10661     {
10662         import core.lifetime: move;
10663         this.put(r.move, m);
10664     }
10665 
10666     ///
10667     this(Range)(Range r, T m, T s)
10668         if (isIterable!Range)
10669     {
10670         import core.lifetime: move;
10671         this.put(r.move, m, s);
10672     }
10673 
10674     ///
10675     this()(T x)
10676     {
10677         this.put(x);
10678     }
10679 
10680     ///
10681     this()(T x, T m)
10682     {
10683         this.put(x, m);
10684     }
10685 
10686     ///
10687     this()(T x, T m, T s)
10688     {
10689         this.put(x, m, s);
10690     }
10691 }
10692 
10693 /// Raw moment
10694 version(mir_stat_test)
10695 @safe pure nothrow
10696 unittest
10697 {
10698     import mir.math.common: approxEqual;
10699     import mir.ndslice.slice: sliced;
10700     import mir.stat.transform: center;
10701 
10702     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
10703               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
10704     auto x = a.center;
10705 
10706     MomentAccumulator!(double, 2, Summation.naive) v;
10707     v.put(x);
10708 
10709     assert(v.moment.approxEqual(54.76562 / 12));
10710 
10711     v.put(4.0);
10712     assert(v.moment.approxEqual(70.76562 / 13));
10713 }
10714 
10715 // Raw Moment: test putting accumulator
10716 version(mir_stat_test)
10717 @safe pure nothrow
10718 unittest
10719 {
10720     import mir.math.common: approxEqual;
10721     import mir.ndslice.slice: sliced;
10722     import mir.stat.transform: center;
10723 
10724     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
10725               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
10726     auto b = a.center;
10727     auto x = b[0 .. 6];
10728     auto y = b[6 .. $];
10729 
10730     MomentAccumulator!(double, 2, Summation.naive) v;
10731     v.put(x);
10732     assert(v.moment.approxEqual(13.492188 / 6));
10733 
10734     MomentAccumulator!(double, 2, Summation.naive) w;
10735     w.put(y);
10736     v.put(w);
10737     assert(v.moment.approxEqual(54.76562 / 12));
10738 }
10739 
10740 // mir.complex test
10741 version(mir_stat_test)
10742 @safe pure nothrow
10743 unittest
10744 {
10745     import mir.complex;
10746     import mir.complex.math: approxEqual;
10747     import mir.ndslice.slice: sliced;
10748     import mir.stat.transform: center;
10749 
10750     alias C = Complex!double;
10751 
10752     auto a = [C(1, 3), C(2), C(3)].sliced;
10753     auto x = a.center;
10754 
10755     MomentAccumulator!(C, 2, Summation.naive) v;
10756     v.put(x);
10757     assert(v.moment.approxEqual(C(-4, -6) / 3));
10758 }
10759 
10760 // Raw Moment: test std.complex
10761 version(mir_stat_test)
10762 @safe pure nothrow
10763 unittest
10764 {
10765     import mir.ndslice.slice: sliced;
10766     import mir.stat.transform: center;
10767     import std.complex: Complex;
10768     import std.math.operations: isClose;
10769 
10770     auto a = [Complex!double(1.0, 3), Complex!double(2.0, 0), Complex!double(3.0, 0)].sliced;
10771     auto x = a.center;
10772 
10773     MomentAccumulator!(Complex!double, 2, Summation.naive) v;
10774     v.put(x);
10775     assert(v.moment.isClose(Complex!double(-4.0, -6.0) / 3));
10776 }
10777 
10778 /// Central moment
10779 version(mir_stat_test)
10780 @safe pure nothrow
10781 unittest
10782 {
10783     import mir.math.common: approxEqual;
10784     import mir.ndslice.slice: sliced;
10785     import mir.stat.transform: center;
10786 
10787     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
10788               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
10789 
10790     MomentAccumulator!(double, 2, Summation.naive) v;
10791     auto m = mean(x);
10792     v.put(x, m);
10793     assert(v.moment.approxEqual(54.76562 / 12));
10794 }
10795 
10796 // Central moment: dynamic array test
10797 version(mir_stat_test)
10798 @safe pure nothrow
10799 unittest
10800 {
10801     import mir.math.common: approxEqual;
10802     import mir.rc.array: RCArray;
10803 
10804     double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
10805                   2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
10806 
10807     MomentAccumulator!(double, 2, Summation.naive) v;
10808     auto m = mean(x);
10809     v.put(x, m);
10810     assert(v.sumOfPower.approxEqual(54.76562));
10811 }
10812 
10813 // Central moment: withAsSlice test
10814 version(mir_stat_test)
10815 @safe pure nothrow @nogc
10816 unittest
10817 {
10818     import mir.math.common: approxEqual;
10819     import mir.rc.array: RCArray;
10820 
10821     static immutable a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
10822                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
10823 
10824     auto x = RCArray!double(12);
10825     foreach(i, ref e; x)
10826         e = a[i];
10827 
10828     MomentAccumulator!(double, 2, Summation.naive) v;
10829     auto m = mean(x);
10830     v.put(x.asSlice.lightScope, m);
10831     assert(v.sumOfPower.approxEqual(54.76562));
10832 }
10833 
10834 // Central moment: Test N == 1
10835 version(mir_stat_test)
10836 @safe pure nothrow
10837 unittest
10838 {
10839     import mir.math.common: approxEqual;
10840     import mir.ndslice.slice: sliced;
10841 
10842     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
10843               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
10844 
10845     MomentAccumulator!(double, 1, Summation.naive) v;
10846     auto m = mean(x);
10847     v.put(x, m);
10848     assert(v.moment.approxEqual(0.0 / 12));
10849     assert(v.count == 12);
10850 }
10851 
10852 /// Standardized moment with scaled calculation
10853 version(mir_stat_test)
10854 @safe pure nothrow
10855 unittest
10856 {
10857     import mir.math.common: approxEqual, sqrt;
10858     import mir.ndslice.slice: sliced;
10859 
10860     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
10861               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
10862 
10863     auto u = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(x);
10864     MomentAccumulator!(double, 3, Summation.naive) v;
10865     v.put(x, u.mean, u.variance(true).sqrt);
10866     assert(v.moment.approxEqual(12.000999 / 12));
10867     assert(v.count == 12);
10868 }
10869 
10870 // standardized moment: dynamic array test
10871 version(mir_stat_test)
10872 @safe pure nothrow
10873 unittest
10874 {
10875     import mir.math.common: approxEqual, sqrt;
10876     import mir.rc.array: RCArray;
10877 
10878     double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
10879                   2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
10880 
10881     auto u = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(x);
10882     MomentAccumulator!(double, 3, Summation.naive) v;
10883     v.put(x, u.mean, u.variance(true).sqrt);
10884     assert(v.sumOfPower.approxEqual(12.000999));
10885 }
10886 
10887 // standardized moment: withAsSlice test
10888 version(mir_stat_test)
10889 @safe pure nothrow @nogc
10890 unittest
10891 {
10892     import mir.math.common: approxEqual, sqrt;
10893     import mir.rc.array: RCArray;
10894 
10895     static immutable a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
10896                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
10897 
10898     auto x = RCArray!double(12);
10899     foreach(i, ref e; x)
10900         e = a[i];
10901 
10902     auto u = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(x);
10903     MomentAccumulator!(double, 3, Summation.naive) v;
10904     v.put(x.asSlice.lightScope, u.mean, u.variance(true).sqrt);
10905     assert(v.sumOfPower.approxEqual(12.000999));
10906 }
10907 
10908 // standardized moment: Test N == 2
10909 version(mir_stat_test)
10910 @safe pure nothrow
10911 unittest
10912 {
10913     import mir.math.common: approxEqual, sqrt;
10914     import mir.ndslice.slice: sliced;
10915     import mir.stat.transform: center;
10916 
10917     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
10918               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
10919 
10920     auto u = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(x);
10921     MomentAccumulator!(double, 2, Summation.naive) v;
10922     v.put(x, u.mean, u.variance(true).sqrt);
10923     assert(v.moment.approxEqual(1.0));
10924     assert(v.count == 12);
10925 }
10926 
10927 // standardized moment: Test N == 1
10928 version(mir_stat_test)
10929 @safe pure nothrow
10930 unittest
10931 {
10932     import mir.math.common: approxEqual, sqrt;
10933     import mir.ndslice.slice: sliced;
10934 
10935     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
10936               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
10937 
10938     auto u = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(x);
10939     MomentAccumulator!(double, 1, Summation.naive) v;
10940     v.put(x, u.mean, u.variance(true).sqrt);
10941     assert(v.moment.approxEqual(0.0));
10942     assert(v.count == 12);
10943 }
10944 
10945 /++
10946 Calculates the n-th raw moment of the input.
10947 
10948 By default, if `F` is not floating point type or complex type, then the result
10949 will have a `double` type if `F` is implicitly convertible to a floating point 
10950 type or a type for which `isComplex!F` is true.
10951 
10952 Params:
10953     F = controls type of output
10954     N = controls n-th raw moment
10955     summation = algorithm for calculating sums (default: Summation.appropriate)
10956 
10957 Returns:
10958     The n-th raw moment of the input, must be floating point or complex type
10959 +/
10960 template rawMoment(F, size_t N, Summation summation = Summation.appropriate)
10961     if (N > 0)
10962 {
10963     import mir.math.sum: ResolveSummationType;
10964     import std.traits: isIterable;
10965 
10966     /++
10967     Params:
10968         r = range, must be finite iterable
10969     +/
10970     @fmamath meanType!F rawMoment(Range)(Range r)
10971         if (isIterable!Range)
10972     {
10973         import core.lifetime: move;
10974         
10975         alias G = typeof(return);
10976         MomentAccumulator!(G, N, ResolveSummationType!(summation, Range, G)) momentAccumulator;
10977         momentAccumulator.put(r.move);
10978         return momentAccumulator.moment;
10979     }
10980 
10981     /++
10982     Params:
10983         ar = values
10984     +/
10985     @fmamath meanType!F rawMoment(scope const F[] ar...)
10986     {
10987         alias G = typeof(return);
10988         MomentAccumulator!(G, N, ResolveSummationType!(summation, const(G)[], G)) momentAccumulator;
10989         momentAccumulator.put(ar);
10990         return momentAccumulator.moment;
10991     }
10992 }
10993 
10994 /// ditto
10995 template rawMoment(size_t N, Summation summation = Summation.appropriate)
10996     if (N > 0)
10997 {
10998     import std.traits: isIterable;
10999 
11000     /++
11001     Params:
11002         r = range, must be finite iterable
11003     +/
11004     @fmamath meanType!Range rawMoment(Range)(Range r)
11005         if (isIterable!Range)
11006     {
11007         import core.lifetime: move;
11008 
11009         alias F = typeof(return);
11010         return .rawMoment!(F, N, summation)(r.move);
11011     }
11012 
11013     /++
11014     Params:
11015         ar = values
11016     +/
11017     @fmamath meanType!T rawMoment(T)(scope const T[] ar...)
11018     {
11019         alias F = typeof(return);
11020         return .rawMoment!(F, N, summation)(ar);
11021     }
11022 }
11023 
11024 /// ditto
11025 template rawMoment(F, size_t N, string summation)
11026     if (N > 0)
11027 {
11028     mixin("alias rawMoment = .rawMoment!(F, N, Summation." ~ summation ~ ");");
11029 }
11030 
11031 /// ditto
11032 template rawMoment(size_t N, string summation)
11033     if (N > 0)
11034 {
11035     mixin("alias rawMoment = .rawMoment!(N, Summation." ~ summation ~ ");");
11036 }
11037 
11038 /// Basic implementation
11039 version(mir_stat_test)
11040 @safe pure nothrow
11041 unittest
11042 {
11043     import mir.math.common: approxEqual;
11044     import mir.ndslice.slice: sliced;
11045 
11046     assert(rawMoment!2([1.0, 2, 3]).approxEqual(14.0 / 3));
11047     assert(rawMoment!3([1.0, 2, 3]).approxEqual(36.0 / 3));
11048 
11049     assert(rawMoment!(float, 2)([0, 1, 2, 3, 4, 5].sliced(3, 2)).approxEqual(55f / 6));
11050     static assert(is(typeof(rawMoment!(float, 2)([1, 2, 3])) == float));
11051 }
11052 
11053 /// Raw Moment of vector
11054 version(mir_stat_test)
11055 @safe pure nothrow
11056 unittest
11057 {
11058     import mir.math.common: approxEqual;
11059     import mir.ndslice.slice: sliced;
11060     import mir.stat.transform: center;
11061 
11062     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
11063               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
11064     auto x = a.center;
11065 
11066     assert(x.rawMoment!2.approxEqual(54.76562 / 12));
11067 }
11068 
11069 /// Raw Moment of matrix
11070 version(mir_stat_test)
11071 @safe pure
11072 unittest
11073 {
11074     import mir.math.common: approxEqual;
11075     import mir.ndslice.fuse: fuse;
11076     import mir.stat.transform: center;
11077 
11078     auto a = [
11079         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
11080         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
11081     ].fuse;
11082     auto x = a.center;
11083 
11084     assert(x.rawMoment!2.approxEqual(54.76562 / 12));
11085 }
11086 
11087 /// Can also set algorithm or output type
11088 version(mir_stat_test)
11089 @safe pure nothrow
11090 unittest
11091 {
11092     import mir.math.common: approxEqual;
11093     import mir.ndslice.slice: sliced;
11094     import mir.ndslice.topology: repeat;
11095     import mir.stat.transform: center;
11096 
11097     //Set sum algorithm or output type
11098 
11099     auto a = [1.0, 1e100, 1, -1e100].sliced;
11100     auto b = a * 10_000;
11101     auto x = b.center;
11102 
11103     /++
11104     Due to Floating Point precision, when centering `x`, subtracting the mean 
11105     from the second and fourth numbers has no effect. Further, after centering 
11106     and squaring `x`, the first and third numbers in the slice have precision 
11107     too low to be included in the centered sum of squares. 
11108     +/
11109     assert(x.rawMoment!2.approxEqual(2.0e208 / 4));
11110 
11111     assert(x.rawMoment!(2, "kbn").approxEqual(2.0e208 / 4));
11112     assert(x.rawMoment!(2, "kb2").approxEqual(2.0e208 / 4));
11113     assert(x.rawMoment!(2, "precise").approxEqual(2.0e208 / 4));
11114     assert(x.rawMoment!(double, 2, "precise").approxEqual(2.0e208 / 4));
11115 
11116     auto y = uint.max.repeat(3);
11117     auto z = y.rawMoment!(ulong, 2);
11118     assert(z.approxEqual(cast(double) (cast(ulong) uint.max) ^^ 2u));
11119     static assert(is(typeof(z) == double));
11120 }
11121 
11122 // mir.complex test
11123 version(mir_stat_test)
11124 @safe pure nothrow
11125 unittest
11126 {
11127     import mir.complex: Complex;
11128     import mir.complex.math: approxEqual;
11129     import mir.ndslice.slice: sliced;
11130 
11131     alias C = Complex!double;
11132 
11133     auto x = [C(1, 2), C(2, 3), C(3, 4), C(4, 5)].sliced;
11134     assert(x.rawMoment!2.approxEqual(C(-24, 80) / 4));
11135 }
11136 
11137 /++
11138 rawMoment works for complex numbers and other user-defined types (that are either
11139 implicitly convertible to floating point or if `isComplex` is true)
11140 +/
11141 version(mir_stat_test)
11142 @safe pure nothrow
11143 unittest
11144 {
11145     import mir.ndslice.slice: sliced;
11146     import std.complex: Complex;
11147     import std.math.operations: isClose;
11148 
11149     auto x = [Complex!double(1, 2), Complex!double(2, 3), Complex!double(3, 4), Complex!double(4, 5)].sliced;
11150     assert(x.rawMoment!2.isClose(Complex!double(-24, 80)/ 4));
11151 }
11152 
11153 /// Arbitrary raw moment
11154 version(mir_stat_test)
11155 @safe pure nothrow @nogc
11156 unittest
11157 {
11158     import mir.math.common: approxEqual;
11159 
11160     assert(rawMoment!2(1.0, 2, 3).approxEqual(14.0 / 3));
11161     assert(rawMoment!(float, 2)(1, 2, 3).approxEqual(14f / 3));
11162 }
11163 
11164 // dynamic array test
11165 version(mir_stat_test)
11166 @safe pure nothrow
11167 unittest
11168 {
11169     import mir.math.common: approxEqual;
11170 
11171     assert([1.0, 2, 3, 4].rawMoment!2.approxEqual(30.0 / 4));
11172 }
11173 
11174 // @nogc test
11175 version(mir_stat_test)
11176 @safe pure nothrow @nogc
11177 unittest
11178 {
11179     import mir.math.common: approxEqual;
11180     import mir.ndslice.slice: sliced;
11181 
11182     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
11183                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
11184 
11185     assert(x.sliced.rawMoment!2.approxEqual(126.062500 / 12));
11186 }
11187 
11188 /++
11189 Calculates the n-th central moment of the input.
11190 
11191 By default, if `F` is not floating point type or complex type, then the result
11192 will have a `double` type if `F` is implicitly convertible to a floating point 
11193 type or a type for which `isComplex!F` is true.
11194 
11195 Params:
11196     F = controls type of output
11197     N = controls n-th central moment
11198     summation = algorithm for calculating sums (default: Summation.appropriate)
11199 
11200 Returns:
11201     The n-th central moment of the input, must be floating point or complex type
11202 +/
11203 template centralMoment(F, size_t N, Summation summation = Summation.appropriate)
11204     if (N > 0)
11205 {
11206     import mir.math.sum: ResolveSummationType;
11207     import std.traits: isIterable;
11208 
11209     /++
11210     Params:
11211         r = range, must be finite iterable
11212     +/
11213     @fmamath meanType!F centralMoment(Range)(Range r)
11214         if (isIterable!Range)
11215     {
11216         alias G = typeof(return);
11217         static if (N > 1) {
11218             MeanAccumulator!(G, ResolveSummationType!(summation, Range, G)) meanAccumulator;
11219             MomentAccumulator!(G, N, ResolveSummationType!(summation, Range, G)) momentAccumulator;
11220             meanAccumulator.put(r.lightScope);
11221             momentAccumulator.put(r, meanAccumulator.mean);
11222             return momentAccumulator.moment;
11223         } else {
11224             return cast(G) 0.0;
11225         }
11226     }
11227 
11228     /++
11229     Params:
11230         ar = values
11231     +/
11232     @fmamath meanType!F centralMoment(scope const F[] ar...)
11233     {
11234         alias G = typeof(return);
11235         static if (N > 1) {
11236             MeanAccumulator!(G, ResolveSummationType!(summation, const(G)[], G)) meanAccumulator;
11237             MomentAccumulator!(G, N, ResolveSummationType!(summation, const(G)[], G)) momentAccumulator;
11238             meanAccumulator.put(ar);
11239             momentAccumulator.put(ar, meanAccumulator.mean);
11240             return momentAccumulator.moment;
11241         } else {
11242             return cast(G) 0.0;
11243         }
11244     }
11245 }
11246 
11247 /// ditto
11248 template centralMoment(size_t N, Summation summation = Summation.appropriate)
11249     if (N > 0)
11250 {
11251     import std.traits: isIterable;
11252 
11253     /++
11254     Params:
11255         r = range, must be finite iterable
11256     +/
11257     @fmamath meanType!Range centralMoment(Range)(Range r)
11258         if (isIterable!Range)
11259     {
11260         import core.lifetime: move;
11261 
11262         alias F = typeof(return);
11263         return .centralMoment!(F, N, summation)(r.move);
11264     }
11265 
11266     /++
11267     Params:
11268         ar = values
11269     +/
11270     @fmamath meanType!T centralMoment(T)(scope const T[] ar...)
11271     {
11272         alias F = typeof(return);
11273         return .centralMoment!(F, N, summation)(ar);
11274     }
11275 }
11276 
11277 /// ditto
11278 template centralMoment(F, size_t N, string summation)
11279     if (N > 0)
11280 {
11281     mixin("alias centralMoment = .centralMoment!(F, N, Summation." ~ summation ~ ");");
11282 }
11283 
11284 /// ditto
11285 template centralMoment(size_t N, string summation)
11286     if (N > 0)
11287 {
11288     mixin("alias centralMoment = .centralMoment!(N, Summation." ~ summation ~ ");");
11289 }
11290 
11291 /// Basic implementation
11292 version(mir_stat_test)
11293 @safe pure nothrow
11294 unittest
11295 {
11296     import mir.math.common: approxEqual;
11297     import mir.ndslice.slice: sliced;
11298 
11299     assert(centralMoment!2([1.0, 2, 3]).approxEqual(2.0 / 3));
11300     assert(centralMoment!3([1.0, 2, 3]).approxEqual(0.0 / 3));
11301 
11302     assert(centralMoment!(float, 2)([0, 1, 2, 3, 4, 5].sliced(3, 2)).approxEqual(17.5f / 6));
11303     static assert(is(typeof(centralMoment!(float, 2)([1, 2, 3])) == float));
11304 }
11305 
11306 /// Central Moment of vector
11307 version(mir_stat_test)
11308 @safe pure nothrow
11309 unittest
11310 {
11311     import mir.math.common: approxEqual;
11312     import mir.ndslice.slice: sliced;
11313 
11314     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
11315               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
11316 
11317     assert(x.centralMoment!2.approxEqual(54.76562 / 12));
11318 }
11319 
11320 /// Central Moment of matrix
11321 version(mir_stat_test)
11322 @safe pure
11323 unittest
11324 {
11325     import mir.math.common: approxEqual;
11326     import mir.ndslice.fuse: fuse;
11327 
11328     auto x = [
11329         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
11330         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
11331     ].fuse;
11332 
11333     assert(x.centralMoment!2.approxEqual(54.76562 / 12));
11334 }
11335 
11336 /// Can also set algorithm or output type
11337 version(mir_stat_test)
11338 @safe pure nothrow
11339 unittest
11340 {
11341     import mir.math.common: approxEqual;
11342     import mir.ndslice.slice: sliced;
11343     import mir.ndslice.topology: repeat;
11344     import mir.stat.transform: center;
11345 
11346     //Set sum algorithm or output type
11347 
11348     auto a = [1.0, 1e100, 1, -1e100].sliced;
11349     auto b = a * 10_000;
11350     auto x = b.center;
11351 
11352     /++
11353     Due to Floating Point precision, when centering `x`, subtracting the mean 
11354     from the second and fourth numbers has no effect. Further, after centering 
11355     and squaring `x`, the first and third numbers in the slice have precision 
11356     too low to be included in the centered sum of squares. 
11357     +/
11358     assert(x.centralMoment!2.approxEqual(2.0e208 / 4));
11359 
11360     assert(x.centralMoment!(2, "kbn").approxEqual(2.0e208 / 4));
11361     assert(x.centralMoment!(2, "kb2").approxEqual(2.0e208 / 4));
11362     assert(x.centralMoment!(2, "precise").approxEqual(2.0e208 / 4));
11363     assert(x.centralMoment!(double, 2, "precise").approxEqual(2.0e208 / 4));
11364 
11365     auto y = uint.max.repeat(3);
11366     auto z = y.centralMoment!(ulong, 2);
11367     assert(z.approxEqual(0.0));
11368     static assert(is(typeof(z) == double));
11369 }
11370 
11371 // mir.complex test
11372 version(mir_stat_test)
11373 @safe pure nothrow
11374 unittest
11375 {
11376     import mir.complex: Complex;
11377     import mir.complex.math: approxEqual;
11378     import mir.ndslice.slice: sliced;
11379 
11380     alias C = Complex!double;
11381 
11382     auto x = [C(1, 2), C(2, 3), C(3, 4), C(4, 5)].sliced;
11383     assert(x.centralMoment!2.approxEqual(C(0, 10) / 4));
11384 }
11385 
11386 /++
11387 centralMoment works for complex numbers and other user-defined types (that are
11388 either implicitly convertible to floating point or if `isComplex` is true)
11389 +/
11390 version(mir_stat_test)
11391 @safe pure nothrow
11392 unittest
11393 {
11394     import mir.ndslice.slice: sliced;
11395     import std.complex: Complex;
11396     import std.math.operations: isClose;
11397 
11398     auto x = [Complex!double(1, 2), Complex!double(2, 3), Complex!double(3, 4), Complex!double(4, 5)].sliced;
11399     assert(x.centralMoment!2.isClose(Complex!double(0, 10) / 4));
11400 }
11401 
11402 /// Arbitrary central moment
11403 version(mir_stat_test)
11404 @safe pure nothrow @nogc
11405 unittest
11406 {
11407     import mir.math.common: approxEqual;
11408 
11409     assert(centralMoment!2(1.0, 2, 3).approxEqual(2.0 / 3));
11410     assert(centralMoment!(float, 2)(1, 2, 3).approxEqual(2f / 3));
11411 }
11412 
11413 // dynamic array test
11414 version(mir_stat_test)
11415 @safe pure nothrow
11416 unittest
11417 {
11418     import mir.math.common: approxEqual;
11419 
11420     assert([1.0, 2, 3, 4].centralMoment!2.approxEqual(5.0 / 4));
11421 }
11422 
11423 // @nogc test
11424 version(mir_stat_test)
11425 @safe pure nothrow @nogc
11426 unittest
11427 {
11428     import mir.math.common: approxEqual;
11429     import mir.ndslice.slice: sliced;
11430 
11431     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
11432                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
11433 
11434     assert(x.sliced.centralMoment!2.approxEqual(54.765625 / 12));
11435 }
11436 
11437 // test special casing
11438 version(mir_stat_test)
11439 @safe pure nothrow
11440 unittest
11441 {
11442     import mir.math.common: approxEqual;
11443 
11444     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
11445               2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
11446 
11447     assert(x.centralMoment!1.approxEqual(0.0 / 12));
11448 }
11449 
11450 ///
11451 enum StandardizedMomentAlgo
11452 {
11453     /// Calculates n-th standardized moment as E(((x - u) / sigma) ^^ N)
11454     scaled,
11455 
11456     /// Calculates n-th standardized moment as E(((x - u) ^^ N) / ((x - u) ^^ (N / 2)))
11457     centered
11458 }
11459 
11460 /++
11461 Calculates the n-th standardized moment of the input.
11462 
11463 By default, if `F` is not floating point type, then the result will have a
11464 `double` type if `F` is implicitly convertible to a floating point type.
11465 
11466 Params:
11467     F = controls type of output
11468     N = controls n-th standardized moment
11469     summation = algorithm for calculating sums (default: Summation.appropriate)
11470 
11471 Returns:
11472     The n-th standardized moment of the input, must be floating point
11473 +/
11474 template standardizedMoment(F, size_t N,
11475                             StandardizedMomentAlgo standardizedMomentAlgo = StandardizedMomentAlgo.scaled,
11476                             VarianceAlgo varianceAlgo = VarianceAlgo.twoPass,
11477                             Summation summation = Summation.appropriate)
11478     if (N > 0)
11479 {
11480     import mir.math.sum: ResolveSummationType;
11481     import std.traits: isIterable;
11482 
11483     /++
11484     Params:
11485         r = range, must be finite iterable
11486     +/
11487     @fmamath stdevType!F standardizedMoment(Range)(Range r)
11488         if (isIterable!Range)
11489     {
11490         alias G = typeof(return);
11491         static if (N > 2) {
11492             auto varianceAccumulator = VarianceAccumulator!(G, varianceAlgo, ResolveSummationType!(summation, Range, G))(r.lightScope);
11493             MomentAccumulator!(G, N, ResolveSummationType!(summation, Range, G)) momentAccumulator;
11494             static if (standardizedMomentAlgo == StandardizedMomentAlgo.scaled) {
11495                 import mir.math.common: sqrt;
11496 
11497                 momentAccumulator.put(r, varianceAccumulator.mean, varianceAccumulator.variance(true).sqrt);
11498                 return momentAccumulator.moment;
11499             } else static if (standardizedMomentAlgo == StandardizedMomentAlgo.centered) {
11500                 import mir.math.common: pow;
11501 
11502                 momentAccumulator.put(r, varianceAccumulator.mean);
11503                 return momentAccumulator.moment / pow(varianceAccumulator.variance(true), N / 2);
11504             }
11505         } else static if (N == 2) {
11506             return cast(G) 1.0;
11507         } else static if (N == 1) {
11508             return cast(G) 0.0;
11509         }
11510     }
11511 
11512     /++
11513     Params:
11514         ar = values
11515     +/
11516     @fmamath stdevType!F standardizedMoment(scope const F[] ar...)
11517     {
11518         alias G = typeof(return);
11519         static if (N > 2) {
11520             auto varianceAccumulator = VarianceAccumulator!(G, varianceAlgo, ResolveSummationType!(summation, const(G)[], G))(ar);
11521             MomentAccumulator!(G, N, ResolveSummationType!(summation, const(G)[], G)) momentAccumulator;
11522             static if (standardizedMomentAlgo == StandardizedMomentAlgo.scaled) {
11523                 import mir.math.common: sqrt;
11524 
11525                 momentAccumulator.put(ar, varianceAccumulator.mean, varianceAccumulator.variance(true).sqrt);
11526                 return momentAccumulator.moment;
11527             } else static if (standardizedMomentAlgo == StandardizedMomentAlgo.centered) {
11528                 import mir.math.common: pow;
11529 
11530                 momentAccumulator.put(ar, varianceAccumulator.mean);
11531                 return momentAccumulator.moment / pow(varianceAccumulator.variance(true), N / 2);
11532             }
11533         } else static if (N == 2) {
11534             return cast(G) 1.0;
11535         } else static if (N == 1) {
11536             return cast(G) 0.0;
11537         }
11538     }
11539 }
11540 
11541 /// ditto
11542 template standardizedMoment(size_t N,
11543                             StandardizedMomentAlgo standardizedMomentAlgo = StandardizedMomentAlgo.scaled,
11544                             VarianceAlgo varianceAlgo = VarianceAlgo.twoPass,
11545                             Summation summation = Summation.appropriate)
11546     if (N > 0)
11547 {
11548     import std.traits: isIterable;
11549 
11550     /++
11551     Params:
11552         r = range, must be finite iterable
11553     +/
11554     @fmamath stdevType!Range standardizedMoment(Range)(Range r)
11555         if (isIterable!Range)
11556     {
11557         import core.lifetime: move;
11558 
11559         alias F = typeof(return);
11560         return .standardizedMoment!(F, N, standardizedMomentAlgo, varianceAlgo, summation)(r.move);
11561     }
11562 
11563     /++
11564     Params:
11565         ar = values
11566     +/
11567     @fmamath stdevType!T standardizedMoment(T)(scope const T[] ar...)
11568     {
11569         alias F = typeof(return);
11570         return .standardizedMoment!(F, N, standardizedMomentAlgo, varianceAlgo, summation)(ar);
11571     }
11572 }
11573 
11574 /// ditto
11575 template standardizedMoment(F, size_t N, string standardizedMomentAlgo, string varianceAlgo = "twoPass", string summation = "appropriate")
11576     if (N > 0)
11577 {
11578     mixin("alias standardizedMoment = .standardizedMoment!(F, N, StandardizedMomentAlgo." ~ standardizedMomentAlgo ~ ", VarianceAlgo." ~ varianceAlgo ~ ", Summation." ~ summation ~ ");");
11579 }
11580 
11581 /// ditto
11582 template standardizedMoment(size_t N, string standardizedMomentAlgo, string varianceAlgo = "twoPass", string summation = "appropriate")
11583     if (N > 0)
11584 {
11585     mixin("alias standardizedMoment = .standardizedMoment!(N, StandardizedMomentAlgo." ~ standardizedMomentAlgo ~ ", VarianceAlgo." ~ varianceAlgo ~ ", Summation." ~ summation ~ ");");
11586 }
11587 
11588 /// Basic implementation
11589 version(mir_stat_test)
11590 @safe pure nothrow
11591 unittest
11592 {
11593     import mir.math.common: approxEqual;
11594     import mir.ndslice.slice: sliced;
11595 
11596     assert(standardizedMoment!1([1.0, 2, 3]).approxEqual(0.0));
11597     assert(standardizedMoment!2([1.0, 2, 3]).approxEqual(1.0));
11598     assert(standardizedMoment!3([1.0, 2, 3]).approxEqual(0.0 / 3));
11599     assert(standardizedMoment!4([1.0, 2, 3]).approxEqual(4.5 / 3));
11600 
11601     assert(standardizedMoment!(float, 2)([0, 1, 2, 3, 4, 5].sliced(3, 2)).approxEqual(6f / 6));
11602     static assert(is(typeof(standardizedMoment!(float, 2)([1, 2, 3])) == float));
11603 }
11604 
11605 /// Standardized Moment of vector
11606 version(mir_stat_test)
11607 @safe pure nothrow
11608 unittest
11609 {
11610     import mir.math.common: approxEqual;
11611     import mir.ndslice.slice: sliced;
11612 
11613     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
11614               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
11615 
11616     assert(x.standardizedMoment!3.approxEqual(12.000999 / 12));
11617 }
11618 
11619 /// Standardized Moment of matrix
11620 version(mir_stat_test)
11621 @safe pure
11622 unittest
11623 {
11624     import mir.math.common: approxEqual;
11625     import mir.ndslice.fuse: fuse;
11626 
11627     auto x = [
11628         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
11629         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
11630     ].fuse;
11631 
11632     assert(x.standardizedMoment!3.approxEqual(12.000999 / 12));
11633 }
11634 
11635 /// Can also set algorithm type
11636 version(mir_stat_test)
11637 @safe pure
11638 unittest
11639 {
11640     import mir.math.common: approxEqual;
11641     import mir.ndslice.slice: sliced;
11642 
11643     auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
11644               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
11645 
11646     auto x = a + 100_000_000_000;
11647 
11648     // The default algorithm is numerically stable in this case
11649     auto y = x.standardizedMoment!3;
11650     assert(y.approxEqual(12.000999 / 12));
11651 
11652     // The online algorithm is numerically unstable in this case
11653     auto z1 = x.standardizedMoment!(3, "scaled", "online");
11654     assert(!z1.approxEqual(12.000999 / 12));
11655     assert(!z1.approxEqual(y));
11656 
11657     // It is also numerically unstable when using StandardizedMomentAlgo.centered
11658     auto z2 = x.standardizedMoment!(3, "centered", "online");
11659     assert(!z2.approxEqual(12.000999 / 12));
11660     assert(!z2.approxEqual(y));
11661 }
11662 
11663 /// Can also set algorithm or output type
11664 version(mir_stat_test)
11665 @safe pure nothrow
11666 unittest
11667 {
11668     import mir.math.common: approxEqual;
11669     import mir.ndslice.slice: sliced;
11670 
11671     //Set standardized moment algorithm, variance algorithm, sum algorithm, or output type
11672 
11673     auto a = [1.0, 1e98, 1, -1e98].sliced;
11674     auto x = a * 10_000;
11675 
11676     /++
11677     Due to Floating Point precision, when centering `x`, subtracting the mean 
11678     from the second and fourth numbers has no effect. Further, after centering 
11679     and squaring `x`, the first and third numbers in the slice have precision 
11680     too low to be included in the centered sum of squares. 
11681     +/
11682     assert(x.standardizedMoment!3.approxEqual(0.0));
11683 
11684     assert(x.standardizedMoment!(3, "scaled", "online").approxEqual(0.0));
11685     assert(x.standardizedMoment!(3, "centered", "online").approxEqual(0.0));
11686     assert(x.standardizedMoment!(3, "scaled", "online", "kbn").approxEqual(0.0));
11687     assert(x.standardizedMoment!(3, "scaled", "online", "kb2").approxEqual(0.0));
11688     assert(x.standardizedMoment!(3, "scaled", "online", "precise").approxEqual(0.0));
11689     assert(x.standardizedMoment!(double, 3, "scaled", "online", "precise").approxEqual(0.0));
11690 
11691     auto y = [uint.max - 2, uint.max - 1, uint.max].sliced;
11692     auto z = y.standardizedMoment!(ulong, 3);
11693     assert(z == 0.0);
11694     static assert(is(typeof(z) == double));
11695 }
11696 
11697 /++
11698 For integral slices, can pass output type as template parameter to ensure output
11699 type is correct. By default, they get converted to double.
11700 +/
11701 version(mir_stat_test)
11702 @safe pure nothrow
11703 unittest
11704 {
11705     import mir.math.common: approxEqual;
11706     import mir.ndslice.slice: sliced;
11707 
11708     auto x = [0, 1, 1, 2, 4, 4,
11709               2, 7, 5, 1, 2, 0].sliced;
11710 
11711     auto y = x.standardizedMoment!3;
11712     assert(y.approxEqual(9.666455 / 12));
11713     static assert(is(typeof(y) == double));
11714 
11715     assert(x.standardizedMoment!(float, 3).approxEqual(9.666455f / 12));
11716 }
11717 
11718 /// Arbitrary standardized moment
11719 version(mir_stat_test)
11720 @safe pure nothrow @nogc
11721 unittest
11722 {
11723     import mir.math.common: approxEqual;
11724 
11725     assert(standardizedMoment!3(1.0, 2, 3).approxEqual(0.0 / 3));
11726     assert(standardizedMoment!(float, 3)(1, 2, 3).approxEqual(0f / 3));
11727     assert(standardizedMoment!(float, 3, "centered")(1, 2, 3).approxEqual(0f / 3));
11728 }
11729 
11730 // dynamic array test
11731 version(mir_stat_test)
11732 @safe pure nothrow
11733 unittest
11734 {
11735     import mir.math.common: approxEqual;
11736 
11737     assert([1.0, 2, 3, 4].standardizedMoment!3.approxEqual(0.0 / 4));
11738 }
11739 
11740 // @nogc test
11741 version(mir_stat_test)
11742 @safe pure nothrow @nogc
11743 unittest
11744 {
11745     import mir.math.common: approxEqual;
11746     import mir.ndslice.slice: sliced;
11747 
11748     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
11749                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
11750 
11751     assert(x.sliced.standardizedMoment!3.approxEqual(12.000999 / 12));
11752 }
11753 
11754 // test special casing
11755 version(mir_stat_test)
11756 @safe pure nothrow
11757 unittest
11758 {
11759     import mir.math.common: approxEqual;
11760 
11761     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
11762               2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
11763 
11764     assert(x.standardizedMoment!1.approxEqual(0.0 / 12));
11765 }
11766 
11767 ///
11768 enum MomentAlgo
11769 {
11770     /// nth raw moment, E(x ^^ n)
11771     raw,
11772 
11773     /// nth central moment, E((x - u) ^^ n)
11774     central,
11775 
11776     /// nth standardized moment, E(((x - u) / sigma) ^^ n)
11777     standardized
11778 }
11779 
11780 /++
11781 Calculates the n-th moment of the input.
11782 
11783 Params:
11784     F = controls type of output
11785     N = controls n-th standardized moment
11786     momentAlgo = type of moment to be calculated
11787     summation = algorithm for calculating sums (default: Summation.appropriate)
11788 
11789 Returns:
11790     The n-th moment of the input, must be floating point or complex type
11791 +/
11792 template moment(F, size_t N,
11793                 MomentAlgo momentAlgo,
11794                 Summation summation = Summation.appropriate)
11795 {
11796     import mir.math.sum: ResolveSummationType;
11797     import std.traits: isIterable;
11798 
11799     /++
11800     Params:
11801         r = range, must be finite iterable
11802     +/
11803     @fmamath meanType!F moment(Range)(Range r)
11804         if (isIterable!Range && momentAlgo != MomentAlgo.standardized)
11805     {
11806         import core.lifetime: move;
11807 
11808         alias G = typeof(return);
11809         static if (momentAlgo == MomentAlgo.raw) {
11810             return .rawMoment!(G, N, ResolveSummationType!(summation, Range, G))(r.move);
11811         } else static if (momentAlgo == MomentAlgo.central) {
11812             return .centralMoment!(G, N, ResolveSummationType!(summation, Range, G))(r.move);
11813         }
11814     }
11815 
11816     /++
11817     Params:
11818         r = range, must be finite iterable
11819     +/
11820     @fmamath stdevType!F moment(Range)(Range r)
11821         if (isIterable!Range && momentAlgo == MomentAlgo.standardized)
11822     {
11823         import core.lifetime: move;
11824 
11825         alias G = typeof(return);
11826         return .standardizedMoment!(G, N, StandardizedMomentAlgo.scaled, VarianceAlgo.twoPass, ResolveSummationType!(summation, Range, G))(r.move);
11827     }
11828 
11829     /++
11830     Params:
11831         ar = values
11832     +/
11833     @fmamath meanType!F moment()(scope const F[] ar...)
11834         if (momentAlgo != MomentAlgo.standardized)
11835     {
11836         alias G = typeof(return);
11837         static if (momentAlgo == MomentAlgo.raw) {
11838             return .rawMoment!(G, N, ResolveSummationType!(summation, const(G)[], G))(ar);
11839         } else static if (momentAlgo == MomentAlgo.central) {
11840             return .centralMoment!(G, N, ResolveSummationType!(summation, const(G)[], G))(ar);
11841         }
11842     }
11843 
11844     /++
11845     Params:
11846         ar = values
11847     +/
11848     @fmamath stdevType!F moment()(scope const F[] ar...)
11849         if (momentAlgo == MomentAlgo.standardized)
11850     {
11851         alias G = typeof(return);
11852         return .standardizedMoment!(G, N, StandardizedMomentAlgo.scaled, VarianceAlgo.twoPass, ResolveSummationType!(summation, const(G)[], G))(ar);
11853     }
11854 }
11855 
11856 /// ditto
11857 template moment(size_t N,
11858                 MomentAlgo momentAlgo,
11859                 Summation summation = Summation.appropriate)
11860 {
11861     import std.traits: isIterable;
11862 
11863     /++
11864     Params:
11865         r = range, must be finite iterable
11866     +/
11867     @fmamath stdevType!Range moment(Range)(Range r)
11868         if (isIterable!Range)
11869     {
11870         import core.lifetime: move;
11871 
11872         alias F = typeof(return);
11873         return .moment!(F, N, momentAlgo, summation)(r.move);
11874     }
11875 
11876     /++
11877     Params:
11878         ar = values
11879     +/
11880     @fmamath stdevType!T moment(T)(scope const T[] ar...)
11881     {
11882         alias F = typeof(return);
11883         return .moment!(F, N, momentAlgo, summation)(ar);
11884     }
11885 }
11886 
11887 /// ditto
11888 template moment(F, size_t N, string momentAlgo, string summation = "appropriate")
11889 {
11890     mixin("alias moment = .moment!(F, N, MomentAlgo." ~ momentAlgo ~ ", Summation." ~ summation ~ ");");
11891 }
11892 
11893 /// ditto
11894 template moment(size_t N, string momentAlgo, string summation = "appropriate")
11895 {
11896     mixin("alias moment = .moment!(N, MomentAlgo." ~ momentAlgo ~ ", Summation." ~ summation ~ ");");
11897 }
11898 
11899 /// Basic implementation
11900 version(mir_stat_test)
11901 @safe pure nothrow
11902 unittest
11903 {
11904     import mir.math.common: approxEqual;
11905     import mir.ndslice.slice: sliced;
11906 
11907     assert(moment!(1, "raw")([1.0, 2, 3]).approxEqual(6.0 / 3));
11908     assert(moment!(2, "raw")([1.0, 2, 3]).approxEqual(14.0 / 3));
11909     assert(moment!(3, "raw")([1.0, 2, 3]).approxEqual(36.0 / 3));
11910     assert(moment!(4, "raw")([1.0, 2, 3]).approxEqual(98.0 / 3));
11911 
11912     assert(moment!(1, "central")([1.0, 2, 3]).approxEqual(0.0 / 3));
11913     assert(moment!(2, "central")([1.0, 2, 3]).approxEqual(2.0 / 3));
11914     assert(moment!(3, "central")([1.0, 2, 3]).approxEqual(0.0 / 3));
11915     assert(moment!(4, "central")([1.0, 2, 3]).approxEqual(2.0 / 3));
11916 
11917     assert(moment!(1, "standardized")([1.0, 2, 3]).approxEqual(0.0));
11918     assert(moment!(2, "standardized")([1.0, 2, 3]).approxEqual(1.0));
11919     assert(moment!(3, "standardized")([1.0, 2, 3]).approxEqual(0.0 / 3));
11920     assert(moment!(4, "standardized")([1.0, 2, 3]).approxEqual(4.5 / 3));
11921 
11922     assert(moment!(float, 2, "standardized")([0, 1, 2, 3, 4, 5].sliced(3, 2)).approxEqual(6f / 6));
11923     static assert(is(typeof(moment!(float, 2, "standardized")([1, 2, 3])) == float));
11924 }
11925 
11926 /// Standardized Moment of vector
11927 version(mir_stat_test)
11928 @safe pure nothrow
11929 unittest
11930 {
11931     import mir.math.common: approxEqual;
11932     import mir.ndslice.slice: sliced;
11933 
11934     auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
11935               2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced;
11936 
11937     assert(x.moment!(3, "standardized").approxEqual(12.000999 / 12));
11938 }
11939 
11940 /// Standardized Moment of matrix
11941 version(mir_stat_test)
11942 @safe pure
11943 unittest
11944 {
11945     import mir.math.common: approxEqual;
11946     import mir.ndslice.fuse: fuse;
11947 
11948     auto x = [
11949         [0.0, 1.0, 1.5, 2.0, 3.5, 4.25],
11950         [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]
11951     ].fuse;
11952 
11953     assert(x.moment!(3, "standardized").approxEqual(12.000999 / 12));
11954 }
11955 
11956 /++
11957 For integral slices, can pass output type as template parameter to ensure output
11958 type is correct. By default, they get converted to double.
11959 +/
11960 version(mir_stat_test)
11961 @safe pure nothrow
11962 unittest
11963 {
11964     import mir.math.common: approxEqual;
11965     import mir.ndslice.slice: sliced;
11966 
11967     auto x = [0, 1, 1, 2, 4, 4,
11968               2, 7, 5, 1, 2, 0].sliced;
11969 
11970     auto y = x.moment!(3, "standardized");
11971     assert(y.approxEqual(9.666455 / 12));
11972     static assert(is(typeof(y) == double));
11973 
11974     assert(x.moment!(float, 3, "standardized").approxEqual(9.666455f / 12));
11975 }
11976 
11977 /// Arbitrary standardized moment
11978 version(mir_stat_test)
11979 @safe pure nothrow @nogc
11980 unittest
11981 {
11982     import mir.math.common: approxEqual;
11983 
11984     assert(moment!(3, "standardized")(1.0, 2, 3).approxEqual(0.0 / 3));
11985     assert(moment!(float, 3, "standardized")(1, 2, 3).approxEqual(0f / 3));
11986 }
11987 
11988 // dynamic array test
11989 version(mir_stat_test)
11990 @safe pure nothrow
11991 unittest
11992 {
11993     import mir.math.common: approxEqual;
11994 
11995     assert([1.0, 2, 3, 4].moment!(3, "standardized").approxEqual(0.0 / 4));
11996 }
11997 
11998 // @nogc test
11999 version(mir_stat_test)
12000 @safe pure nothrow @nogc
12001 unittest
12002 {
12003     import mir.math.common: approxEqual;
12004     import mir.ndslice.slice: sliced;
12005 
12006     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
12007                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
12008 
12009     assert(x.sliced.moment!(3, "standardized").approxEqual(12.000999 / 12));
12010 }