1 /++
2 Various statistics functions on ndslice. (e.g., bincount, var, logsumexp)
3  +/
4 module numir.stats;
5 
6 import std.traits : isUnsigned, isIntegral, isFloatingPoint, isBoolean;
7 import std.meta : allSatisfy, anySatisfy;
8 
9 import mir.ndslice.slice : isSlice, Slice, SliceKind;
10 import mir.primitives : DimensionCount, elementCount;
11 
12 // version = test_deprecated;
13 
14 /// bincountable type is a one-dimentional slice with unsigned elements
15 enum isBincountable(T) = isUnsigned!(typeof(T.init.front)) && isSlice!T;
16 
17 /++
18 Count number of occurrences of each value in slice of non-negative ints.
19 Params:
20     xs = input slice
21     minlength = a minimum number of bins for the output array
22 Returns:
23     size_t slice of number of ocurrence
24 TODO:
25     support @nogc
26  +/
27 pure nothrow @safe
28 auto bincount(T)(T xs, size_t minlength=0) if (isBincountable!T)
29 {
30     import mir.algorithm.iteration : each, reduce;
31     import numir.core : zeros, resize;
32 
33     auto ret = zeros!size_t(minlength);
34     auto maxx = minlength;
35     xs.each!((x) {
36         if (ret.length < x+1) {
37             maxx = x+1;
38             ret = ret.resize(x+1);
39         }
40         ret[x] += 1;
41     });
42     return ret[0 .. maxx];
43 }
44 
45 /++
46 Count weighted number of occurrences of each value in slice of non-negative ints.
47 Note that empty weight causes compiler error.
48 Params:
49     xs = input slice
50     weights = weights slice of the same length as `xs`
51     minlength = a minimum number of bins for the output array
52 Returns:
53     slice like weights of weighted number of ocurrences
54 TODO:
55     support @nogc
56  +/
57 pure nothrow @safe
58 auto bincount(T, W)(T xs, W weights, size_t minlength=0) if (isBincountable!T && isSlice!W)
59 in
60 {
61     assert(xs.length == weights.length);
62 }
63 do
64 {
65     import numir.core : zeros, resize;
66     import mir.ndslice.slice : DeepElementType;
67 
68     alias D = DeepElementType!(typeof(weights));
69     auto wsh = weights.shape;
70     wsh[0] = minlength;
71     auto ret = zeros!D(wsh);
72     size_t maxx = 0;
73     // TODO use mir.algorithm.iteration.each
74     foreach (i; 0 .. xs.length) {
75         auto x = xs[i];
76         if (ret.length < x+1) {
77             maxx = x+1;
78             ret = ret.resize(x+1);
79         }
80         static if (DimensionCount!W == 1)
81         {
82             ret[x] += weights[i];
83         }
84         else
85         {
86             ret[x][] += weights[i];
87         }
88     }
89     return ret[0 .. maxx];
90 }
91 
92 ///
93 pure @safe
94 unittest
95 {
96     import numir : bincount;
97     import mir.ndslice : sliced, iota, fuse;
98 
99     auto ys = [0, 1, 1, 0, 1].sliced!size_t;
100     assert(ys.bincount == [2, 3]);
101     assert(ys.bincount(iota(ys.length)) == [0+3, 1+2+4]);
102     assert(ys.bincount([[1, 0], [-1, 0], [-1, 0], [1, 0], [-1, 0]].fuse) == [[2, 0], [-3,0]]);
103     assert([].sliced!size_t.bincount == [].sliced!size_t);
104     assert([].sliced!size_t.bincount([].sliced!double) == [].sliced!double);
105 }
106 
107 
108 /+
109 Computes the median of a slice. Internally, this function allocates copy of x to sort.
110 
111 Params: x = input slice
112 
113 Returns: a median value if x.length is the odd number, otherwise mean of two median values
114 
115 See_Also: https://docs.scipy.org/doc/numpy/reference/generated/numpy.median.html
116  +/
117 pure nothrow @safe
118 auto median(S)(S x) if (isSlice!S)
119 in
120 {
121     assert(x.length > 0);
122 }
123 do
124 {
125     import std.algorithm.sorting : isSorted;
126     import std.traits : Unqual;
127     import mir.ndslice.slice : DeepElementType;
128     import mir.ndslice.allocation : slice;
129     import mir.ndslice.sorting : sort;
130     import numir.core : empty;
131 
132     immutable i = x.length / 2;
133     if (x.isSorted)
134  {
135         return x.length % 2 == 1 ? x[i]
136             : 0.5 * (x[i-1] + x[i]);
137     }
138     alias R = Unqual!(DeepElementType!S);
139     auto s = empty!R(x.shape);
140     s[] = x;
141     s.sort;
142 
143     return x.length % 2 == 1 ? s[i]
144         : 0.5 * (s[i-1] + s[i]);
145 }
146 
147 ///
148 pure nothrow @safe
149 unittest
150 {
151     import mir.ndslice : map, alongDim, byDim, iota, slice, sliced;
152 
153     // sorted cases
154     assert(iota(5).median == 2);
155     // [[0,1],
156     //  [2,3],
157     //  [4,5]]
158     assert(iota(3, 2).byDim!1.map!median == [2, 3]);
159     assert(iota(3, 2).byDim!0.map!median == [0.5, 2.5, 4.5]);
160 
161     assert(iota(3, 2).alongDim!0.map!median == [2, 3]);
162     assert(iota(3, 2).alongDim!1.map!median == [0.5, 2.5, 4.5]);
163 
164     // not sorted cases
165     auto x = [1, 5, 1, 3, 4].sliced;
166     assert(x.median == 3);
167     auto y = [1, 5, 1, 3, 4, 4].sliced;
168     assert(y.median == 3.5);
169 
170     auto z = [1, 5, 1, 2, 4, 2,
171               1, 5, 1, 2, 4, 3,
172               1, 5, 1, 3, 4, 3].sliced(3, 6);
173 
174     assert(z.byDim!1.map!median == [1, 5, 1, 2, 4, 3]);
175     assert(z.byDim!0.map!median.slice == [2.0, 2.5, 3.0]); // FIXME remove .slice here
176 
177     assert(z.alongDim!0.map!median == [1, 5, 1, 2, 4, 3]);
178     assert(z.alongDim!1.map!median == [2.0, 2.5, 3.0]);
179 }
180 
181 
182 nothrow @nogc: // everything bellow here is nothrow and @nogc.
183 
184 deprecated("use `import mir.math.stat: mean;`")
185 nothrow @nogc template mean(sumTemplateArgs ...)
186 {
187     import mir.ndslice.topology : as;
188     import mir.math.sum : sum;
189 
190     /++
191     Params:
192         slice = input slice
193     Returns:
194         mean of slice
195     +/
196     auto mean(S)(S slice) if (isSlice!S)
197     {
198         return slice.sum!(sumTemplateArgs) / slice.elementCount;
199     }
200 
201     deprecated // twice
202     auto mean(S, Seed)(S slice, Seed seed) if (isSlice!S)
203     {
204         return slice.sum!(sumTemplateArgs)(seed) / slice.elementCount;
205     }
206 }
207 
208 // maybe not need
209 deprecated("use `import mir.math.stat: mean;`")
210 auto mean(S)(S slice) if (isSlice!S)
211 {
212     return slice.mean!"appropriate";
213 }
214 
215 // maybe not need
216 deprecated("use `import mir.math.stat: mean;`")
217 auto mean(S, Seed)(S slice, Seed seed) if (isSlice!S)
218 {
219     return slice.mean!"appropriate"(seed);
220 }
221 
222 
223 version(test_deprecated)
224 @nogc pure nothrow
225 unittest
226 {
227     import mir.ndslice.slice : sliced;
228 
229     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
230                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
231     assert(x.sliced.mean == 29.25 / 12);
232 }
233 
234 version(test_deprecated)
235 @nogc pure nothrow
236 unittest
237 {
238     import mir.ndslice.slice : sliced;
239 
240     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
241                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
242     assert(x.sliced(2, 6).mean == 29.25 / 12);
243 }
244 
245 version(test_deprecated)
246 @nogc pure nothrow
247 unittest
248 {
249     import mir.ndslice.slice : sliced;
250     import mir.ndslice.topology : alongDim, byDim, map;
251 
252     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
253                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
254     static immutable result = [1, 4.25, 3.25, 1.5, 2.5, 2.125];
255 
256     // Use byDim or alongDim with map to compute mean of row/column.
257     assert(x.sliced(2, 6).byDim!1.map!mean == result);
258     assert(x.sliced(2, 6).alongDim!0.map!mean == result);
259 
260     // FIXME
261     // Without using map, computes the mean of the whole slice
262     // assert(x.sliced(2, 6).byDim!1.mean == x.sliced.mean);
263     // assert(x.sliced(2, 6).alongDim!0.mean == x.sliced.mean);
264 }
265 
266 version(test_deprecated)
267 @nogc nothrow
268 unittest
269 {
270     import mir.ndslice.slice: sliced;
271     import mir.ndslice.topology: map, repeat;
272 
273     //Set sum algorithm or output type
274     static immutable a = [1, 1e100, 1, -1e100];
275     auto x = a.sliced * 10_000;
276     assert(x.mean!"kbn" == 20_000 / 4);
277     assert(x.mean!"kb2" == 20_000 / 4);
278     assert(x.mean!"precise" == 20_000 / 4);
279     assert(x.mean!(double, "precise") == 20_000.0 / 4);
280 
281     //Provide a seed
282     auto y = uint.max.repeat(3);
283     assert(y.mean(ulong.init) == 12884901885 / 3);
284 }
285 
286 version(test_deprecated)
287 @nogc pure nothrow
288 unittest
289 {
290     import mir.ndslice.slice : sliced;
291     import std.math : approxEqual;
292 
293     static immutable x = [0, 1, 1, 2, 4, 4,
294                           2, 7, 5, 1, 2, 0];
295     assert(approxEqual(x.sliced.mean!double, 29.0 / 12, 1.0e-10));
296 }
297 
298 version(test_deprecated)
299 @nogc pure nothrow
300 unittest
301 {
302     import mir.ndslice.slice : sliced;
303 
304     static immutable cdouble[] x = [1.0 + 2i, 2 + 3i, 3 + 4i, 4 + 5i];
305     static immutable cdouble result = 2.5 + 3.5i;
306     assert(x.sliced.mean == result);
307 }
308 
309 
310 version(test_deprecated)
311 nothrow pure @safe @nogc
312 unittest
313 {
314     import numir : mean;
315     import mir.ndslice : alongDim, iota, as, map;
316     /*
317       [[0,1,2],
318        [3,4,5]]
319      */
320     auto x = iota(2, 3).as!double;
321     assert(x.mean == (5.0 / 2.0));
322 
323     static immutable m0 = [(0.0+3.0)/2.0, (1.0+4.0)/2.0, (2.0+5.0)/2.0];
324     assert(x.alongDim!0.map!mean == m0);
325     assert(x.alongDim!(-2).map!mean == m0);
326 
327     static immutable m1 = [(0.0+1.0+2.0)/3.0, (3.0+4.0+5.0)/3.0];
328     assert(x.alongDim!1.map!mean == m1);
329     assert(x.alongDim!(-1).map!mean == m1);
330 
331     assert(iota(2, 3, 4, 5).as!double.alongDim!0.map!mean == iota([3, 4, 5], 3 * 4 * 5 / 2));
332 }
333 
334 
335 /++
336 Computes the geometric mean of a slice.
337 The product in gmean is computed in logarithms to avoid FP overflows.
338 
339 Params:
340     slice = input slice
341 Returns:
342     geometric mean of slice
343 
344 See_Also:
345     @9il comment https://github.com/libmir/numir/pull/24#discussion_r168958617
346  +/
347 deprecated("use `import mir.math.stat: gmean;`")
348 pure nothrow @safe @nogc
349 auto gmean(S)(S slice) if (isSlice!S)
350 {
351     import mir.algorithm.iteration : each;
352     import mir.ndslice.slice : DeepElementType;
353     import mir.math.numeric : ProdAccumulator;
354     import mir.math.common : exp2, pow;
355     import std.math : ldexp;
356     import std.traits : Unqual;
357     alias D = Unqual!(DeepElementType!(typeof(slice)));
358 
359     ProdAccumulator!D pr;
360     slice.each!(e => pr.put(e));
361     auto y = cast(D) 1.0 / slice.elementCount;
362     auto z = y * pr.exp;
363     auto ep = cast(int) z;
364     auto ma = pr.prod.pow(y) * exp2(z - ep);
365     return ldexp(ma, ep);
366     /*
367       (pr.x * 2 ^^ pr.exp) ^^ y
368       = 2 ^^ (y * (log2(pr.x) + pr.exp))
369       = pr.x ^^ y * 2 ^^ z
370       = (pr.x ^^ y * 2 ^^ (z - floor(z))) * 2 ^^ (floor(z))
371      */
372 }
373 
374 /++
375 Computes the geometric mean of a slice.
376 
377 Params:
378     slice = input slice
379     seed = seed used to calculate product (should be 1 in most cases)
380 Returns:
381     geometric mean of slice
382 +/
383 deprecated("use `import mir.math.stat: gmean;`")
384 pure nothrow @safe @nogc
385 auto gmean(S, Seed)(S slice, Seed seed) if (isSlice!S)
386 {
387     import mir.algorithm.iteration : each;
388     import mir.ndslice.slice : DeepElementType;
389     import mir.math.numeric : ProdAccumulator;
390     import mir.math.common : exp2, pow;
391     import std.math : ldexp;
392     import std.traits : Unqual;
393 
394     import std.bitmanip : DoubleRep;
395     DoubleRep rep;
396     rep.value = seed;
397     ProdAccumulator!Seed pr;
398     pr.exp = rep.exponent;
399     rep.exponent = 1;
400     pr.x = rep.value * 0.5;
401     slice.each!(e => pr.put(e));
402     auto y = cast(Seed) 1.0 / slice.elementCount;
403     auto z = y * pr.exp;
404     auto ep = cast(int) z;
405     auto ma = pr.x.pow(y) * exp2(z - ep);
406     return ldexp(ma, ep);
407     /*
408       (seed * pr.x * 2 ^^ pr.exp) ^^ y
409       = 2 ^^ (y * (log2(seed * pr.x) + pr.exp))
410       = pr.x ^^ y * 2 ^^ z
411       = (pr.x ^^ y * 2 ^^ (z - floor(z))) * 2 ^^ (floor(z))
412      */
413 }
414 
415 /// Geometric mean of vector
416 version(test_deprecated)
417 pure nothrow @safe @nogc
418 unittest
419 {
420     import mir.ndslice.slice : sliced;
421     import std.math : approxEqual;
422 
423     static immutable x = [1.1, 0.99, 1.01, 1.2, 0.9, 1.05];
424 
425     auto y = x.sliced.gmean;
426     assert(approxEqual(y, 1.037513));
427 }
428 
429 /// Geometric mean of matrix
430 version(test_deprecated)
431 pure nothrow @safe @nogc
432 unittest
433 {
434     import mir.ndslice.slice : sliced;
435     import std.math : approxEqual;
436 
437     static immutable x = [1.1, 0.99, 1.01, 1.2, 0.9, 1.05];
438 
439     auto y = x.sliced(2, 3).gmean;
440     assert(approxEqual(y, 1.037513));
441 }
442 
443 /// Column geometric mean of matrix
444 version(test_deprecated)
445 pure nothrow @safe @nogc
446 unittest
447 {
448     import mir.ndslice.slice : sliced;
449     import mir.ndslice.topology : alongDim, byDim, map;
450     import numir : approxEqual;
451 
452     static immutable x = [1.1, 0.99, 1.01, 1.2, 0.9, 1.05];
453     static immutable y = [1.148913, 0.943928, 1.029806];
454     // Use byDim or alongDim with map to compute mean of row/column.
455     assert(approxEqual(x.sliced(2, 3).byDim!1.map!gmean, y.sliced));
456     assert(approxEqual(x.sliced(2, 3).alongDim!0.map!gmean, y.sliced));
457 }
458 
459 /// Geometric mean of vector with seed
460 version(test_deprecated)
461 pure nothrow @safe @nogc
462 unittest
463 {
464     import mir.ndslice.slice : sliced;
465     import std.math : approxEqual;
466 
467     enum l = 2.0 ^^ (double.max_exp - 1);
468     enum s = 2.0 ^^ -(double.max_exp - 1);
469     static immutable x = [l, l, l, s, s, s, 0.8 * 2.0 ^^ 10];
470 
471     real seed = 1;
472     auto y = x.sliced.gmean(seed);
473     assert(approxEqual(y, (0.8 * 2.0 ^^ 10) ^^ (1.0 / 7.0)));
474 }
475 
476 /++
477 Computes the harmonic mean of a slice.
478 
479 Params:
480     sumTemplateArgs = template arguments to pass to mir.math.sum (to compute the
481                       harmonic mean)
482 
483 See_also: $(MATHREF sum, sum)
484 +/
485 deprecated("use `import mir.math.stat: hmean;`")
486 nothrow @nogc template hmean(sumTemplateArgs...)
487 {
488     import mir.ndslice.slice : DeepElementType;
489 
490     /++
491     Params:
492         slice = input slice
493     Returns:
494         harmonic mean of slice
495     +/
496     auto hmean(S)
497                                            (S slice)
498         if (isFloatingPoint!(DeepElementType!(S)))
499     {
500         import mir.math.stat: mean;
501         return 1 / (1.0 / slice).mean!sumTemplateArgs;
502     }
503 
504     deprecated
505     auto hmean(S, Seed)
506                                 (S slice, Seed seed)
507         if (isFloatingPoint!(DeepElementType!(S)))
508     {
509         import mir.math.stat: mean;
510         return 1 / ((1.0 / slice).mean!(sumTemplateArgs) + seed);
511     }
512 }
513 
514 /// Harmonic mean of vector
515 version(test_deprecated)
516 pure nothrow @safe @nogc
517 unittest
518 {
519     import mir.ndslice.slice : sliced;
520     import std.math : approxEqual;
521 
522     static immutable x = [20.0, 100.0, 2000.0, 10.0, 5.0, 2.0];
523 
524     auto y = x.sliced.hmean;
525     assert(approxEqual(y, 6.97269));
526 }
527 
528 /// Harmonic mean of matrix
529 version(test_deprecated)
530 pure nothrow @safe @nogc
531 unittest
532 {
533     import mir.ndslice.slice : sliced;
534     import std.math : approxEqual;
535 
536     static immutable x = [20.0, 100.0, 2000.0, 10.0, 5.0, 2.0];
537 
538     auto y = x.sliced(2, 3).hmean;
539     assert(approxEqual(y, 6.97269));
540 }
541 
542 /// Column harmonic mean of matrix
543 version(test_deprecated)
544 pure nothrow @safe @nogc
545 unittest
546 {
547     import mir.ndslice.slice : sliced;
548     import mir.ndslice.topology : alongDim, byDim, map;
549     import numir : approxEqual;
550 
551     static immutable _x = [20.0, 100.0, 2000.0, 10.0, 5.0, 2.0];
552     auto x = _x.sliced(2, 3);
553     static immutable _y = [13.33333, 9.52381, 3.996004];
554     auto y = _y.sliced;
555 
556     // Use byDim or alongDim with map to compute mean of row/column.
557     assert(approxEqual(x.byDim!1.map!hmean, y));
558     assert(approxEqual(x.alongDim!0.map!hmean, y));
559 }
560 
561 /// Can also pass arguments to hmean
562 version(test_deprecated)
563 nothrow @nogc
564 unittest
565 {
566     import mir.ndslice.slice: sliced;
567     import mir.ndslice.topology: map, repeat;
568     import std.math : approxEqual;
569 
570     //Set sum algorithm or output type
571     static immutable _x = [1, 1e100, 1, -1e100];
572     auto x = _x.sliced * 10_000;
573     assert(approxEqual(x.hmean!"kbn", 20_000));
574     assert(approxEqual(x.hmean!"kb2", 20_000));
575     assert(approxEqual(x.hmean!"precise", 20_000));
576     assert(approxEqual(x.hmean!(double, "precise"), 20_000.0));
577 }
578 
579 /++
580 Lazily centers a slice.
581 
582 Params:
583     sumTemplateArgs = template arguments to pass to mir.math.sum (to compute the
584                       mean of the slice)
585 
586 See_also: $(MATHREF sum, sum)
587 +/
588 deprecated("use `import mir.math.stat: center;`")
589 nothrow pure @nogc template center(sumTemplateArgs...)
590 {
591     /++
592     Params:
593         slice = input slice
594     Returns:
595         centered slice
596     +/
597     auto center(S)(S slice) if (isSlice!S)
598     {
599         import mir.math.stat: mean;
600         return slice - slice.mean!(sumTemplateArgs);
601     }
602 
603     deprecated
604     auto center(S, Seed)(S slice, Seed seed) if (isSlice!S)
605     {
606         import mir.math.stat: mean;
607         return slice - (slice.mean!(sumTemplateArgs) + seed);
608     }
609 }
610 
611 /// Center vector
612 version(test_deprecated)
613 pure nothrow @nogc
614 unittest
615 {
616     import mir.ndslice.slice : sliced;
617 
618     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
619                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
620     static immutable result = [-2.4375, -1.4375, -0.9375, -0.4375,  1.0625,  1.8125,
621                                -0.4375,  5.0625,  2.5625, -1.4375, -0.9375, -2.4375];
622     assert(x.sliced.center == result.sliced);
623 }
624 
625 /// Center matrix
626 version(test_deprecated)
627 pure nothrow @nogc
628 unittest
629 {
630     import mir.ndslice.slice : sliced;
631 
632     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
633                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
634     static immutable result = [-2.4375, -1.4375, -0.9375, -0.4375,  1.0625,  1.8125,
635                                -0.4375,  5.0625,  2.5625, -1.4375, -0.9375, -2.4375];
636     assert(x.sliced(2, 6).center == result.sliced(2, 6));
637 }
638 
639 /++
640 Lazily centers a slice and then takes each value to a power.
641 
642 Params:
643     sumTemplateArgs = template arguments to pass to mir.math.sum (to compute the
644                       mean of the slice)
645 
646 See_also: $(MATHREF sum, sum)
647 +/
648 pure nothrow @nogc private template deviationsPow(sumTemplateArgs...)
649 {
650     import std.traits;
651     /++
652     Params:
653         slice = input slice
654         order = order of power
655     Returns:
656         centered slice with each each value taken to power
657     +/
658     auto deviationsPow(S, Order)
659         (S slice, in Order order) if (isSlice!S)
660     {
661         import mir.math.stat: mean;
662         return (slice - slice.mean!(sumTemplateArgs)) ^^ order;
663     }
664 
665     deprecated
666     auto deviationsPow(S, Order, Seed)
667         (S slice, in Order order, Seed seed) if (isSlice!S)
668     {
669         import mir.math.stat: mean;
670         return (slice - (slice.mean!(sumTemplateArgs) + seed)) ^^ order;
671     }
672 }
673 
674 /// Squared deviations of vector
675 pure nothrow @nogc
676 unittest
677 {
678     import mir.ndslice.slice : sliced;
679     import std.math : approxEqual;
680 
681     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
682                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
683     static immutable result = [5.941406, 2.066406, 0.878906, 0.191406, 1.128906, 3.285156,
684                                0.191406, 25.62891, 6.566406, 2.066406, 0.878906, 5.941406];
685 
686     // FIXME 2.0 -> 2 gives error
687     assert(approxEqual(x.sliced.deviationsPow(2.0), result.sliced));
688 }
689 
690 /// Squared deviations of matrix
691 pure nothrow @nogc
692 unittest
693 {
694     import mir.ndslice.slice : sliced;
695     import std.math : approxEqual;
696 
697     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
698                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
699     static immutable _result = [5.941406, 2.066406, 0.878906, 0.191406, 1.128906, 3.285156,
700                                 0.191406, 25.62891, 6.566406, 2.066406, 0.878906, 5.941406];
701     auto result = _result.sliced(2, 6);
702 
703     auto y = x.sliced(2, 6).deviationsPow(2);
704 
705     assert(approxEqual(y[0], result[0]));
706     assert(approxEqual(y[1], result[1]));
707 }
708 
709 /++
710 Computes the n-th central moment of a slice.
711 
712 Params:
713     sumTemplateArgs = template arguments to pass to mir.math.sum (to compute the
714                       mean of the slice and the relevant moment)
715 
716 See_also: $(MATHREF sum, sum)
717 +/
718 pure nothrow @nogc template moment(sumTemplateArgs...)
719 {
720     /++
721     Params:
722         slice = input slice
723         order = order of moment
724     Returns:
725         n-th central moment of slice
726     +/
727     auto moment(S, Order)
728         (S slice, in Order order) if (isSlice!S)
729     {
730         import mir.math.stat: mean;
731         return ((slice - slice.mean!sumTemplateArgs) ^^ order).mean!sumTemplateArgs;
732     }
733 
734     deprecated
735     auto moment(S, Order, Seed)
736         (S slice,  in Order order, Seed seed) if (isSlice!S)
737     {
738         import mir.math.stat: mean;
739         immutable(size_t) sliceSize = slice.size;
740         Seed seedMoment = seed; //so as to not re-use seed below
741         return ((slice - (slice.mean!sumTemplateArgs + seed)) ^^ order)
742             .mean!sumTemplateArgs(seedMoment);
743     }
744 }
745 
746 /// 2nd central moment of vector
747 pure nothrow @nogc
748 unittest
749 {
750     import std.math : approxEqual;
751     import mir.ndslice.slice : sliced;
752 
753     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
754                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
755 
756     // FIXME assert(approxEqual(x.sliced.moment(2), 54.76563 / 12));
757     assert(approxEqual(x.sliced.moment(2.0), 54.76563 / 12));
758 }
759 
760 /// 2nd central moment of matrix
761 pure nothrow @nogc
762 unittest
763 {
764     import mir.ndslice.slice : sliced;
765     import std.math : approxEqual;
766 
767     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
768                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
769 
770     assert(approxEqual(x.sliced(2, 6).moment(2.0), 54.76563 / 12));
771 }
772 
773 /// Row 2nd central moment of matrix
774 pure nothrow @safe @nogc
775 unittest
776 {
777     import mir.ndslice.slice : sliced;
778     import mir.ndslice.topology : byDim, alongDim, map;
779     import std.math : approxEqual;
780 
781     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
782                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
783     static immutable y = [2.092014, 6.722222];
784 
785     // Use byDim or alongDim with map to compute moment of row/column.
786     assert(approxEqual(x.sliced(2, 6).byDim!0.map!(a => a.moment(2.0)), y.sliced));
787     assert(approxEqual(x.sliced(2, 6).alongDim!1.map!(a => a.moment(2.0)), y.sliced));
788 }
789 
790 /++
791 Computes the variance of a slice.
792 
793 Params:
794     sumTemplateArgs = template arguments to pass to mir.math.sum (to compute the
795                       mean & variance of the slice)
796 
797 TODO:
798     merge fast implementation in https://github.com/libmir/numir/pull/22#issuecomment-366526194
799 
800 See_also: $(MATHREF sum, sum)
801 +/
802 deprecated("use `import mir.math.stat: variance;`")
803 pure nothrow @nogc template var(sumTemplateArgs...)
804 {
805     /++
806     Params:
807         slice = input slice
808         isPopulation = true (default) if computing population variance, false otherwise
809     Returns:
810         variance of slice
811     +/
812     auto var(S)(S slice, bool isPopulation = true) if (isSlice!S)
813     {
814         import mir.math.stat: mean;
815         size_t sliceSize = slice.elementCount;
816         auto v = ((slice - slice.mean!sumTemplateArgs) ^^ 2.0).mean!sumTemplateArgs;
817         if (!isPopulation) { v *= cast(double) sliceSize / (sliceSize - 1); }
818         return v;
819     }
820 
821     deprecated
822     auto var(S, Seed)(S slice, Seed seed, bool isPopulation = true) if (isSlice!S)
823     {
824         size_t sliceSize = slice.elementCount;
825         auto v = ((slice - (slice.mean!sumTemplateArgs + seed)) ^^ 2.0).mean!sumTemplateArgs;
826         if (!isPopulation) { v *= cast(double) sliceSize / (sliceSize - 1); }
827         return v;
828     }
829 }
830 
831 /// Variance of vector
832 version(test_deprecated)
833 pure nothrow @nogc
834 unittest
835 {
836     import std.math : approxEqual;
837     import mir.ndslice.slice : sliced;
838 
839     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
840                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
841 
842     assert(approxEqual(x.sliced.var, 54.76563 / 12));
843     assert(approxEqual(x.sliced.var(false), 54.76563 / 11));
844     assert(approxEqual(x.sliced.var(true), 54.76563 / 12));
845 }
846 
847 /// Variance of matrix
848 version(test_deprecated)
849 pure nothrow @nogc
850 unittest
851 {
852     import mir.ndslice.slice : sliced;
853     import std.math : approxEqual;
854 
855     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
856                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
857 
858     assert(approxEqual(x.sliced(2, 6).var, 54.76563 / 12));
859     assert(approxEqual(x.sliced(2, 6).var(false), 54.76563 / 11));
860     assert(approxEqual(x.sliced(2, 6).var(true), 54.76563 / 12));
861 }
862 
863 /// Row variance of matrix
864 version(test_deprecated)
865 pure nothrow @safe @nogc
866 unittest
867 {
868     import mir.ndslice.slice : sliced;
869     import mir.ndslice.topology : alongDim, byDim, map;
870     import numir : approxEqual;
871 
872     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
873                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
874     static immutable y = [2.092014, 6.722222];
875 
876     // Use byDim or alongDim with map to compute variance of row/column.
877     assert(approxEqual(x.sliced(2, 6).byDim!0.map!(a => a.var(true)), y.sliced));
878     assert(approxEqual(x.sliced(2, 6).alongDim!1.map!(a => a.var(true)), y.sliced));
879 }
880 
881 /// Compute variance tensors along specified dimention of tensors with the negative dim support
882 version(test_deprecated)
883 pure nothrow @safe @nogc
884 unittest
885 {
886     import mir.ndslice : alongDim, iota, map, as;
887     import numir : var;
888     /*
889       [[1, 2],
890        [3, 4]]
891      */
892     auto x = iota([2, 2], 1).as!double;
893     static immutable v0 = [1.0, 1.0];
894     static immutable v1 = [0.25, 0.25];
895     /*
896       [[1, 2, 3],
897        [4, 5, 6]]
898      */
899     auto y = iota([2, 3], 1).as!double;
900     static immutable v2 = [2.25, 2.25, 2.25];
901     // static foreach (faster; [true, false])
902     {
903         assert(x.alongDim!0.map!var == v0);
904         assert(x.alongDim!(-2).map!var == v0);
905 
906         assert(x.alongDim!1.map!var == v1);
907         assert(x.alongDim!(-1).map!var == v1);
908 
909         assert(y.alongDim!0.map!var == v2);
910         assert(y.alongDim!(-2).map!var == v2);
911     }
912 }
913 
914 /++
915 Computes the standard deviation of a slice.
916 
917 Params:
918 
919     sumTemplateArgs = template arguments to pass to mir.math.sum (to compute the
920                       mean & variance of the slice)
921 
922 See_also: $(MATHREF sum, sum)
923 +/
924 deprecated("use `import mir.math.stat: standardDeviation;`")
925 pure nothrow @nogc template std(sumTemplateArgs...)
926 {
927     import mir.math.common : sqrt;
928 
929     /++
930     Params:
931         slice = input slice
932         isPopulation = true (default) if computing population standard deviation, false
933                        otherwise
934     Returns:
935         standard deviation of slice
936     +/
937     auto std(S)(S slice, bool isPopulation = true) if (isSlice!S)
938     {
939         return slice.var!(sumTemplateArgs)(isPopulation).sqrt;
940     }
941 
942     deprecated
943     auto std(S, Seed)(S slice, Seed seed, bool isPopulation = false) if (isSlice!S)
944     {
945         return slice.var!(sumTemplateArgs)(seed, isPopulation).sqrt;
946     }
947 }
948 
949 /// Standard deviation of vector
950 version(test_deprecated)
951 pure nothrow @nogc
952 unittest
953 {
954     import std.math : approxEqual;
955     import mir.ndslice.slice : sliced;
956     import mir.math.common : sqrt;
957 
958     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
959                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
960 
961     assert(approxEqual(x.sliced.std, sqrt(54.76563 / 12)));
962     assert(approxEqual(x.sliced.std(false), sqrt(54.76563 / 11)));
963     assert(approxEqual(x.sliced.std(true), sqrt(54.76563 / 12)));
964 }
965 
966 /// Standard deviation of matrix
967 version(test_deprecated)
968 pure nothrow @nogc
969 unittest
970 {
971     import mir.ndslice.slice : sliced;
972     import std.math : approxEqual;
973     import mir.math.common : sqrt;
974 
975     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
976                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
977 
978     assert(approxEqual(x.sliced(2, 6).std, sqrt(54.76563 / 12)));
979     assert(approxEqual(x.sliced(2, 6).std(false), sqrt(54.76563 / 11)));
980     assert(approxEqual(x.sliced(2, 6).std(true), sqrt(54.76563 / 12)));
981 }
982 
983 /// Row standard deviation of matrix
984 version(test_deprecated)
985 pure nothrow @safe @nogc
986 unittest
987 {
988     import mir.ndslice.slice : sliced;
989     import mir.ndslice.topology : alongDim, byDim, map;
990     import mir.math.common : sqrt;
991     import numir : approxEqual;
992 
993     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
994                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
995     static immutable y = [sqrt(2.092014), sqrt(6.722222)];
996 
997     // Use byDim or alongDim or alongDim with map to compute variance of row/column.
998     assert(approxEqual(x.sliced(2, 6).byDim!0.map!(a => a.std(true)), y.sliced));
999     assert(approxEqual(x.sliced(2, 6).alongDim!1.map!(a => a.std(true)), y.sliced));
1000 }
1001 
1002 /++
1003 Computes the zscore of a slice.
1004 
1005 Params:
1006     sumTemplateArgs = template arguments to pass to mir.math.sum (to compute the
1007                       mean & standard deviation of the slice)
1008 
1009 See_also: $(MATHREF sum, sum)
1010 +/
1011 pure nothrow @nogc template zscore(sumTemplateArgs...)
1012 {
1013     import mir.math.sum : sum;
1014     import mir.math.common : sqrt;
1015 
1016     /++
1017     Params:
1018         slice = input slice
1019         isPopulation = true (default) if computing population standard
1020                        deviation, false otherwise
1021     Returns:
1022         zscore of slice
1023     +/
1024     auto zscore(S)(S slice, bool isPopulation = true) if (isSlice!S)
1025     {
1026         auto sliceSize = slice.elementCount;
1027         auto sliceCenter = slice - slice.sum!(sumTemplateArgs) / sliceSize;
1028         if (isPopulation == false) { sliceSize--; }
1029         auto sliceVar = (sliceCenter ^^ 2).sum!(sumTemplateArgs) / sliceSize;
1030         return sliceCenter / sliceVar.sqrt;
1031     }
1032 
1033     deprecated
1034     auto zscore(S, Seed)(S slice, Seed seed, bool isPopulation = true) if (isSlice!S)
1035     {
1036         size_t sliceSize = slice.elementCount;
1037         Seed seedVar = seed;        //so as to not re-use seed below
1038         auto sliceMean = slice.sum!(sumTemplateArgs)(seed) / sliceSize;
1039         auto sliceCenter = slice - sliceMean;
1040         if (isPopulation == false) { sliceSize--; }
1041         auto sliceVar = (sliceCenter ^^ 2).sum!(sumTemplateArgs)(seedVar) / sliceSize;
1042         return sliceCenter / sliceVar.sqrt;
1043     }
1044 }
1045 
1046 /// Z-score of vector
1047 pure nothrow @nogc
1048 unittest
1049 {
1050     import mir.ndslice.slice : sliced;
1051     import std.math : approxEqual;
1052 
1053     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
1054                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
1055     static immutable resultSample = [-1.09241, -0.64424, -0.42016, -0.19607, 0.47618, 0.812307,
1056                                      -0.19607, 2.268858, 1.148434, -0.64424, -0.42016, -1.09241];
1057     static immutable resultPop = [-1.14099, -0.67289, -0.43884, -0.20479, 0.497354, 0.848427,
1058                                   -0.20479, 2.369745, 1.199501, -0.67289, -0.43884, -1.14099];
1059 
1060     assert(approxEqual(x.sliced.zscore, resultPop.sliced));
1061     assert(approxEqual(x.sliced.zscore(false), resultSample.sliced));
1062     assert(approxEqual(x.sliced.zscore(true), resultPop.sliced));
1063 
1064 }
1065 
1066 /// Z-score of matrix
1067 pure nothrow @nogc
1068 unittest
1069 {
1070     import mir.ndslice.slice : sliced;
1071     import std.math : approxEqual;
1072 
1073     static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25,
1074                           2.0, 7.5, 5.0, 1.0, 1.5, 0.0];
1075     static immutable _resultSample = [-1.09241, -0.64424, -0.42016, -0.19607, 0.47618, 0.812307,
1076                                       -0.19607, 2.268858, 1.148434, -0.64424, -0.42016, -1.09241];
1077     auto resultSample = _resultSample.sliced(2, 6);
1078     static immutable _resultPop = [-1.14099, -0.67289, -0.43884, -0.20479, 0.497354, 0.848427,
1079                                    -0.20479, 2.369745, 1.199501, -0.67289, -0.43884, -1.14099];
1080     auto resultPop = _resultPop.sliced(2, 6);
1081 
1082     auto y1 = x.sliced(2, 6).zscore;
1083     assert(approxEqual(y1[0], resultPop[0]));
1084     assert(approxEqual(y1[1], resultPop[1]));
1085 
1086     auto y2 = x.sliced(2, 6).zscore(false);
1087     assert(approxEqual(y2[0], resultSample[0]));
1088     assert(approxEqual(y2[1], resultSample[1]));
1089 
1090     auto y3 = x.sliced(2, 6).zscore(true);
1091     assert(approxEqual(y3[0], resultPop[0]));
1092     assert(approxEqual(y3[1], resultPop[1]));
1093 }
1094 
1095 
1096 /++
1097 Computes the log of the sum of exponentials of input elements.
1098 
1099 Params:
1100     sumTemplateArgs = template arguments to pass to mir.math.sum (to compute the
1101                       mean & standard deviation of the slice)
1102 
1103 See_also: https://github.com/scipy/scipy/blob/maintenance/1.0.x/scipy/special/_logsumexp.py
1104 +/
1105 nothrow @nogc template logsumexp(sumTemplateArgs...)
1106 {
1107     import mir.algorithm.iteration : maxPos;
1108     import mir.ndslice.topology : map;
1109     import mir.math.common : exp, log;
1110     import mir.math.sum : sum;
1111     import mir.ndslice.slice : DeepElementType;
1112     import mir.algorithm.iteration : all;
1113 
1114     /++
1115     Params
1116         a = input slice
1117 
1118     Returns:
1119         log-sum-exp value. if `a` is empty, this function returns -inf as mir.math.sum([]) does too.
1120      +/
1121     auto logsumexp(A)(A a) if (isSlice!A)
1122     in
1123     {
1124         assert(a.all!"a >= 0");
1125     }
1126     do
1127     {
1128         if (a.elementCount == 0) return -DeepElementType!A.infinity;
1129         auto a_max = a.maxPos.first;
1130         auto tmp = map!exp(a - a_max);
1131         return tmp.sum!sumTemplateArgs.log + a_max;
1132     }
1133 
1134     /++
1135     Params
1136         a = input slice
1137         b = scaling factor slice for `exp(a)` must have the same shape to b
1138     Returns:
1139         log-sum-exp value. if `a` is empty, this function returns -inf as mir.math.sum([]) does too.
1140      +/
1141     auto logsumexp(A, B)(A a, B b) if (isSlice!A && isSlice!B)
1142     in
1143     {
1144         import numir.testing : assertShapeEqual;
1145         assert(a.all!"a >= 0");
1146         assertShapeEqual(a, b);
1147     }
1148     do
1149     {
1150         if (a.elementCount == 0) return -DeepElementType!A.infinity;
1151         auto a_max = a.maxPos.first;
1152         auto tmp = map!exp(a - a_max) * b;
1153         return tmp.sum!sumTemplateArgs.log + a_max;
1154     }
1155 }
1156 
1157 ///
1158 pure nothrow @nogc
1159 unittest
1160 {
1161     // logsumexp example from doctest https://github.com/scipy/scipy/blob/maintenance/1.0.x/scipy/special/_logsumexp.py
1162     import mir.ndslice : iota, map, sliced, as;
1163     import mir.math.common : log, exp;
1164     import mir.math.sum;
1165     import std.math : approxEqual;
1166     {
1167         auto x = iota(10).as!double;
1168         assert(logsumexp(x).approxEqual(9.4586297444267107));
1169         assert(log(sum(map!exp(x))).approxEqual(logsumexp(x)));
1170     }
1171     {
1172         auto x = iota(0).as!double;
1173         assert(sum(x) == 0);
1174         assert(logsumexp(x) == -double.infinity);
1175         assert(log(sum(map!exp(x))).approxEqual(logsumexp(x)));
1176     }
1177     {
1178         auto a = iota(10).as!double;
1179         auto b = iota([10], 10, -1).as!double;
1180         assert(logsumexp(a, b).approxEqual(9.9170178533034665));
1181         assert(log(sum(map!exp(a) * b)).approxEqual(logsumexp(a, b)));
1182     }
1183     {
1184         auto a = iota(0).as!double;
1185         auto b = iota(0).as!double;
1186         assert(logsumexp(a, b) == -double.infinity);
1187         assert(log(sum(map!exp(a) * b)).approxEqual(logsumexp(a, b)));
1188     }
1189 }