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 }