1 /++ 2 This module contains base statistical algorithms. 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 License: $(HTTP www.apache.org/licenses/LICENSE-2.0, Apache-2.0) 9 10 Authors: Shigeki Karita (original numir code), Ilia Ki, John Michael Hall 11 12 Copyright: 2020 Ilia Ki, Kaleidic Associates Advisory Limited, Symmetry Investments 13 14 Macros: 15 SUBREF = $(REF_ALTTEXT $(TT $2), $2, mir, math, $1)$(NBSP) 16 T2=$(TR $(TDNW $(LREF $1)) $(TD $+)) 17 T4=$(TR $(TDNW $(LREF $1)) $(TD $2) $(TD $3) $(TD $4)) 18 +/ 19 module mir.math.stat; 20 21 import core.lifetime: move; 22 import mir.internal.utility: isFloatingPoint; 23 import mir.math.common: fmamath; 24 import mir.math.sum; 25 import mir.ndslice.slice: Slice, SliceKind, hasAsSlice; 26 import mir.primitives; 27 import std.traits: Unqual, isArray, isMutable, isIterable, isIntegral, CommonType; 28 29 /// 30 public import mir.math.sum: Summation; 31 32 /// 33 package(mir) 34 template statType(T, bool checkComplex = true) 35 { 36 import mir.internal.utility: isFloatingPoint; 37 38 static if (isFloatingPoint!T) { 39 import std.traits: Unqual; 40 alias statType = Unqual!T; 41 } else static if (is(T : double)) { 42 alias statType = double; 43 } else static if (checkComplex) { 44 import mir.internal.utility: isComplex; 45 static if (isComplex!T) { 46 static if (__traits(getAliasThis, T).length == 1) 47 { 48 alias statType = .statType!(typeof(__traits(getMember, T, __traits(getAliasThis, T)[0]))); 49 } 50 else 51 { 52 alias statType = Unqual!T; 53 } 54 } else { 55 static assert(0, "statType: type " ~ T.stringof ~ " must be convertible to a complex floating point type"); 56 } 57 } else { 58 static assert(0, "statType: type " ~ T.stringof ~ " must be convertible to a floating point type"); 59 } 60 } 61 62 version(mir_test) 63 @safe pure nothrow @nogc 64 unittest 65 { 66 static assert(is(statType!int == double)); 67 static assert(is(statType!uint == double)); 68 static assert(is(statType!double == double)); 69 static assert(is(statType!float == float)); 70 static assert(is(statType!real == real)); 71 72 static assert(is(statType!(const(int)) == double)); 73 static assert(is(statType!(immutable(int)) == double)); 74 static assert(is(statType!(const(double)) == double)); 75 static assert(is(statType!(immutable(double)) == double)); 76 } 77 78 version(mir_test) 79 @safe pure nothrow @nogc 80 unittest 81 { 82 import mir.complex: Complex; 83 84 static assert(is(statType!(Complex!float) == Complex!float)); 85 static assert(is(statType!(Complex!double) == Complex!double)); 86 static assert(is(statType!(Complex!real) == Complex!real)); 87 } 88 89 version(mir_test) 90 @safe pure nothrow @nogc 91 unittest 92 { 93 static struct Foo { 94 float x; 95 alias x this; 96 } 97 98 static assert(is(statType!Foo == double)); // note: this is not float 99 } 100 101 version(mir_test) 102 @safe pure nothrow @nogc 103 unittest 104 { 105 import mir.complex; 106 static struct Foo { 107 Complex!float x; 108 alias x this; 109 } 110 111 static assert(is(statType!Foo == Complex!float)); 112 } 113 114 version(mir_test) 115 @safe pure nothrow @nogc 116 unittest 117 { 118 static struct Foo { 119 double x; 120 alias x this; 121 } 122 123 static assert(is(statType!Foo == double)); 124 } 125 126 version(mir_test) 127 @safe pure nothrow @nogc 128 unittest 129 { 130 import mir.complex; 131 static struct Foo { 132 Complex!double x; 133 alias x this; 134 } 135 136 static assert(is(statType!Foo == Complex!double)); 137 } 138 139 version(mir_test) 140 @safe pure nothrow @nogc 141 unittest 142 { 143 static struct Foo { 144 real x; 145 alias x this; 146 } 147 148 static assert(is(statType!Foo == double)); // note: this is not real 149 } 150 151 version(mir_test) 152 @safe pure nothrow @nogc 153 unittest 154 { 155 import mir.complex; 156 static struct Foo { 157 Complex!real x; 158 alias x this; 159 } 160 161 static assert(is(statType!Foo == Complex!real)); 162 } 163 164 version(mir_test) 165 @safe pure nothrow @nogc 166 unittest 167 { 168 static struct Foo { 169 int x; 170 alias x this; 171 } 172 173 static assert(is(statType!Foo == double)); // note: this is not ints 174 } 175 176 /// 177 package(mir) 178 template meanType(T) 179 { 180 import mir.math.sum: sumType; 181 182 alias U = sumType!T; 183 184 static if (__traits(compiles, { 185 auto temp = U.init + U.init; 186 auto a = temp / 2; 187 temp += U.init; 188 })) { 189 alias V = typeof((U.init + U.init) / 2); 190 alias meanType = statType!V; 191 } else { 192 static assert(0, "meanType: Can't calculate mean of elements of type " ~ U.stringof); 193 } 194 } 195 196 version(mir_test) 197 @safe pure nothrow @nogc 198 unittest 199 { 200 static assert(is(meanType!(int[]) == double)); 201 static assert(is(meanType!(double[]) == double)); 202 static assert(is(meanType!(float[]) == float)); 203 } 204 205 version(mir_test) 206 @safe pure nothrow @nogc 207 unittest 208 { 209 import mir.complex; 210 static assert(is(meanType!(Complex!float[]) == Complex!float)); 211 } 212 213 version(mir_test) 214 @safe pure nothrow @nogc 215 unittest 216 { 217 static struct Foo { 218 float x; 219 alias x this; 220 } 221 222 static assert(is(meanType!(Foo[]) == float)); 223 } 224 225 version(mir_test) 226 @safe pure nothrow @nogc 227 unittest 228 { 229 import mir.complex; 230 static struct Foo { 231 Complex!float x; 232 alias x this; 233 } 234 235 static assert(is(meanType!(Foo[]) == Complex!float)); 236 } 237 238 /++ 239 Output range for mean. 240 +/ 241 struct MeanAccumulator(T, Summation summation) 242 { 243 /// 244 size_t count; 245 /// 246 Summator!(T, summation) summator; 247 248 /// 249 F mean(F = T)() const @safe @property pure nothrow @nogc 250 { 251 return cast(F) summator.sum / cast(F) count; 252 } 253 254 /// 255 F sum(F = T)() const @safe @property pure nothrow @nogc 256 { 257 return cast(F) summator.sum; 258 } 259 260 /// 261 void put(Range)(Range r) 262 if (isIterable!Range) 263 { 264 static if (hasShape!Range) 265 { 266 count += r.elementCount; 267 summator.put(r); 268 } 269 else 270 { 271 foreach(x; r) 272 { 273 count++; 274 summator.put(x); 275 } 276 } 277 } 278 279 /// 280 void put()(T x) 281 { 282 count++; 283 summator.put(x); 284 } 285 286 /// 287 void put(F = T)(MeanAccumulator!(F, summation) m) 288 { 289 count += m.count; 290 summator.put(cast(T) m.summator); 291 } 292 } 293 294 /// 295 version(mir_test) 296 @safe pure nothrow 297 unittest 298 { 299 import mir.ndslice.slice: sliced; 300 301 MeanAccumulator!(double, Summation.pairwise) x; 302 x.put([0.0, 1, 2, 3, 4].sliced); 303 assert(x.mean == 2); 304 x.put(5); 305 assert(x.mean == 2.5); 306 } 307 308 version(mir_test) 309 @safe pure nothrow 310 unittest 311 { 312 import mir.ndslice.slice: sliced; 313 314 MeanAccumulator!(float, Summation.pairwise) x; 315 x.put([0, 1, 2, 3, 4].sliced); 316 assert(x.mean == 2); 317 assert(x.sum == 10); 318 x.put(5); 319 assert(x.mean == 2.5); 320 } 321 322 version(mir_test) 323 @safe pure nothrow 324 unittest 325 { 326 double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25]; 327 double[] y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]; 328 329 MeanAccumulator!(float, Summation.pairwise) m0; 330 m0.put(x); 331 MeanAccumulator!(float, Summation.pairwise) m1; 332 m1.put(y); 333 m0.put(m1); 334 assert(m0.mean == 29.25 / 12); 335 } 336 337 /++ 338 Computes the mean of the input. 339 340 By default, if `F` is not floating point type or complex type, then the result 341 will have a `double` type if `F` is implicitly convertible to a floating point 342 type or a type for which `isComplex!F` is true. 343 344 Params: 345 F = controls type of output 346 summation = algorithm for calculating sums (default: Summation.appropriate) 347 Returns: 348 The mean of all the elements in the input, must be floating point or complex type 349 350 See_also: 351 $(SUBREF sum, Summation) 352 +/ 353 template mean(F, Summation summation = Summation.appropriate) 354 { 355 /++ 356 Params: 357 r = range, must be finite iterable 358 +/ 359 @fmamath meanType!F mean(Range)(Range r) 360 if (isIterable!Range) 361 { 362 alias G = typeof(return); 363 MeanAccumulator!(G, ResolveSummationType!(summation, Range, G)) mean; 364 mean.put(r.move); 365 return mean.mean; 366 } 367 368 /++ 369 Params: 370 ar = values 371 +/ 372 @fmamath meanType!F mean(scope const F[] ar...) 373 { 374 alias G = typeof(return); 375 MeanAccumulator!(G, ResolveSummationType!(summation, const(G)[], G)) mean; 376 mean.put(ar); 377 return mean.mean; 378 } 379 } 380 381 /// ditto 382 template mean(Summation summation = Summation.appropriate) 383 { 384 /++ 385 Params: 386 r = range, must be finite iterable 387 +/ 388 @fmamath meanType!Range mean(Range)(Range r) 389 if (isIterable!Range) 390 { 391 alias F = typeof(return); 392 return .mean!(F, summation)(r.move); 393 } 394 395 /++ 396 Params: 397 ar = values 398 +/ 399 @fmamath meanType!T mean(T)(scope const T[] ar...) 400 { 401 alias F = typeof(return); 402 return .mean!(F, summation)(ar); 403 } 404 } 405 406 /// ditto 407 template mean(F, string summation) 408 { 409 mixin("alias mean = .mean!(F, Summation." ~ summation ~ ");"); 410 } 411 412 /// ditto 413 template mean(string summation) 414 { 415 mixin("alias mean = .mean!(Summation." ~ summation ~ ");"); 416 } 417 418 /// 419 version(mir_test) 420 @safe pure nothrow 421 unittest 422 { 423 import mir.ndslice.slice: sliced; 424 import mir.complex; 425 alias C = Complex!double; 426 427 assert(mean([1.0, 2, 3]) == 2); 428 assert(mean([C(1, 3), C(2), C(3)]) == C(2, 1)); 429 430 assert(mean!float([0, 1, 2, 3, 4, 5].sliced(3, 2)) == 2.5); 431 432 static assert(is(typeof(mean!float([1, 2, 3])) == float)); 433 } 434 435 /// Mean of vector 436 version(mir_test) 437 @safe pure nothrow 438 unittest 439 { 440 import mir.ndslice.slice: sliced; 441 442 auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 443 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 444 assert(x.mean == 29.25 / 12); 445 } 446 447 /// Mean of matrix 448 version(mir_test) 449 @safe pure 450 unittest 451 { 452 import mir.ndslice.fuse: fuse; 453 454 auto x = [ 455 [0.0, 1.0, 1.5, 2.0, 3.5, 4.25], 456 [2.0, 7.5, 5.0, 1.0, 1.5, 0.0] 457 ].fuse; 458 459 assert(x.mean == 29.25 / 12); 460 } 461 462 /// Column mean of matrix 463 version(mir_test) 464 @safe pure 465 unittest 466 { 467 import mir.ndslice.fuse: fuse; 468 import mir.ndslice.topology: alongDim, byDim, map; 469 import mir.algorithm.iteration: all; 470 import mir.math.common: approxEqual; 471 472 auto x = [ 473 [0.0, 1.0, 1.5, 2.0, 3.5, 4.25], 474 [2.0, 7.5, 5.0, 1.0, 1.5, 0.0] 475 ].fuse; 476 auto result = [1, 4.25, 3.25, 1.5, 2.5, 2.125]; 477 478 // Use byDim or alongDim with map to compute mean of row/column. 479 assert(x.byDim!1.map!mean.all!approxEqual(result)); 480 assert(x.alongDim!0.map!mean.all!approxEqual(result)); 481 482 // FIXME 483 // Without using map, computes the mean of the whole slice 484 // assert(x.byDim!1.mean == x.sliced.mean); 485 // assert(x.alongDim!0.mean == x.sliced.mean); 486 } 487 488 /// Can also set algorithm or output type 489 version(mir_test) 490 @safe pure nothrow 491 unittest 492 { 493 import mir.ndslice.slice: sliced; 494 import mir.ndslice.topology: repeat; 495 496 //Set sum algorithm or output type 497 498 auto a = [1, 1e100, 1, -1e100].sliced; 499 500 auto x = a * 10_000; 501 502 assert(x.mean!"kbn" == 20_000 / 4); 503 assert(x.mean!"kb2" == 20_000 / 4); 504 assert(x.mean!"precise" == 20_000 / 4); 505 assert(x.mean!(double, "precise") == 20_000.0 / 4); 506 507 auto y = uint.max.repeat(3); 508 assert(y.mean!ulong == 12884901885 / 3); 509 } 510 511 /++ 512 For integral slices, pass output type as template parameter to ensure output 513 type is correct. 514 +/ 515 version(mir_test) 516 @safe pure nothrow 517 unittest 518 { 519 import mir.math.common: approxEqual; 520 import mir.ndslice.slice: sliced; 521 522 auto x = [0, 1, 1, 2, 4, 4, 523 2, 7, 5, 1, 2, 0].sliced; 524 525 auto y = x.mean; 526 assert(y.approxEqual(29.0 / 12, 1.0e-10)); 527 static assert(is(typeof(y) == double)); 528 529 assert(x.mean!float.approxEqual(29f / 12, 1.0e-10)); 530 } 531 532 /++ 533 Mean works for complex numbers and other user-defined types (provided they 534 can be converted to a floating point or complex type) 535 +/ 536 version(mir_test) 537 @safe pure nothrow 538 unittest 539 { 540 import mir.complex.math: approxEqual; 541 import mir.ndslice.slice: sliced; 542 import mir.complex; 543 alias C = Complex!double; 544 545 auto x = [C(1.0, 2), C(2, 3), C(3, 4), C(4, 5)].sliced; 546 assert(x.mean.approxEqual(C(2.5, 3.5))); 547 } 548 549 /// Compute mean tensors along specified dimention of tensors 550 version(mir_test) 551 @safe pure nothrow 552 unittest 553 { 554 import mir.ndslice: alongDim, iota, as, map; 555 /++ 556 [[0,1,2], 557 [3,4,5]] 558 +/ 559 auto x = iota(2, 3).as!double; 560 assert(x.mean == (5.0 / 2.0)); 561 562 auto m0 = [(0.0+3.0)/2.0, (1.0+4.0)/2.0, (2.0+5.0)/2.0]; 563 assert(x.alongDim!0.map!mean == m0); 564 assert(x.alongDim!(-2).map!mean == m0); 565 566 auto m1 = [(0.0+1.0+2.0)/3.0, (3.0+4.0+5.0)/3.0]; 567 assert(x.alongDim!1.map!mean == m1); 568 assert(x.alongDim!(-1).map!mean == m1); 569 570 assert(iota(2, 3, 4, 5).as!double.alongDim!0.map!mean == iota([3, 4, 5], 3 * 4 * 5 / 2)); 571 } 572 573 /// Arbitrary mean 574 version(mir_test) 575 @safe pure nothrow @nogc 576 unittest 577 { 578 assert(mean(1.0, 2, 3) == 2); 579 assert(mean!float(1, 2, 3) == 2); 580 } 581 582 version(mir_test) 583 @safe pure nothrow 584 unittest 585 { 586 assert([1.0, 2, 3, 4].mean == 2.5); 587 } 588 589 version(mir_test) 590 @safe pure nothrow 591 unittest 592 { 593 import mir.algorithm.iteration: all; 594 import mir.math.common: approxEqual; 595 import mir.ndslice.topology: iota, alongDim, map; 596 597 auto x = iota([2, 2], 1); 598 auto y = x.alongDim!1.map!mean; 599 assert(y.all!approxEqual([1.5, 3.5])); 600 static assert(is(meanType!(typeof(y)) == double)); 601 } 602 603 version(mir_test) 604 @safe pure nothrow @nogc 605 unittest 606 { 607 import mir.ndslice.slice: sliced; 608 609 static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 610 2.0, 7.5, 5.0, 1.0, 1.5, 0.0]; 611 612 assert(x.sliced.mean == 29.25 / 12); 613 assert(x.sliced.mean!float == 29.25 / 12); 614 } 615 616 /// 617 package(mir) 618 template hmeanType(T) 619 { 620 import mir.math.sum: sumType; 621 622 alias U = sumType!T; 623 624 static if (__traits(compiles, { 625 U t = U.init + cast(U) 1; //added for when U.init = 0 626 auto temp = cast(U) 1 / t + cast(U) 1 / t; 627 })) { 628 alias V = typeof(cast(U) 1 / ((cast(U) 1 / U.init + cast(U) 1 / U.init) / cast(U) 2)); 629 alias hmeanType = statType!V; 630 } else { 631 static assert(0, "hmeanType: Can't calculate hmean of elements of type " ~ U.stringof); 632 } 633 } 634 635 version(mir_test) 636 @safe pure nothrow @nogc 637 unittest 638 { 639 import mir.complex; 640 static assert(is(hmeanType!(int[]) == double)); 641 static assert(is(hmeanType!(double[]) == double)); 642 static assert(is(hmeanType!(float[]) == float)); 643 static assert(is(hmeanType!(Complex!float[]) == Complex!float)); 644 } 645 646 version(mir_test) 647 @safe pure nothrow @nogc 648 unittest 649 { 650 import mir.complex; 651 static struct Foo { 652 float x; 653 alias x this; 654 } 655 656 static struct Bar { 657 Complex!float x; 658 alias x this; 659 } 660 661 static assert(is(hmeanType!(Foo[]) == float)); 662 static assert(is(hmeanType!(Bar[]) == Complex!float)); 663 } 664 665 /++ 666 Computes the harmonic mean of the input. 667 668 By default, if `F` is not floating point type or complex type, then the result 669 will have a `double` type if `F` is implicitly convertible to a floating point 670 type or a type for which `isComplex!F` is true. 671 672 Params: 673 F = controls type of output 674 summation = algorithm for calculating sums (default: Summation.appropriate) 675 Returns: 676 harmonic mean of all the elements of the input, must be floating point or complex type 677 678 See_also: 679 $(SUBREF sum, Summation) 680 +/ 681 template hmean(F, Summation summation = Summation.appropriate) 682 { 683 /++ 684 Params: 685 r = range 686 +/ 687 @fmamath hmeanType!F hmean(Range)(Range r) 688 if (isIterable!Range) 689 { 690 import mir.ndslice.topology: map; 691 692 alias G = typeof(return); 693 auto numerator = cast(G) 1; 694 695 static if (summation == Summation.fast && __traits(compiles, r.move.map!"numerator / a")) 696 { 697 return numerator / r.move.map!"numerator / a".mean!(G, summation); 698 } 699 else 700 { 701 MeanAccumulator!(G, ResolveSummationType!(summation, Range, G)) imean; 702 foreach (e; r) 703 imean.put(numerator / e); 704 return numerator / imean.mean; 705 } 706 } 707 708 /++ 709 Params: 710 ar = values 711 +/ 712 @fmamath hmeanType!F hmean(scope const F[] ar...) 713 { 714 alias G = typeof(return); 715 716 auto numerator = cast(G) 1; 717 718 static if (summation == Summation.fast && __traits(compiles, ar.map!"numerator / a")) 719 { 720 return numerator / ar.map!"numerator / a".mean!(G, summation); 721 } 722 else 723 { 724 MeanAccumulator!(G, ResolveSummationType!(summation, const(G)[], G)) imean; 725 foreach (e; ar) 726 imean.put(numerator / e); 727 return numerator / imean.mean; 728 } 729 } 730 } 731 732 /// ditto 733 template hmean(Summation summation = Summation.appropriate) 734 { 735 /++ 736 Params: 737 r = range 738 +/ 739 @fmamath hmeanType!Range hmean(Range)(Range r) 740 if (isIterable!Range) 741 { 742 alias F = typeof(return); 743 return .hmean!(F, summation)(r.move); 744 } 745 746 /++ 747 Params: 748 ar = values 749 +/ 750 @fmamath hmeanType!T hmean(T)(scope const T[] ar...) 751 { 752 alias F = typeof(return); 753 return .hmean!(F, summation)(ar); 754 } 755 } 756 757 /// ditto 758 template hmean(F, string summation) 759 { 760 mixin("alias hmean = .hmean!(F, Summation." ~ summation ~ ");"); 761 } 762 763 /// ditto 764 template hmean(string summation) 765 { 766 mixin("alias hmean = .hmean!(Summation." ~ summation ~ ");"); 767 } 768 769 /// Harmonic mean of vector 770 version(mir_test) 771 @safe pure nothrow 772 unittest 773 { 774 import mir.math.common: approxEqual; 775 import mir.ndslice.slice: sliced; 776 777 auto x = [20.0, 100.0, 2000.0, 10.0, 5.0, 2.0].sliced; 778 779 assert(x.hmean.approxEqual(6.97269)); 780 } 781 782 /// Harmonic mean of matrix 783 version(mir_test) 784 pure @safe 785 unittest 786 { 787 import mir.math.common: approxEqual; 788 import mir.ndslice.fuse: fuse; 789 790 auto x = [ 791 [20.0, 100.0, 2000.0], 792 [10.0, 5.0, 2.0] 793 ].fuse; 794 795 assert(x.hmean.approxEqual(6.97269)); 796 } 797 798 /// Column harmonic mean of matrix 799 version(mir_test) 800 pure @safe 801 unittest 802 { 803 import mir.algorithm.iteration: all; 804 import mir.math.common: approxEqual; 805 import mir.ndslice: fuse; 806 import mir.ndslice.topology: alongDim, byDim, map; 807 808 auto x = [ 809 [20.0, 100.0, 2000.0], 810 [ 10.0, 5.0, 2.0] 811 ].fuse; 812 813 auto y = [13.33333, 9.52381, 3.996004]; 814 815 // Use byDim or alongDim with map to compute mean of row/column. 816 assert(x.byDim!1.map!hmean.all!approxEqual(y)); 817 assert(x.alongDim!0.map!hmean.all!approxEqual(y)); 818 } 819 820 /// Can also pass arguments to hmean 821 version(mir_test) 822 pure @safe nothrow 823 unittest 824 { 825 import mir.math.common: approxEqual; 826 import mir.ndslice.topology: repeat; 827 import mir.ndslice.slice: sliced; 828 829 //Set sum algorithm or output type 830 auto x = [1, 1e-100, 1, -1e-100].sliced; 831 832 assert(x.hmean!"kb2".approxEqual(2)); 833 assert(x.hmean!"precise".approxEqual(2)); 834 assert(x.hmean!(double, "precise").approxEqual(2)); 835 836 //Provide the summation type 837 assert(float.max.repeat(3).hmean!double.approxEqual(float.max)); 838 } 839 840 /++ 841 For integral slices, pass output type as template parameter to ensure output 842 type is correct. 843 +/ 844 version(mir_test) 845 @safe pure nothrow 846 unittest 847 { 848 import mir.math.common: approxEqual; 849 import mir.ndslice.slice: sliced; 850 851 auto x = [20, 100, 2000, 10, 5, 2].sliced; 852 853 auto y = x.hmean; 854 855 assert(y.approxEqual(6.97269)); 856 static assert(is(typeof(y) == double)); 857 858 assert(x.hmean!float.approxEqual(6.97269)); 859 } 860 861 /++ 862 hmean works for complex numbers and other user-defined types (provided they 863 can be converted to a floating point or complex type) 864 +/ 865 version(mir_test) 866 @safe pure nothrow 867 unittest 868 { 869 import mir.complex.math: approxEqual; 870 import mir.ndslice.slice: sliced; 871 import mir.complex; 872 alias C = Complex!double; 873 874 auto x = [C(1, 2), C(2, 3), C(3, 4), C(4, 5)].sliced; 875 assert(x.hmean.approxEqual(C(1.97110904, 3.14849332))); 876 } 877 878 /// Arbitrary harmonic mean 879 version(mir_test) 880 @safe pure nothrow @nogc 881 unittest 882 { 883 import mir.math.common: approxEqual; 884 import mir.ndslice.slice: sliced; 885 886 auto x = hmean(20.0, 100, 2000, 10, 5, 2); 887 assert(x.approxEqual(6.97269)); 888 889 auto y = hmean!float(20, 100, 2000, 10, 5, 2); 890 assert(y.approxEqual(6.97269)); 891 } 892 893 version(mir_test) 894 @safe pure nothrow @nogc 895 unittest 896 { 897 import mir.math.common: approxEqual; 898 import mir.ndslice.slice: sliced; 899 900 static immutable x = [20.0, 100.0, 2000.0, 10.0, 5.0, 2.0]; 901 902 assert(x.sliced.hmean.approxEqual(6.97269)); 903 assert(x.sliced.hmean!float.approxEqual(6.97269)); 904 } 905 906 private 907 F nthroot(F)(in F x, in size_t n) 908 if (isFloatingPoint!F) 909 { 910 import mir.math.common: sqrt, pow; 911 912 if (n > 2) { 913 return pow(x, cast(F) 1 / cast(F) n); 914 } else if (n == 2) { 915 return sqrt(x); 916 } else if (n == 1) { 917 return x; 918 } else { 919 return cast(F) 1; 920 } 921 } 922 923 version(mir_test) 924 @safe pure nothrow @nogc 925 unittest 926 { 927 import mir.math.common: approxEqual; 928 929 assert(nthroot(9.0, 0).approxEqual(1)); 930 assert(nthroot(9.0, 1).approxEqual(9)); 931 assert(nthroot(9.0, 2).approxEqual(3)); 932 assert(nthroot(9.5, 2).approxEqual(3.08220700)); 933 assert(nthroot(9.0, 3).approxEqual(2.08008382)); 934 } 935 936 /++ 937 Output range for gmean. 938 +/ 939 struct GMeanAccumulator(T) 940 if (isMutable!T && isFloatingPoint!T) 941 { 942 import mir.math.numeric: ProdAccumulator; 943 944 /// 945 size_t count; 946 /// 947 ProdAccumulator!T prodAccumulator; 948 949 /// 950 F gmean(F = T)() const @property 951 if (isFloatingPoint!F) 952 { 953 import mir.math.common: exp2; 954 955 return nthroot(cast(F) prodAccumulator.mantissa, count) * exp2(cast(F) prodAccumulator.exp / count); 956 } 957 958 /// 959 void put(Range)(Range r) 960 if (isIterable!Range) 961 { 962 static if (hasShape!Range) 963 { 964 count += r.elementCount; 965 prodAccumulator.put(r); 966 } 967 else 968 { 969 foreach(x; r) 970 { 971 count++; 972 prodAccumulator.put(x); 973 } 974 } 975 } 976 977 /// 978 void put()(T x) 979 { 980 count++; 981 prodAccumulator.put(x); 982 } 983 } 984 985 /// 986 version(mir_test) 987 @safe pure nothrow 988 unittest 989 { 990 import mir.math.common: approxEqual; 991 import mir.ndslice.slice: sliced; 992 993 GMeanAccumulator!double x; 994 x.put([1.0, 2, 3, 4].sliced); 995 assert(x.gmean.approxEqual(2.21336384)); 996 x.put(5); 997 assert(x.gmean.approxEqual(2.60517108)); 998 } 999 1000 version(mir_test) 1001 @safe pure nothrow 1002 unittest 1003 { 1004 import mir.math.common: approxEqual; 1005 import mir.ndslice.slice: sliced; 1006 1007 GMeanAccumulator!float x; 1008 x.put([1, 2, 3, 4].sliced); 1009 assert(x.gmean.approxEqual(2.21336384)); 1010 x.put(5); 1011 assert(x.gmean.approxEqual(2.60517108)); 1012 } 1013 1014 /// 1015 package(mir) 1016 template gmeanType(T) 1017 { 1018 import mir.math.numeric: prodType; 1019 1020 alias U = prodType!T; 1021 1022 static if (__traits(compiles, { 1023 auto temp = U.init * U.init; 1024 auto a = nthroot(temp, 2); 1025 temp *= U.init; 1026 })) { 1027 alias V = typeof(nthroot(U.init * U.init, 2)); 1028 alias gmeanType = statType!(V, false); 1029 } else { 1030 static assert(0, "gmeanType: Can't calculate gmean of elements of type " ~ U.stringof); 1031 } 1032 } 1033 1034 version(mir_test) 1035 @safe pure nothrow @nogc 1036 unittest 1037 { 1038 static assert(is(gmeanType!int == double)); 1039 static assert(is(gmeanType!double == double)); 1040 static assert(is(gmeanType!float == float)); 1041 static assert(is(gmeanType!(int[]) == double)); 1042 static assert(is(gmeanType!(double[]) == double)); 1043 static assert(is(gmeanType!(float[]) == float)); 1044 } 1045 1046 /++ 1047 Computes the geometric average of the input. 1048 1049 By default, if `F` is not floating point type, then the result will have a 1050 `double` type if `F` is implicitly convertible to a floating point type. 1051 1052 Params: 1053 r = range, must be finite iterable 1054 Returns: 1055 The geometric average of all the elements in the input, must be floating point type 1056 1057 See_also: 1058 $(SUBREF numeric, prod) 1059 +/ 1060 @fmamath gmeanType!F gmean(F, Range)(Range r) 1061 if (isFloatingPoint!F && isIterable!Range) 1062 { 1063 alias G = typeof(return); 1064 GMeanAccumulator!G gmean; 1065 gmean.put(r.move); 1066 return gmean.gmean; 1067 } 1068 1069 /// ditto 1070 @fmamath gmeanType!Range gmean(Range)(Range r) 1071 if (isIterable!Range) 1072 { 1073 alias G = typeof(return); 1074 return .gmean!(G, Range)(r.move); 1075 } 1076 1077 /++ 1078 Params: 1079 ar = values 1080 +/ 1081 @fmamath gmeanType!F gmean(F)(scope const F[] ar...) 1082 if (isFloatingPoint!F) 1083 { 1084 alias G = typeof(return); 1085 GMeanAccumulator!G gmean; 1086 gmean.put(ar); 1087 return gmean.gmean; 1088 } 1089 1090 /// 1091 version(mir_test) 1092 @safe pure nothrow 1093 unittest 1094 { 1095 import mir.math.common: approxEqual; 1096 import mir.ndslice.slice: sliced; 1097 1098 assert(gmean([1.0, 2, 3]).approxEqual(1.81712059)); 1099 1100 assert(gmean!float([1, 2, 3, 4, 5, 6].sliced(3, 2)).approxEqual(2.99379516)); 1101 1102 static assert(is(typeof(gmean!float([1, 2, 3])) == float)); 1103 } 1104 1105 /// Geometric mean of vector 1106 version(mir_test) 1107 @safe pure nothrow 1108 unittest 1109 { 1110 import mir.math.common: approxEqual; 1111 import mir.ndslice.slice: sliced; 1112 1113 auto x = [3.0, 1.0, 1.5, 2.0, 3.5, 4.25, 1114 2.0, 7.5, 5.0, 1.0, 1.5, 2.0].sliced; 1115 1116 assert(x.gmean.approxEqual(2.36178395)); 1117 } 1118 1119 /// Geometric mean of matrix 1120 version(mir_test) 1121 @safe pure 1122 unittest 1123 { 1124 import mir.math.common: approxEqual; 1125 import mir.ndslice.fuse: fuse; 1126 1127 auto x = [ 1128 [3.0, 1.0, 1.5, 2.0, 3.5, 4.25], 1129 [2.0, 7.5, 5.0, 1.0, 1.5, 2.0] 1130 ].fuse; 1131 1132 assert(x.gmean.approxEqual(2.36178395)); 1133 } 1134 1135 /// Column gmean of matrix 1136 version(mir_test) 1137 @safe pure 1138 unittest 1139 { 1140 import mir.algorithm.iteration: all; 1141 import mir.math.common: approxEqual; 1142 import mir.ndslice.fuse: fuse; 1143 import mir.ndslice.topology: alongDim, byDim, map; 1144 1145 auto x = [ 1146 [3.0, 1.0, 1.5, 2.0, 3.5, 4.25], 1147 [2.0, 7.5, 5.0, 1.0, 1.5, 2.0] 1148 ].fuse; 1149 auto result = [2.44948974, 2.73861278, 2.73861278, 1.41421356, 2.29128784, 2.91547594]; 1150 1151 // Use byDim or alongDim with map to compute mean of row/column. 1152 assert(x.byDim!1.map!gmean.all!approxEqual(result)); 1153 assert(x.alongDim!0.map!gmean.all!approxEqual(result)); 1154 1155 // FIXME 1156 // Without using map, computes the mean of the whole slice 1157 // assert(x.byDim!1.gmean.all!approxEqual(result)); 1158 // assert(x.alongDim!0.gmean.all!approxEqual(result)); 1159 } 1160 1161 /// Can also set output type 1162 version(mir_test) 1163 @safe pure nothrow 1164 unittest 1165 { 1166 import mir.math.common: approxEqual; 1167 import mir.ndslice.slice: sliced; 1168 import mir.ndslice.topology: repeat; 1169 1170 auto x = [5120.0, 7340032, 32, 3758096384].sliced; 1171 1172 assert(x.gmean!float.approxEqual(259281.45295212)); 1173 1174 auto y = uint.max.repeat(2); 1175 assert(y.gmean!float.approxEqual(cast(float) uint.max)); 1176 } 1177 1178 /++ 1179 For integral slices, pass output type as template parameter to ensure output 1180 type is correct. 1181 +/ 1182 version(mir_test) 1183 @safe pure nothrow 1184 unittest 1185 { 1186 import mir.math.common: approxEqual; 1187 import mir.ndslice.slice: sliced; 1188 1189 auto x = [5, 1, 1, 2, 4, 4, 1190 2, 7, 5, 1, 2, 10].sliced; 1191 1192 auto y = x.gmean; 1193 static assert(is(typeof(y) == double)); 1194 1195 assert(x.gmean!float.approxEqual(2.79160522)); 1196 } 1197 1198 /// gean works for user-defined types, provided the nth root can be taken for them 1199 version(mir_test) 1200 @safe pure nothrow 1201 unittest 1202 { 1203 static struct Foo { 1204 float x; 1205 alias x this; 1206 } 1207 1208 import mir.math.common: approxEqual; 1209 import mir.ndslice.slice: sliced; 1210 1211 auto x = [Foo(1.0), Foo(2.0), Foo(3.0)].sliced; 1212 assert(x.gmean.approxEqual(1.81712059)); 1213 } 1214 1215 /// Compute gmean tensors along specified dimention of tensors 1216 version(mir_test) 1217 @safe pure 1218 unittest 1219 { 1220 import mir.algorithm.iteration: all; 1221 import mir.math.common: approxEqual; 1222 import mir.ndslice.fuse: fuse; 1223 import mir.ndslice.topology: alongDim, iota, map; 1224 1225 auto x = [ 1226 [1.0, 2, 3], 1227 [4.0, 5, 6] 1228 ].fuse; 1229 1230 assert(x.gmean.approxEqual(2.99379516)); 1231 1232 auto result0 = [2.0, 3.16227766, 4.24264069]; 1233 assert(x.alongDim!0.map!gmean.all!approxEqual(result0)); 1234 assert(x.alongDim!(-2).map!gmean.all!approxEqual(result0)); 1235 1236 auto result1 = [1.81712059, 4.93242414]; 1237 assert(x.alongDim!1.map!gmean.all!approxEqual(result1)); 1238 assert(x.alongDim!(-1).map!gmean.all!approxEqual(result1)); 1239 1240 auto y = [ 1241 [ 1242 [1.0, 2, 3], 1243 [4.0, 5, 6] 1244 ], [ 1245 [7.0, 8, 9], 1246 [10.0, 9, 10] 1247 ] 1248 ].fuse; 1249 1250 auto result3 = [ 1251 [2.64575131, 4.0, 5.19615242], 1252 [6.32455532, 6.70820393, 7.74596669] 1253 ]; 1254 assert(y.alongDim!0.map!gmean.all!approxEqual(result3)); 1255 } 1256 1257 /// Arbitrary gmean 1258 version(mir_test) 1259 @safe pure nothrow @nogc 1260 unittest 1261 { 1262 import mir.math.common: approxEqual; 1263 1264 assert(gmean(1.0, 2, 3).approxEqual(1.81712059)); 1265 assert(gmean!float(1, 2, 3).approxEqual(1.81712059)); 1266 } 1267 1268 version(mir_test) 1269 @safe pure nothrow 1270 unittest 1271 { 1272 import mir.math.common: approxEqual; 1273 1274 assert([1.0, 2, 3, 4].gmean.approxEqual(2.21336384)); 1275 } 1276 1277 version(mir_test) 1278 @safe pure nothrow 1279 unittest 1280 { 1281 import mir.math.common: approxEqual; 1282 1283 assert(gmean([1, 2, 3]).approxEqual(1.81712059)); 1284 } 1285 1286 version(mir_test) 1287 @safe pure nothrow @nogc 1288 unittest 1289 { 1290 import mir.math.common: approxEqual; 1291 import mir.ndslice.slice: sliced; 1292 1293 static immutable x = [3.0, 1.0, 1.5, 2.0, 3.5, 4.25, 1294 2.0, 7.5, 5.0, 1.0, 1.5, 2.0]; 1295 1296 assert(x.sliced.gmean.approxEqual(2.36178395)); 1297 assert(x.sliced.gmean!float.approxEqual(2.36178395)); 1298 } 1299 1300 /++ 1301 Computes the median of `slice`. 1302 1303 By default, if `F` is not floating point type or complex type, then the result 1304 will have a `double` type if `F` is implicitly convertible to a floating point 1305 type or a type for which `isComplex!F` is true. 1306 1307 Can also pass a boolean variable, `allowModify`, that allows the input slice to 1308 be modified. By default, a reference-counted copy is made. 1309 1310 Params: 1311 F = output type 1312 allowModify = Allows the input slice to be modified, default is false 1313 Returns: 1314 the median of the slice 1315 1316 See_also: 1317 $(SUBREF stat, mean) 1318 +/ 1319 template median(F, bool allowModify = false) 1320 { 1321 /++ 1322 Params: 1323 slice = slice 1324 +/ 1325 @nogc 1326 meanType!F median(Iterator, size_t N, SliceKind kind)(Slice!(Iterator, N, kind) slice) 1327 { 1328 static assert (!allowModify || 1329 isMutable!(slice.DeepElement), 1330 "allowModify must be false or the input must be mutable"); 1331 alias G = typeof(return); 1332 size_t len = slice.elementCount; 1333 assert(len > 0, "median: slice must have length greater than zero"); 1334 1335 import mir.ndslice.topology: as, flattened; 1336 1337 static if (!allowModify) { 1338 import mir.ndslice.allocation: rcslice; 1339 1340 if (len > 2) { 1341 auto view = slice.lightScope; 1342 auto val = view.as!(Unqual!(slice.DeepElement)).rcslice; 1343 auto temp = val.lightScope.flattened; 1344 return .median!(G, true)(temp); 1345 } else { 1346 return mean!G(slice); 1347 } 1348 } else { 1349 import mir.ndslice.sorting: partitionAt; 1350 1351 auto temp = slice.flattened; 1352 1353 if (len > 5) { 1354 size_t half_n = len / 2; 1355 partitionAt(temp, half_n); 1356 if (len % 2 == 1) { 1357 return cast(G) temp[half_n]; 1358 } else { 1359 //move largest value in first half of slice to half_n - 1 1360 partitionAt(temp[0 .. half_n], half_n - 1); 1361 return (temp[half_n - 1] + temp[half_n]) / cast(G) 2; 1362 } 1363 } else { 1364 return smallMedianImpl!(G)(temp); 1365 } 1366 } 1367 } 1368 } 1369 1370 /// ditto 1371 template median(bool allowModify = false) 1372 { 1373 /// ditto 1374 meanType!(Slice!(Iterator, N, kind)) 1375 median(Iterator, size_t N, SliceKind kind)(Slice!(Iterator, N, kind) slice) 1376 { 1377 static assert (!allowModify || 1378 isMutable!(DeepElementType!(Slice!(Iterator, N, kind))), 1379 "allowModify must be false or the input must be mutable"); 1380 alias F = typeof(return); 1381 return .median!(F, allowModify)(slice.move); 1382 } 1383 } 1384 1385 /++ 1386 Params: 1387 ar = array 1388 +/ 1389 meanType!(T[]) median(T)(scope const T[] ar...) 1390 { 1391 import mir.ndslice.slice: sliced; 1392 1393 alias F = typeof(return); 1394 return median!(F, false)(ar.sliced); 1395 } 1396 1397 /++ 1398 Params: 1399 withAsSlice = input that satisfies hasAsSlice 1400 +/ 1401 auto median(T)(T withAsSlice) 1402 if (hasAsSlice!T) 1403 { 1404 return median(withAsSlice.asSlice); 1405 } 1406 1407 /// Median of vector 1408 version(mir_test) 1409 @safe pure nothrow 1410 unittest 1411 { 1412 import mir.ndslice.slice: sliced; 1413 1414 auto x0 = [9.0, 1, 0, 2, 3, 4, 6, 8, 7, 10, 5].sliced; 1415 assert(x0.median == 5); 1416 1417 auto x1 = [9.0, 1, 0, 2, 3, 4, 6, 8, 7, 10].sliced; 1418 assert(x1.median == 5); 1419 } 1420 1421 /// Median of dynamic array 1422 version(mir_test) 1423 @safe pure nothrow 1424 unittest 1425 { 1426 auto x0 = [9.0, 1, 0, 2, 3, 4, 6, 8, 7, 10, 5]; 1427 assert(x0.median == 5); 1428 1429 auto x1 = [9.0, 1, 0, 2, 3, 4, 6, 8, 7, 10]; 1430 assert(x1.median == 5); 1431 } 1432 1433 /// Median of matrix 1434 version(mir_test) 1435 @safe pure 1436 unittest 1437 { 1438 import mir.ndslice.fuse: fuse; 1439 1440 auto x0 = [ 1441 [9.0, 1, 0, 2, 3], 1442 [4.0, 6, 8, 7, 10] 1443 ].fuse; 1444 1445 assert(x0.median == 5); 1446 } 1447 1448 /// Row median of matrix 1449 version(mir_test) 1450 @safe pure 1451 unittest 1452 { 1453 import mir.algorithm.iteration: all; 1454 import mir.math.common: approxEqual; 1455 import mir.ndslice.fuse: fuse; 1456 import mir.ndslice.slice: sliced; 1457 import mir.ndslice.topology: alongDim, byDim, map; 1458 1459 auto x = [ 1460 [0.0, 1.0, 1.5, 2.0, 3.5, 4.25], 1461 [2.0, 7.5, 5.0, 1.0, 1.5, 0.0] 1462 ].fuse; 1463 1464 auto result = [1.75, 1.75].sliced; 1465 1466 // Use byDim or alongDim with map to compute median of row/column. 1467 assert(x.byDim!0.map!median.all!approxEqual(result)); 1468 assert(x.alongDim!1.map!median.all!approxEqual(result)); 1469 } 1470 1471 /// Can allow original slice to be modified or set output type 1472 version(mir_test) 1473 @safe pure nothrow 1474 unittest 1475 { 1476 import mir.ndslice.slice: sliced; 1477 1478 auto x0 = [9.0, 1, 0, 2, 3, 4, 6, 8, 7, 10, 5].sliced; 1479 assert(x0.median!true == 5); 1480 1481 auto x1 = [9, 1, 0, 2, 3, 4, 6, 8, 7, 10].sliced; 1482 assert(x1.median!(float, true) == 5); 1483 } 1484 1485 /// Arbitrary median 1486 version(mir_test) 1487 @safe pure nothrow 1488 unittest 1489 { 1490 assert(median(0, 1, 2, 3, 4) == 2); 1491 } 1492 1493 // @nogc test 1494 version(mir_test) 1495 @safe pure nothrow @nogc 1496 unittest 1497 { 1498 import mir.ndslice.slice: sliced; 1499 1500 static immutable x = [9.0, 1, 0, 2, 3]; 1501 assert(x.sliced.median == 2); 1502 } 1503 1504 // withAsSlice test 1505 version(mir_test) 1506 @safe pure nothrow @nogc 1507 unittest 1508 { 1509 import mir.math.common: approxEqual; 1510 import mir.rc.array: RCArray; 1511 1512 static immutable a = [9.0, 1, 0, 2, 3, 4, 6, 8, 7, 10, 5]; 1513 1514 auto x = RCArray!double(11); 1515 foreach(i, ref e; x) 1516 e = a[i]; 1517 1518 assert(x.median.approxEqual(5)); 1519 } 1520 1521 /++ 1522 For integral slices, can pass output type as template parameter to ensure output 1523 type is correct 1524 +/ 1525 version(mir_test) 1526 @safe pure nothrow 1527 unittest 1528 { 1529 import mir.ndslice.slice: sliced; 1530 1531 auto x = [9, 1, 0, 2, 3, 4, 6, 8, 7, 10].sliced; 1532 assert(x.median!float == 5f); 1533 1534 auto y = x.median; 1535 assert(y == 5.0); 1536 static assert(is(typeof(y) == double)); 1537 } 1538 1539 // additional logic tests 1540 version(mir_test) 1541 @safe pure nothrow 1542 unittest 1543 { 1544 import mir.math.common: approxEqual; 1545 import mir.ndslice.slice: sliced; 1546 1547 auto x = [3, 3, 2, 0, 2, 0].sliced; 1548 assert(x.median!float.approxEqual(2)); 1549 1550 x[] = [2, 2, 4, 0, 4, 3]; 1551 assert(x.median!float.approxEqual(2.5)); 1552 x[] = [1, 4, 5, 4, 4, 3]; 1553 assert(x.median!float.approxEqual(4)); 1554 x[] = [1, 5, 3, 5, 2, 2]; 1555 assert(x.median!float.approxEqual(2.5)); 1556 x[] = [4, 3, 2, 1, 4, 5]; 1557 assert(x.median!float.approxEqual(3.5)); 1558 x[] = [4, 5, 3, 5, 5, 4]; 1559 assert(x.median!float.approxEqual(4.5)); 1560 x[] = [3, 3, 3, 0, 0, 1]; 1561 assert(x.median!float.approxEqual(2)); 1562 x[] = [4, 2, 2, 1, 2, 5]; 1563 assert(x.median!float.approxEqual(2)); 1564 x[] = [2, 3, 1, 4, 5, 5]; 1565 assert(x.median!float.approxEqual(3.5)); 1566 x[] = [1, 1, 4, 5, 5, 5]; 1567 assert(x.median!float.approxEqual(4.5)); 1568 x[] = [2, 4, 0, 5, 1, 0]; 1569 assert(x.median!float.approxEqual(1.5)); 1570 x[] = [3, 5, 2, 5, 4, 2]; 1571 assert(x.median!float.approxEqual(3.5)); 1572 x[] = [3, 5, 4, 1, 4, 3]; 1573 assert(x.median!float.approxEqual(3.5)); 1574 x[] = [4, 2, 0, 3, 1, 3]; 1575 assert(x.median!float.approxEqual(2.5)); 1576 x[] = [100, 4, 5, 0, 5, 1]; 1577 assert(x.median!float.approxEqual(4.5)); 1578 x[] = [100, 5, 4, 0, 5, 1]; 1579 assert(x.median!float.approxEqual(4.5)); 1580 x[] = [100, 5, 4, 0, 1, 5]; 1581 assert(x.median!float.approxEqual(4.5)); 1582 x[] = [4, 5, 100, 1, 5, 0]; 1583 assert(x.median!float.approxEqual(4.5)); 1584 x[] = [0, 1, 2, 2, 3, 4]; 1585 assert(x.median!float.approxEqual(2)); 1586 x[] = [0, 2, 2, 3, 4, 5]; 1587 assert(x.median!float.approxEqual(2.5)); 1588 } 1589 1590 // smallMedianImpl tests 1591 version(mir_test) 1592 @safe pure nothrow 1593 unittest 1594 { 1595 import mir.math.common: approxEqual; 1596 import mir.ndslice.slice: sliced; 1597 1598 auto x0 = [9.0, 1, 0, 2, 3].sliced; 1599 assert(x0.median.approxEqual(2)); 1600 1601 auto x1 = [9.0, 1, 0, 2].sliced; 1602 assert(x1.median.approxEqual(1.5)); 1603 1604 auto x2 = [9.0, 0, 1].sliced; 1605 assert(x2.median.approxEqual(1)); 1606 1607 auto x3 = [1.0, 0].sliced; 1608 assert(x3.median.approxEqual(0.5)); 1609 1610 auto x4 = [1.0].sliced; 1611 assert(x4.median.approxEqual(1)); 1612 } 1613 1614 // Check issue #328 fixed 1615 version(mir_test) 1616 @safe pure nothrow 1617 unittest { 1618 import mir.ndslice.topology: iota; 1619 1620 auto x = iota(18); 1621 auto y = median(x); 1622 assert(y == 8.5); 1623 } 1624 1625 private pure @trusted nothrow @nogc 1626 F smallMedianImpl(F, Iterator)(Slice!Iterator slice) 1627 { 1628 size_t n = slice.elementCount; 1629 1630 assert(n > 0, "smallMedianImpl: slice must have elementCount greater than 0"); 1631 assert(n <= 5, "smallMedianImpl: slice must have elementCount of 5 or less"); 1632 1633 import mir.functional: naryFun; 1634 import mir.ndslice.sorting: medianOf; 1635 import mir.utility: swapStars; 1636 1637 auto sliceI0 = slice._iterator; 1638 1639 if (n == 1) { 1640 return cast(F) *sliceI0; 1641 } 1642 1643 auto sliceI1 = sliceI0; 1644 ++sliceI1; 1645 1646 if (n > 2) { 1647 auto sliceI2 = sliceI1; 1648 ++sliceI2; 1649 alias less = naryFun!("a < b"); 1650 1651 if (n == 3) { 1652 medianOf!less(sliceI0, sliceI1, sliceI2); 1653 return cast(F) *sliceI1; 1654 } else { 1655 auto sliceI3 = sliceI2; 1656 ++sliceI3; 1657 if (n == 4) { 1658 // Put min in slice[0], lower median in slice[1] 1659 medianOf!less(sliceI0, sliceI1, sliceI2, sliceI3); 1660 // Ensure slice[2] < slice[3] 1661 medianOf!less(sliceI2, sliceI3); 1662 return cast(F) (*sliceI1 + *sliceI2) / cast(F) 2; 1663 } else { 1664 auto sliceI4 = sliceI3; 1665 ++sliceI4; 1666 medianOf!less(sliceI0, sliceI1, sliceI2, sliceI3, sliceI4); 1667 return cast(F) *sliceI2; 1668 } 1669 } 1670 } else { 1671 return cast(F) (*sliceI0 + *sliceI1) / cast(F) 2; 1672 } 1673 } 1674 1675 // smallMedianImpl tests 1676 version(mir_test) 1677 @safe pure nothrow 1678 unittest 1679 { 1680 import mir.math.common: approxEqual; 1681 import mir.ndslice.slice: sliced; 1682 1683 auto x0 = [9.0, 1, 0, 2, 3].sliced; 1684 assert(x0.smallMedianImpl!double.approxEqual(2)); 1685 1686 auto x1 = [9.0, 1, 0, 2].sliced; 1687 assert(x1.smallMedianImpl!double.approxEqual(1.5)); 1688 1689 auto x2 = [9.0, 0, 1].sliced; 1690 assert(x2.smallMedianImpl!double.approxEqual(1)); 1691 1692 auto x3 = [1.0, 0].sliced; 1693 assert(x3.smallMedianImpl!double.approxEqual(0.5)); 1694 1695 auto x4 = [1.0].sliced; 1696 assert(x4.smallMedianImpl!double.approxEqual(1)); 1697 1698 auto x5 = [2.0, 1, 0, 9].sliced; 1699 assert(x5.smallMedianImpl!double.approxEqual(1.5)); 1700 1701 auto x6 = [1.0, 2, 0, 9].sliced; 1702 assert(x6.smallMedianImpl!double.approxEqual(1.5)); 1703 1704 auto x7 = [1.0, 0, 9, 2].sliced; 1705 assert(x7.smallMedianImpl!double.approxEqual(1.5)); 1706 } 1707 1708 /++ 1709 Centers `slice`, which must be a finite iterable. 1710 1711 By default, `slice` is centered by the mean. A custom function may also be 1712 provided using `centralTendency`. 1713 1714 Returns: 1715 The elements in the slice with the average subtracted from them. 1716 +/ 1717 template center(alias centralTendency = mean!(Summation.appropriate)) 1718 { 1719 import mir.ndslice.slice: isConvertibleToSlice, isSlice, Slice, SliceKind; 1720 /++ 1721 Params: 1722 slice = slice 1723 +/ 1724 auto center(Iterator, size_t N, SliceKind kind)( 1725 Slice!(Iterator, N, kind) slice) 1726 { 1727 import core.lifetime: move; 1728 import mir.ndslice.internal: LeftOp, ImplicitlyUnqual; 1729 import mir.ndslice.topology: vmap; 1730 1731 auto m = centralTendency(slice.lightScope); 1732 alias T = typeof(m); 1733 return slice.move.vmap(LeftOp!("-", ImplicitlyUnqual!T)(m)); 1734 } 1735 1736 /// ditto 1737 auto center(T)(T x) 1738 if (isConvertibleToSlice!T && !isSlice!T) 1739 { 1740 import mir.ndslice.slice: toSlice; 1741 return center(x.toSlice); 1742 } 1743 } 1744 1745 /// Center vector 1746 version(mir_test) 1747 @safe pure nothrow 1748 unittest 1749 { 1750 import mir.algorithm.iteration: all; 1751 import mir.math.common: approxEqual; 1752 import mir.ndslice.slice: sliced; 1753 1754 auto x = [1.0, 2, 3, 4, 5, 6].sliced; 1755 assert(x.center.all!approxEqual([-2.5, -1.5, -0.5, 0.5, 1.5, 2.5])); 1756 1757 // Can center using different functions 1758 assert(x.center!hmean.all!approxEqual([-1.44898, -0.44898, 0.55102, 1.55102, 2.55102, 3.55102])); 1759 assert(x.center!gmean.all!approxEqual([-1.99379516, -0.99379516, 0.00620483, 1.00620483, 2.00620483, 3.00620483])); 1760 assert(x.center!median.all!approxEqual([-2.5, -1.5, -0.5, 0.5, 1.5, 2.5])); 1761 1762 // center operates lazily, if original slice is changed, then 1763 auto y = x.center; 1764 assert(y.all!approxEqual([-2.5, -1.5, -0.5, 0.5, 1.5, 2.5])); 1765 x[0]++; 1766 assert(y.all!approxEqual([-1.5, -1.5, -0.5, 0.5, 1.5, 2.5])); 1767 } 1768 1769 /// Example of lazy behavior of center 1770 version(mir_test) 1771 @safe pure nothrow 1772 unittest 1773 { 1774 import mir.algorithm.iteration: all; 1775 import mir.math.common: approxEqual; 1776 import mir.ndslice.allocation: slice; 1777 import mir.ndslice.slice: sliced; 1778 1779 auto x = [1.0, 2, 3, 4, 5, 6].sliced; 1780 auto y = x.center; 1781 auto z = x.center.slice; 1782 assert(y.all!approxEqual([-2.5, -1.5, -0.5, 0.5, 1.5, 2.5])); 1783 x[0]++; 1784 // y changes, while z does not 1785 assert(y.all!approxEqual([-1.5, -1.5, -0.5, 0.5, 1.5, 2.5])); 1786 assert(z.all!approxEqual([-2.5, -1.5, -0.5, 0.5, 1.5, 2.5])); 1787 } 1788 1789 /// Center dynamic array 1790 version(mir_test) 1791 @safe pure nothrow 1792 unittest 1793 { 1794 import mir.algorithm.iteration: all; 1795 import mir.math.common: approxEqual; 1796 1797 auto x = [1.0, 2, 3, 4, 5, 6]; 1798 assert(x.center.all!approxEqual([-2.5, -1.5, -0.5, 0.5, 1.5, 2.5])); 1799 } 1800 1801 /// Center matrix 1802 version(mir_test) 1803 @safe pure 1804 unittest 1805 { 1806 import mir.algorithm.iteration: all; 1807 import mir.math.common: approxEqual; 1808 import mir.ndslice: fuse; 1809 1810 auto x = [ 1811 [0.0, 1, 2], 1812 [3.0, 4, 5] 1813 ].fuse; 1814 1815 auto y = [ 1816 [-2.5, -1.5, -0.5], 1817 [ 0.5, 1.5, 2.5] 1818 ].fuse; 1819 1820 assert(x.center.all!approxEqual(y)); 1821 } 1822 1823 /// Column center matrix 1824 version(mir_test) 1825 @safe pure 1826 unittest 1827 { 1828 import mir.algorithm.iteration: all, equal; 1829 import mir.math.common: approxEqual; 1830 import mir.ndslice: fuse; 1831 import mir.ndslice.topology: alongDim, byDim, map; 1832 1833 auto x = [ 1834 [20.0, 100.0, 2000.0], 1835 [10.0, 5.0, 2.0] 1836 ].fuse; 1837 1838 auto result = [ 1839 [ 5.0, 47.5, 999], 1840 [-5.0, -47.5, -999] 1841 ].fuse; 1842 1843 // Use byDim with map to compute average of row/column. 1844 auto xCenterByDim = x.byDim!1.map!center; 1845 auto resultByDim = result.byDim!1; 1846 assert(xCenterByDim.equal!(equal!approxEqual)(resultByDim)); 1847 1848 auto xCenterAlongDim = x.alongDim!0.map!center; 1849 auto resultAlongDim = result.alongDim!0; 1850 assert(xCenterByDim.equal!(equal!approxEqual)(resultAlongDim)); 1851 } 1852 1853 /// Can also pass arguments to average function used by center 1854 version(mir_test) 1855 pure @safe nothrow 1856 unittest 1857 { 1858 import mir.ndslice.slice: sliced; 1859 1860 //Set sum algorithm or output type 1861 auto a = [1, 1e100, 1, -1e100]; 1862 1863 auto x = a.sliced * 10_000; 1864 1865 //Due to Floating Point precision, subtracting the mean from the second 1866 //and fourth numbers in `x` does not change the value of the result 1867 auto result = [5000, 1e104, 5000, -1e104].sliced; 1868 1869 assert(x.center!(mean!"kbn") == result); 1870 assert(x.center!(mean!"kb2") == result); 1871 assert(x.center!(mean!"precise") == result); 1872 } 1873 1874 /++ 1875 Passing a centered input to `variance` or `standardDeviation` with the 1876 `assumeZeroMean` algorithm is equivalent to calculating `variance` or 1877 `standardDeviation` on the original input. 1878 +/ 1879 version(mir_test) 1880 @safe pure nothrow 1881 unittest 1882 { 1883 import mir.algorithm.iteration: all; 1884 import mir.math.common: approxEqual; 1885 import mir.ndslice.slice: sliced; 1886 1887 auto x = [1.0, 2, 3, 4, 5, 6].sliced; 1888 assert(x.center.variance!"assumeZeroMean".approxEqual(x.variance)); 1889 assert(x.center.standardDeviation!"assumeZeroMean".approxEqual(x.standardDeviation)); 1890 } 1891 1892 // dynamic array test 1893 version(mir_test) 1894 @safe pure nothrow 1895 unittest 1896 { 1897 import mir.algorithm.iteration: all; 1898 import mir.math.common: approxEqual; 1899 1900 double[] x = [1.0, 2, 3, 4, 5, 6]; 1901 1902 assert(x.center.all!approxEqual([-2.5, -1.5, -0.5, 0.5, 1.5, 2.5])); 1903 } 1904 1905 // withAsSlice test 1906 version(mir_test) 1907 @safe pure nothrow @nogc 1908 unittest 1909 { 1910 import mir.algorithm.iteration: all; 1911 import mir.math.common: approxEqual; 1912 import mir.rc.array: RCArray; 1913 1914 static immutable a = [1.0, 2, 3, 4, 5, 6]; 1915 static immutable result = [-2.5, -1.5, -0.5, 0.5, 1.5, 2.5]; 1916 1917 auto x = RCArray!double(6); 1918 foreach(i, ref e; x) 1919 e = a[i]; 1920 1921 assert(x.center.all!approxEqual(result)); 1922 } 1923 1924 /++ 1925 Output range that applies function `fun` to each input before summing 1926 +/ 1927 struct MapSummator(alias fun, T, Summation summation) 1928 if(isMutable!T) 1929 { 1930 /// 1931 Summator!(T, summation) summator; 1932 1933 /// 1934 F sum(F = T)() const @property 1935 { 1936 return cast(F) summator.sum; 1937 } 1938 1939 /// 1940 void put(Range)(Range r) 1941 if (isIterable!Range) 1942 { 1943 import mir.ndslice.topology: map; 1944 summator.put(r.map!fun); 1945 } 1946 1947 /// 1948 void put()(T x) 1949 { 1950 summator.put(fun(x)); 1951 } 1952 } 1953 1954 /// 1955 version(mir_test) 1956 @safe pure nothrow 1957 unittest 1958 { 1959 import mir.math.common: powi; 1960 import mir.ndslice.slice: sliced; 1961 1962 alias f = (double x) => (powi(x, 2)); 1963 MapSummator!(f, double, Summation.pairwise) x; 1964 x.put([0.0, 1, 2, 3, 4].sliced); 1965 assert(x.sum == 30.0); 1966 x.put(5); 1967 assert(x.sum == 55.0); 1968 } 1969 1970 version(mir_test) 1971 @safe pure nothrow 1972 unittest 1973 { 1974 import mir.ndslice.slice: sliced; 1975 1976 alias f = (double x) => (x + 1); 1977 MapSummator!(f, double, Summation.pairwise) x; 1978 x.put([0.0, 1, 2, 3, 4].sliced); 1979 assert(x.sum == 15.0); 1980 x.put(5); 1981 assert(x.sum == 21.0); 1982 } 1983 1984 version(mir_test) 1985 @safe pure nothrow @nogc 1986 unittest 1987 { 1988 import mir.ndslice.slice: sliced; 1989 1990 alias f = (double x) => (x + 1); 1991 MapSummator!(f, double, Summation.pairwise) x; 1992 static immutable a = [0.0, 1, 2, 3, 4]; 1993 x.put(a.sliced); 1994 assert(x.sum == 15.0); 1995 x.put(5); 1996 assert(x.sum == 21.0); 1997 } 1998 1999 version(mir_test) 2000 @safe pure 2001 unittest 2002 { 2003 import mir.ndslice.fuse: fuse; 2004 import mir.ndslice.slice: sliced; 2005 2006 alias f = (double x) => (x + 1); 2007 MapSummator!(f, double, Summation.pairwise) x; 2008 auto a = [ 2009 [0.0, 1, 2], 2010 [3.0, 4, 5] 2011 ].fuse; 2012 auto b = [6.0, 7, 8].sliced; 2013 x.put(a); 2014 assert(x.sum == 21.0); 2015 x.put(b); 2016 assert(x.sum == 45.0); 2017 } 2018 2019 /++ 2020 Variance algorithms. 2021 2022 See Also: 2023 $(WEB en.wikipedia.org/wiki/Algorithms_for_calculating_variance, Algorithms for calculating variance). 2024 +/ 2025 enum VarianceAlgo 2026 { 2027 /++ 2028 Performs Welford's online algorithm for updating variance. Can also `put` 2029 another VarianceAccumulator of the same type, which uses the parallel 2030 algorithm from Chan et al., described above. 2031 +/ 2032 online, 2033 2034 /++ 2035 Calculates variance using E(x^^2) - E(x)^2 (alowing for adjustments for 2036 population/sample variance). This algorithm can be numerically unstable. 2037 +/ 2038 naive, 2039 2040 /++ 2041 Calculates variance using a two-pass algorithm whereby the input is first 2042 centered and then the sum of squares is calculated from that. 2043 +/ 2044 twoPass, 2045 2046 /++ 2047 Calculates variance assuming the mean of the dataseries is zero. 2048 +/ 2049 assumeZeroMean 2050 } 2051 2052 /// 2053 struct VarianceAccumulator(T, VarianceAlgo varianceAlgo, Summation summation) 2054 if (isMutable!T && varianceAlgo == VarianceAlgo.naive) 2055 { 2056 import mir.functional: naryFun; 2057 2058 /// 2059 this(Range)(Range r) 2060 if (isIterable!Range) 2061 { 2062 import core.lifetime: move; 2063 this.put(r.move); 2064 } 2065 2066 /// 2067 this()(T x) 2068 { 2069 this.put(x); 2070 } 2071 2072 /// 2073 MeanAccumulator!(T, summation) meanAccumulator; 2074 2075 /// 2076 Summator!(T, summation) sumOfSquares; 2077 2078 /// 2079 void put(Range)(Range r) 2080 if (isIterable!Range) 2081 { 2082 foreach(x; r) 2083 { 2084 this.put(x); 2085 } 2086 } 2087 2088 /// 2089 void put()(T x) 2090 { 2091 meanAccumulator.put(x); 2092 sumOfSquares.put(x * x); 2093 } 2094 2095 const: 2096 2097 /// 2098 size_t count() @property 2099 { 2100 return meanAccumulator.count; 2101 } 2102 2103 /// 2104 F mean(F = T)() const @property 2105 { 2106 return meanAccumulator.mean; 2107 } 2108 2109 /// 2110 F variance(F = T)(bool isPopulation) @property 2111 { 2112 return cast(F) sumOfSquares.sum / (count + isPopulation - 1) - 2113 (cast(F) meanAccumulator.mean) ^^ 2 * (cast(F) count / (count + isPopulation - 1)); 2114 } 2115 } 2116 2117 /// 2118 version(mir_test) 2119 @safe pure nothrow 2120 unittest 2121 { 2122 import mir.math.common: approxEqual; 2123 import mir.ndslice.slice: sliced; 2124 2125 auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 2126 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 2127 2128 enum PopulationTrueCT = true; 2129 enum PopulationFalseCT = false; 2130 bool PopulationTrueRT = true; 2131 bool PopulationFalseRT = false; 2132 2133 VarianceAccumulator!(double, VarianceAlgo.naive, Summation.naive) v; 2134 v.put(x); 2135 assert(v.variance(PopulationTrueRT).approxEqual(54.76562 / 12)); 2136 assert(v.variance(PopulationTrueCT).approxEqual(54.76562 / 12)); 2137 assert(v.variance(PopulationFalseRT).approxEqual(54.76562 / 11)); 2138 assert(v.variance(PopulationFalseCT).approxEqual(54.76562 / 11)); 2139 2140 v.put(4.0); 2141 assert(v.variance(PopulationTrueRT).approxEqual(57.01923 / 13)); 2142 assert(v.variance(PopulationTrueCT).approxEqual(57.01923 / 13)); 2143 assert(v.variance(PopulationFalseRT).approxEqual(57.01923 / 12)); 2144 assert(v.variance(PopulationFalseCT).approxEqual(57.01923 / 12)); 2145 } 2146 2147 /// 2148 struct VarianceAccumulator(T, VarianceAlgo varianceAlgo, Summation summation) 2149 if (isMutable!T && 2150 varianceAlgo == VarianceAlgo.online) 2151 { 2152 /// 2153 this(Range)(Range r) 2154 if (isIterable!Range) 2155 { 2156 import core.lifetime: move; 2157 this.put(r.move); 2158 } 2159 2160 /// 2161 this()(T x) 2162 { 2163 this.put(x); 2164 } 2165 2166 /// 2167 MeanAccumulator!(T, summation) meanAccumulator; 2168 2169 /// 2170 Summator!(T, summation) centeredSumOfSquares; 2171 2172 /// 2173 void put(Range)(Range r) 2174 if (isIterable!Range) 2175 { 2176 foreach(x; r) 2177 { 2178 this.put(x); 2179 } 2180 } 2181 2182 /// 2183 void put()(T x) 2184 { 2185 T delta = x; 2186 if (count > 0) { 2187 delta -= meanAccumulator.mean; 2188 } 2189 meanAccumulator.put(x); 2190 centeredSumOfSquares.put(delta * (x - meanAccumulator.mean)); 2191 } 2192 2193 /// 2194 void put()(VarianceAccumulator!(T, varianceAlgo, summation) v) 2195 { 2196 size_t oldCount = count; 2197 T delta = v.mean; 2198 if (oldCount > 0) { 2199 delta -= meanAccumulator.mean; 2200 } 2201 meanAccumulator.put!T(v.meanAccumulator); 2202 centeredSumOfSquares.put(v.centeredSumOfSquares.sum + delta * delta * v.count * oldCount / count); 2203 } 2204 2205 const: 2206 2207 /// 2208 size_t count() @property 2209 { 2210 return meanAccumulator.count; 2211 } 2212 2213 /// 2214 F mean(F = T)() const @property 2215 { 2216 return meanAccumulator.mean; 2217 } 2218 2219 /// 2220 F variance(F = T)(bool isPopulation) @property 2221 { 2222 return cast(F) centeredSumOfSquares.sum / (count + isPopulation - 1); 2223 } 2224 } 2225 2226 /// 2227 version(mir_test) 2228 @safe pure nothrow 2229 unittest 2230 { 2231 import mir.math.common: approxEqual; 2232 import mir.ndslice.slice: sliced; 2233 2234 auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 2235 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 2236 2237 enum PopulationTrueCT = true; 2238 enum PopulationFalseCT = false; 2239 bool PopulationTrueRT = true; 2240 bool PopulationFalseRT = false; 2241 2242 VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive) v; 2243 v.put(x); 2244 2245 assert(v.variance(PopulationTrueRT).approxEqual(54.76562 / 12)); 2246 assert(v.variance(PopulationTrueCT).approxEqual(54.76562 / 12)); 2247 assert(v.variance(PopulationFalseRT).approxEqual(54.76562 / 11)); 2248 assert(v.variance(PopulationFalseCT).approxEqual(54.76562 / 11)); 2249 2250 v.put(4.0); 2251 assert(v.variance(PopulationTrueRT).approxEqual(57.01923 / 13)); 2252 assert(v.variance(PopulationTrueCT).approxEqual(57.01923 / 13)); 2253 assert(v.variance(PopulationFalseRT).approxEqual(57.01923 / 12)); 2254 assert(v.variance(PopulationFalseCT).approxEqual(57.01923 / 12)); 2255 } 2256 2257 /// 2258 version(mir_test) 2259 @safe pure nothrow 2260 unittest 2261 { 2262 import mir.math.common: approxEqual; 2263 import mir.ndslice.slice: sliced; 2264 2265 auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced; 2266 auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 2267 2268 enum PopulationTrueCT = true; 2269 enum PopulationFalseCT = false; 2270 bool PopulationTrueRT = true; 2271 bool PopulationFalseRT = false; 2272 2273 VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive) v; 2274 v.put(x); 2275 assert(v.variance(PopulationTrueRT).approxEqual(12.55208 / 6)); 2276 assert(v.variance(PopulationTrueCT).approxEqual(12.55208 / 6)); 2277 assert(v.variance(PopulationFalseRT).approxEqual(12.55208 / 5)); 2278 assert(v.variance(PopulationFalseCT).approxEqual(12.55208 / 5)); 2279 2280 v.put(y); 2281 assert(v.variance(PopulationTrueRT).approxEqual(54.76562 / 12)); 2282 assert(v.variance(PopulationTrueCT).approxEqual(54.76562 / 12)); 2283 assert(v.variance(PopulationFalseRT).approxEqual(54.76562 / 11)); 2284 assert(v.variance(PopulationFalseCT).approxEqual(54.76562 / 11)); 2285 } 2286 2287 version(mir_test) 2288 @safe pure nothrow 2289 unittest 2290 { 2291 import mir.math.common: approxEqual; 2292 import mir.ndslice.slice: sliced; 2293 2294 auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced; 2295 auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 2296 2297 enum PopulationTrueCT = true; 2298 enum PopulationFalseCT = false; 2299 bool PopulationTrueRT = true; 2300 bool PopulationFalseRT = false; 2301 2302 VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive) v; 2303 v.put(x); 2304 assert(v.variance(PopulationTrueRT).approxEqual(12.55208 / 6)); 2305 assert(v.variance(PopulationTrueCT).approxEqual(12.55208 / 6)); 2306 assert(v.variance(PopulationFalseRT).approxEqual(12.55208 / 5)); 2307 assert(v.variance(PopulationFalseCT).approxEqual(12.55208 / 5)); 2308 2309 VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive) w; 2310 w.put(y); 2311 v.put(w); 2312 assert(v.variance(PopulationTrueRT).approxEqual(54.76562 / 12)); 2313 assert(v.variance(PopulationTrueCT).approxEqual(54.76562 / 12)); 2314 assert(v.variance(PopulationFalseRT).approxEqual(54.76562 / 11)); 2315 assert(v.variance(PopulationFalseCT).approxEqual(54.76562 / 11)); 2316 } 2317 2318 version(mir_test) 2319 @safe pure nothrow 2320 unittest 2321 { 2322 import mir.complex.math: approxEqual; 2323 import mir.ndslice.slice: sliced; 2324 import mir.complex: Complex; 2325 2326 auto x = [Complex!double(1.0, 3), Complex!double(2), Complex!double(3)].sliced; 2327 2328 VarianceAccumulator!(Complex!double, VarianceAlgo.online, Summation.naive) v; 2329 v.put(x); 2330 assert(v.variance(true).approxEqual(Complex!double(-4.0, -6) / 3)); 2331 assert(v.variance(false).approxEqual(Complex!double(-4.0, -6) / 2)); 2332 } 2333 2334 version(mir_test) 2335 @safe pure nothrow 2336 unittest 2337 { 2338 import mir.math.common: approxEqual; 2339 import mir.ndslice.slice: sliced; 2340 2341 auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 2342 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 2343 2344 VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive) v; 2345 v.put(x); 2346 assert(v.variance(false).approxEqual(54.76562 / 11)); 2347 2348 v.put(4.0); 2349 assert(v.variance(false).approxEqual(57.01923 / 12)); 2350 } 2351 2352 version(mir_test) 2353 @safe pure nothrow 2354 unittest 2355 { 2356 import mir.math.common: approxEqual; 2357 import mir.ndslice.slice: sliced; 2358 2359 auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25].sliced; 2360 auto y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 2361 2362 VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive) v; 2363 v.put(x); 2364 assert(v.variance(false).approxEqual(12.55208 / 5)); 2365 2366 VarianceAccumulator!(double, VarianceAlgo.online, Summation.naive) w; 2367 w.put(y); 2368 v.put(w); 2369 assert(v.variance(false).approxEqual(54.76562 / 11)); 2370 } 2371 2372 /// 2373 struct VarianceAccumulator(T, VarianceAlgo varianceAlgo, Summation summation) 2374 if (isMutable!T && varianceAlgo == VarianceAlgo.twoPass) 2375 { 2376 import mir.functional: naryFun; 2377 import mir.ndslice.slice: Slice, SliceKind, hasAsSlice; 2378 2379 /// 2380 MeanAccumulator!(T, summation) meanAccumulator; 2381 2382 /// 2383 Summator!(T, summation) centeredSumOfSquares; 2384 2385 /// 2386 this(Iterator, size_t N, SliceKind kind)( 2387 Slice!(Iterator, N, kind) slice) 2388 { 2389 import core.lifetime: move; 2390 import mir.ndslice.internal: LeftOp; 2391 import mir.ndslice.topology: vmap, map; 2392 2393 meanAccumulator.put(slice.lightScope); 2394 centeredSumOfSquares.put(slice.move.vmap(LeftOp!("-", T)(meanAccumulator.mean)).map!(naryFun!"a * a")); 2395 } 2396 2397 /// 2398 this(U)(U[] array) 2399 { 2400 import mir.ndslice.slice: sliced; 2401 this(array.sliced); 2402 } 2403 2404 /// 2405 this(T)(T withAsSlice) 2406 if (hasAsSlice!T) 2407 { 2408 this(withAsSlice.asSlice); 2409 } 2410 2411 /// 2412 this()(T x) 2413 { 2414 meanAccumulator.put(x); 2415 centeredSumOfSquares.put(cast(T) 0); 2416 } 2417 2418 const: 2419 2420 /// 2421 size_t count() @property 2422 { 2423 return meanAccumulator.count; 2424 } 2425 2426 /// 2427 F mean(F = T)() const @property 2428 { 2429 return meanAccumulator.mean; 2430 } 2431 2432 /// 2433 F variance(F = T)(bool isPopulation) @property 2434 { 2435 return cast(F) centeredSumOfSquares.sum / (count + isPopulation - 1); 2436 } 2437 } 2438 2439 /// 2440 version(mir_test) 2441 @safe pure nothrow 2442 unittest 2443 { 2444 import mir.math.common: approxEqual; 2445 import mir.ndslice.slice: sliced; 2446 2447 auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 2448 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 2449 2450 enum PopulationTrueCT = true; 2451 enum PopulationFalseCT = false; 2452 bool PopulationTrueRT = true; 2453 bool PopulationFalseRT = false; 2454 2455 auto v = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(x); 2456 assert(v.variance(PopulationTrueRT).approxEqual(54.76562 / 12)); 2457 assert(v.variance(PopulationTrueCT).approxEqual(54.76562 / 12)); 2458 assert(v.variance(PopulationFalseRT).approxEqual(54.76562 / 11)); 2459 assert(v.variance(PopulationFalseCT).approxEqual(54.76562 / 11)); 2460 } 2461 2462 // dynamic array test 2463 version(mir_test) 2464 @safe pure nothrow 2465 unittest 2466 { 2467 import mir.math.common: approxEqual; 2468 import mir.rc.array: RCArray; 2469 2470 double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 2471 2.0, 7.5, 5.0, 1.0, 1.5, 0.0]; 2472 2473 auto v = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(x); 2474 assert(v.centeredSumOfSquares.sum.approxEqual(54.76562)); 2475 } 2476 2477 // withAsSlice test 2478 version(mir_test) 2479 @safe pure nothrow @nogc 2480 unittest 2481 { 2482 import mir.math.common: approxEqual; 2483 import mir.rc.array: RCArray; 2484 2485 static immutable a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 2486 2.0, 7.5, 5.0, 1.0, 1.5, 0.0]; 2487 2488 auto x = RCArray!double(12); 2489 foreach(i, ref e; x) 2490 e = a[i]; 2491 2492 auto v = VarianceAccumulator!(double, VarianceAlgo.twoPass, Summation.naive)(x); 2493 assert(v.centeredSumOfSquares.sum.approxEqual(54.76562)); 2494 } 2495 2496 /// 2497 struct VarianceAccumulator(T, VarianceAlgo varianceAlgo, Summation summation) 2498 if (isMutable!T && varianceAlgo == VarianceAlgo.assumeZeroMean) 2499 { 2500 import mir.ndslice.slice: Slice, SliceKind, hasAsSlice; 2501 2502 private size_t _count; 2503 2504 /// 2505 Summator!(T, summation) centeredSumOfSquares; 2506 2507 /// 2508 this(Range)(Range r) 2509 if (isIterable!Range) 2510 { 2511 this.put(r); 2512 } 2513 2514 /// 2515 this()(T x) 2516 { 2517 this.put(x); 2518 } 2519 2520 /// 2521 void put(Range)(Range r) 2522 if (isIterable!Range) 2523 { 2524 foreach(x; r) 2525 { 2526 this.put(x); 2527 } 2528 } 2529 2530 /// 2531 void put()(T x) 2532 { 2533 _count++; 2534 centeredSumOfSquares.put(x * x); 2535 } 2536 2537 /// 2538 void put()(VarianceAccumulator!(T, varianceAlgo, summation) v) 2539 { 2540 _count += v.count; 2541 centeredSumOfSquares.put(v.centeredSumOfSquares.sum); 2542 } 2543 2544 const: 2545 2546 /// 2547 size_t count() @property 2548 { 2549 return _count; 2550 } 2551 2552 /// 2553 F mean(F = T)() const @property 2554 { 2555 return cast(F) 0; 2556 } 2557 2558 /// 2559 F variance(F = T)(bool isPopulation) @property 2560 { 2561 return cast(F) centeredSumOfSquares.sum / (count + isPopulation - 1); 2562 } 2563 } 2564 2565 /// 2566 version(mir_test) 2567 @safe pure nothrow 2568 unittest 2569 { 2570 import mir.math.common: approxEqual; 2571 import mir.ndslice.slice: sliced; 2572 2573 auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 2574 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 2575 auto x = a.center; 2576 2577 enum PopulationTrueCT = true; 2578 enum PopulationFalseCT = false; 2579 bool PopulationTrueRT = true; 2580 bool PopulationFalseRT = false; 2581 2582 VarianceAccumulator!(double, VarianceAlgo.assumeZeroMean, Summation.naive) v; 2583 v.put(x); 2584 2585 assert(v.variance(PopulationTrueRT).approxEqual(54.76562 / 12)); 2586 assert(v.variance(PopulationTrueCT).approxEqual(54.76562 / 12)); 2587 assert(v.variance(PopulationFalseRT).approxEqual(54.76562 / 11)); 2588 assert(v.variance(PopulationFalseCT).approxEqual(54.76562 / 11)); 2589 2590 v.put(4.0); 2591 assert(v.variance(PopulationTrueRT).approxEqual(70.76562 / 13)); 2592 assert(v.variance(PopulationTrueCT).approxEqual(70.76562 / 13)); 2593 assert(v.variance(PopulationFalseRT).approxEqual(70.76562 / 12)); 2594 assert(v.variance(PopulationFalseCT).approxEqual(70.76562 / 12)); 2595 } 2596 2597 /// 2598 version(mir_test) 2599 @safe pure nothrow 2600 unittest 2601 { 2602 import mir.math.common: approxEqual; 2603 import mir.ndslice.slice: sliced; 2604 2605 auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 2606 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 2607 auto b = a.center; 2608 auto x = b[0 .. 6]; 2609 auto y = b[6 .. $]; 2610 2611 enum PopulationTrueCT = true; 2612 enum PopulationFalseCT = false; 2613 bool PopulationTrueRT = true; 2614 bool PopulationFalseRT = false; 2615 2616 VarianceAccumulator!(double, VarianceAlgo.assumeZeroMean, Summation.naive) v; 2617 v.put(x); 2618 assert(v.variance(PopulationTrueRT).approxEqual(13.492188 / 6)); 2619 assert(v.variance(PopulationTrueCT).approxEqual(13.492188 / 6)); 2620 assert(v.variance(PopulationFalseRT).approxEqual(13.492188 / 5)); 2621 assert(v.variance(PopulationFalseCT).approxEqual(13.492188 / 5)); 2622 2623 v.put(y); 2624 assert(v.variance(PopulationTrueRT).approxEqual(54.76562 / 12)); 2625 assert(v.variance(PopulationTrueCT).approxEqual(54.76562 / 12)); 2626 assert(v.variance(PopulationFalseRT).approxEqual(54.76562 / 11)); 2627 assert(v.variance(PopulationFalseCT).approxEqual(54.76562 / 11)); 2628 } 2629 2630 version(mir_test) 2631 @safe pure nothrow 2632 unittest 2633 { 2634 import mir.math.common: approxEqual; 2635 import mir.ndslice.slice: sliced; 2636 2637 auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 2638 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 2639 auto b = a.center; 2640 auto x = b[0 .. 6]; 2641 auto y = b[6 .. $]; 2642 2643 enum PopulationTrueCT = true; 2644 enum PopulationFalseCT = false; 2645 bool PopulationTrueRT = true; 2646 bool PopulationFalseRT = false; 2647 2648 VarianceAccumulator!(double, VarianceAlgo.assumeZeroMean, Summation.naive) v; 2649 v.put(x); 2650 assert(v.variance(PopulationTrueRT).approxEqual(13.492188 / 6)); 2651 assert(v.variance(PopulationTrueCT).approxEqual(13.492188 / 6)); 2652 assert(v.variance(PopulationFalseRT).approxEqual(13.492188 / 5)); 2653 assert(v.variance(PopulationFalseCT).approxEqual(13.492188 / 5)); 2654 2655 VarianceAccumulator!(double, VarianceAlgo.assumeZeroMean, Summation.naive) w; 2656 w.put(y); 2657 v.put(w); 2658 assert(v.variance(PopulationTrueRT).approxEqual(54.76562 / 12)); 2659 assert(v.variance(PopulationTrueCT).approxEqual(54.76562 / 12)); 2660 assert(v.variance(PopulationFalseRT).approxEqual(54.76562 / 11)); 2661 assert(v.variance(PopulationFalseCT).approxEqual(54.76562 / 11)); 2662 } 2663 2664 version(mir_test) 2665 @safe pure nothrow 2666 unittest 2667 { 2668 import mir.complex.math: approxEqual; 2669 import mir.ndslice.slice: sliced; 2670 import mir.complex: Complex; 2671 2672 auto a = [Complex!double(1.0, 3), Complex!double(2), Complex!double(3)].sliced; 2673 auto x = a.center; 2674 2675 VarianceAccumulator!(Complex!double, VarianceAlgo.assumeZeroMean, Summation.naive) v; 2676 v.put(x); 2677 assert(v.variance(true).approxEqual(Complex!double(-4.0, -6) / 3)); 2678 assert(v.variance(false).approxEqual(Complex!double(-4.0, -6) / 2)); 2679 } 2680 2681 version(mir_test) 2682 @safe pure nothrow 2683 unittest 2684 { 2685 import mir.math.common: approxEqual; 2686 import mir.ndslice.slice: sliced; 2687 2688 auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 2689 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 2690 auto x = a.center; 2691 2692 VarianceAccumulator!(double, VarianceAlgo.assumeZeroMean, Summation.naive) v; 2693 v.put(x); 2694 assert(v.variance(false).approxEqual(54.76562 / 11)); 2695 2696 v.put(4.0); 2697 assert(v.variance(false).approxEqual(70.76562 / 12)); 2698 } 2699 2700 version(mir_test) 2701 @safe pure nothrow 2702 unittest 2703 { 2704 import mir.math.common: approxEqual; 2705 import mir.ndslice.slice: sliced; 2706 2707 auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 2708 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 2709 auto b = a.center; 2710 auto x = b[0 .. 6]; 2711 auto y = b[6 .. $]; 2712 2713 VarianceAccumulator!(double, VarianceAlgo.assumeZeroMean, Summation.naive) v; 2714 v.put(x); 2715 assert(v.variance(false).approxEqual(13.492188 / 5)); 2716 2717 VarianceAccumulator!(double, VarianceAlgo.assumeZeroMean, Summation.naive) w; 2718 w.put(y); 2719 v.put(w); 2720 assert(v.variance(false).approxEqual(54.76562 / 11)); 2721 } 2722 2723 /++ 2724 Calculates the variance of the input 2725 2726 By default, if `F` is not floating point type or complex type, then the result 2727 will have a `double` type if `F` is implicitly convertible to a floating point 2728 type or a type for which `isComplex!F` is true. 2729 2730 Params: 2731 F = controls type of output 2732 varianceAlgo = algorithm for calculating variance (default: VarianceAlgo.online) 2733 summation = algorithm for calculating sums (default: Summation.appropriate) 2734 Returns: 2735 The variance of the input, must be floating point or complex type 2736 +/ 2737 template variance( 2738 F, 2739 VarianceAlgo varianceAlgo = VarianceAlgo.online, 2740 Summation summation = Summation.appropriate) 2741 { 2742 /++ 2743 Params: 2744 r = range, must be finite iterable 2745 isPopulation = true if population variance, false if sample variance (default) 2746 +/ 2747 @fmamath meanType!F variance(Range)(Range r, bool isPopulation = false) 2748 if (isIterable!Range) 2749 { 2750 import core.lifetime: move; 2751 2752 alias G = typeof(return); 2753 auto varianceAccumulator = VarianceAccumulator!(G, varianceAlgo, ResolveSummationType!(summation, Range, G))(r.move); 2754 return varianceAccumulator.variance(isPopulation); 2755 } 2756 2757 /++ 2758 Params: 2759 ar = values 2760 +/ 2761 @fmamath meanType!F variance(scope const F[] ar...) 2762 { 2763 alias G = typeof(return); 2764 auto varianceAccumulator = VarianceAccumulator!(G, varianceAlgo, ResolveSummationType!(summation, const(G)[], G))(ar); 2765 return varianceAccumulator.variance(false); 2766 } 2767 } 2768 2769 /// ditto 2770 template variance( 2771 VarianceAlgo varianceAlgo = VarianceAlgo.online, 2772 Summation summation = Summation.appropriate) 2773 { 2774 /++ 2775 Params: 2776 r = range, must be finite iterable 2777 isPopulation = true if population variance, false if sample variance (default) 2778 +/ 2779 @fmamath meanType!Range variance(Range)(Range r, bool isPopulation = false) 2780 if(isIterable!Range) 2781 { 2782 import core.lifetime: move; 2783 2784 alias F = typeof(return); 2785 return .variance!(F, varianceAlgo, summation)(r.move, isPopulation); 2786 } 2787 2788 /++ 2789 Params: 2790 ar = values 2791 +/ 2792 @fmamath meanType!T variance(T)(scope const T[] ar...) 2793 { 2794 alias F = typeof(return); 2795 return .variance!(F, varianceAlgo, summation)(ar); 2796 } 2797 } 2798 2799 /// ditto 2800 template variance(F, string varianceAlgo, string summation = "appropriate") 2801 { 2802 mixin("alias variance = .variance!(F, VarianceAlgo." ~ varianceAlgo ~ ", Summation." ~ summation ~ ");"); 2803 } 2804 2805 /// ditto 2806 template variance(string varianceAlgo, string summation = "appropriate") 2807 { 2808 mixin("alias variance = .variance!(VarianceAlgo." ~ varianceAlgo ~ ", Summation." ~ summation ~ ");"); 2809 } 2810 2811 /// 2812 version(mir_test) 2813 @safe pure nothrow 2814 unittest 2815 { 2816 import mir.math.common: approxEqual; 2817 import mir.complex.math: capproxEqual = approxEqual; 2818 import mir.ndslice.slice: sliced; 2819 import mir.complex; 2820 alias C = Complex!double; 2821 2822 assert(variance([1.0, 2, 3]).approxEqual(2.0 / 2)); 2823 assert(variance([1.0, 2, 3], true).approxEqual(2.0 / 3)); 2824 2825 assert(variance([C(1, 3), C(2), C(3)]).capproxEqual(C(-4, -6) / 2)); 2826 2827 assert(variance!float([0, 1, 2, 3, 4, 5].sliced(3, 2)).approxEqual(17.5 / 5)); 2828 2829 static assert(is(typeof(variance!float([1, 2, 3])) == float)); 2830 } 2831 2832 /// Variance of vector 2833 version(mir_test) 2834 @safe pure nothrow 2835 unittest 2836 { 2837 import mir.math.common: approxEqual; 2838 import mir.ndslice.slice: sliced; 2839 2840 auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 2841 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 2842 2843 assert(x.variance.approxEqual(54.76562 / 11)); 2844 } 2845 2846 /// Variance of matrix 2847 version(mir_test) 2848 @safe pure 2849 unittest 2850 { 2851 import mir.math.common: approxEqual; 2852 import mir.ndslice.fuse: fuse; 2853 2854 auto x = [ 2855 [0.0, 1.0, 1.5, 2.0, 3.5, 4.25], 2856 [2.0, 7.5, 5.0, 1.0, 1.5, 0.0] 2857 ].fuse; 2858 2859 assert(x.variance.approxEqual(54.76562 / 11)); 2860 } 2861 2862 /// Column variance of matrix 2863 version(mir_test) 2864 @safe pure 2865 unittest 2866 { 2867 import mir.algorithm.iteration: all; 2868 import mir.math.common: approxEqual; 2869 import mir.ndslice.fuse: fuse; 2870 import mir.ndslice.topology: alongDim, byDim, map; 2871 2872 auto x = [ 2873 [0.0, 1.0, 1.5, 2.0], 2874 [3.5, 4.25, 2.0, 7.5], 2875 [5.0, 1.0, 1.5, 0.0] 2876 ].fuse; 2877 auto result = [13.16667 / 2, 7.041667 / 2, 0.1666667 / 2, 30.16667 / 2]; 2878 2879 // Use byDim or alongDim with map to compute variance of row/column. 2880 assert(x.byDim!1.map!variance.all!approxEqual(result)); 2881 assert(x.alongDim!0.map!variance.all!approxEqual(result)); 2882 2883 // FIXME 2884 // Without using map, computes the variance of the whole slice 2885 // assert(x.byDim!1.variance == x.sliced.variance); 2886 // assert(x.alongDim!0.variance == x.sliced.variance); 2887 } 2888 2889 /// Can also set algorithm type 2890 version(mir_test) 2891 @safe pure nothrow 2892 unittest 2893 { 2894 import mir.math.common: approxEqual; 2895 import mir.ndslice.slice: sliced; 2896 2897 auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 2898 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 2899 2900 auto x = a + 1_000_000_000; 2901 2902 auto y = x.variance; 2903 assert(y.approxEqual(54.76562 / 11)); 2904 2905 // The naive algorithm is numerically unstable in this case 2906 auto z0 = x.variance!"naive"; 2907 assert(!z0.approxEqual(y)); 2908 2909 // But the two-pass algorithm provides a consistent answer 2910 auto z1 = x.variance!"twoPass"; 2911 assert(z1.approxEqual(y)); 2912 2913 // And the assumeZeroMean algorithm is way off 2914 auto z2 = x.variance!"assumeZeroMean"; 2915 assert(z2.approxEqual(1.2e19 / 11)); 2916 } 2917 2918 /// Can also set algorithm or output type 2919 version(mir_test) 2920 @safe pure nothrow 2921 unittest 2922 { 2923 import mir.math.common: approxEqual; 2924 import mir.ndslice.slice: sliced; 2925 import mir.ndslice.topology: repeat; 2926 2927 //Set population variance, variance algorithm, sum algorithm or output type 2928 2929 auto a = [1.0, 1e100, 1, -1e100].sliced; 2930 auto x = a * 10_000; 2931 2932 bool populationTrueRT = true; 2933 bool populationFalseRT = false; 2934 enum PopulationTrueCT = true; 2935 2936 /++ 2937 Due to Floating Point precision, when centering `x`, subtracting the mean 2938 from the second and fourth numbers has no effect. Further, after centering 2939 and squaring `x`, the first and third numbers in the slice have precision 2940 too low to be included in the centered sum of squares. 2941 +/ 2942 assert(x.variance(populationFalseRT).approxEqual(2.0e208 / 3)); 2943 assert(x.variance(populationTrueRT).approxEqual(2.0e208 / 4)); 2944 assert(x.variance(PopulationTrueCT).approxEqual(2.0e208 / 4)); 2945 2946 assert(x.variance!("online").approxEqual(2.0e208 / 3)); 2947 assert(x.variance!("online", "kbn").approxEqual(2.0e208 / 3)); 2948 assert(x.variance!("online", "kb2").approxEqual(2.0e208 / 3)); 2949 assert(x.variance!("online", "precise").approxEqual(2.0e208 / 3)); 2950 assert(x.variance!(double, "online", "precise").approxEqual(2.0e208 / 3)); 2951 assert(x.variance!(double, "online", "precise")(populationTrueRT).approxEqual(2.0e208 / 4)); 2952 2953 auto y = uint.max.repeat(3); 2954 auto z = y.variance!ulong; 2955 assert(z == 0.0); 2956 static assert(is(typeof(z) == double)); 2957 } 2958 2959 /++ 2960 For integral slices, pass output type as template parameter to ensure output 2961 type is correct. 2962 +/ 2963 version(mir_test) 2964 @safe pure nothrow 2965 unittest 2966 { 2967 import mir.math.common: approxEqual; 2968 import mir.ndslice.slice: sliced; 2969 2970 auto x = [0, 1, 1, 2, 4, 4, 2971 2, 7, 5, 1, 2, 0].sliced; 2972 2973 auto y = x.variance; 2974 assert(y.approxEqual(50.91667 / 11)); 2975 static assert(is(typeof(y) == double)); 2976 2977 assert(x.variance!float.approxEqual(50.91667 / 11)); 2978 } 2979 2980 /++ 2981 Variance works for complex numbers and other user-defined types (provided they 2982 can be converted to a floating point or complex type) 2983 +/ 2984 version(mir_test) 2985 @safe pure nothrow 2986 unittest 2987 { 2988 import mir.complex.math: approxEqual; 2989 import mir.ndslice.slice: sliced; 2990 import mir.complex; 2991 alias C = Complex!double; 2992 2993 auto x = [C(1, 2), C(2, 3), C(3, 4), C(4, 5)].sliced; 2994 assert(x.variance.approxEqual((C(0, 10)) / 3)); 2995 } 2996 2997 /// Compute variance along specified dimention of tensors 2998 version(mir_test) 2999 @safe pure 3000 unittest 3001 { 3002 import mir.algorithm.iteration: all; 3003 import mir.math.common: approxEqual; 3004 import mir.ndslice.fuse: fuse; 3005 import mir.ndslice.topology: as, iota, alongDim, map, repeat; 3006 3007 auto x = [ 3008 [0.0, 1, 2], 3009 [3.0, 4, 5] 3010 ].fuse; 3011 3012 assert(x.variance.approxEqual(17.5 / 5)); 3013 3014 auto m0 = [4.5, 4.5, 4.5]; 3015 assert(x.alongDim!0.map!variance.all!approxEqual(m0)); 3016 assert(x.alongDim!(-2).map!variance.all!approxEqual(m0)); 3017 3018 auto m1 = [1.0, 1.0]; 3019 assert(x.alongDim!1.map!variance.all!approxEqual(m1)); 3020 assert(x.alongDim!(-1).map!variance.all!approxEqual(m1)); 3021 3022 assert(iota(2, 3, 4, 5).as!double.alongDim!0.map!variance.all!approxEqual(repeat(3600.0 / 2, 3, 4, 5))); 3023 } 3024 3025 /// Arbitrary variance 3026 version(mir_test) 3027 @safe pure nothrow @nogc 3028 unittest 3029 { 3030 assert(variance(1.0, 2, 3) == 1.0); 3031 assert(variance!float(1, 2, 3) == 1f); 3032 } 3033 3034 version(mir_test) 3035 @safe pure nothrow 3036 unittest 3037 { 3038 import mir.math.common: approxEqual; 3039 3040 assert([1.0, 2, 3, 4].variance.approxEqual(5.0 / 3)); 3041 } 3042 3043 version(mir_test) 3044 @safe pure nothrow 3045 unittest 3046 { 3047 import mir.algorithm.iteration: all; 3048 import mir.math.common: approxEqual; 3049 import mir.ndslice.topology: iota, alongDim, map; 3050 3051 auto x = iota([2, 2], 1); 3052 auto y = x.alongDim!1.map!variance; 3053 assert(y.all!approxEqual([0.5, 0.5])); 3054 static assert(is(meanType!(typeof(y)) == double)); 3055 } 3056 3057 version(mir_test) 3058 @safe pure nothrow @nogc 3059 unittest 3060 { 3061 import mir.math.common: approxEqual; 3062 import mir.ndslice.slice: sliced; 3063 3064 static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 3065 2.0, 7.5, 5.0, 1.0, 1.5, 0.0]; 3066 3067 assert(x.sliced.variance.approxEqual(54.76562 / 11)); 3068 assert(x.sliced.variance!float.approxEqual(54.76562 / 11)); 3069 } 3070 3071 /// 3072 package(mir) 3073 template stdevType(T) 3074 { 3075 import mir.internal.utility: isFloatingPoint; 3076 3077 alias U = meanType!T; 3078 3079 static if (isFloatingPoint!U) { 3080 alias stdevType = U; 3081 } else { 3082 static assert(0, "stdevType: Can't calculate standard deviation of elements of type " ~ U.stringof); 3083 } 3084 } 3085 3086 version(mir_test) 3087 @safe pure nothrow @nogc 3088 unittest 3089 { 3090 static assert(is(stdevType!(int[]) == double)); 3091 static assert(is(stdevType!(double[]) == double)); 3092 static assert(is(stdevType!(float[]) == float)); 3093 } 3094 3095 version(mir_test) 3096 @safe pure nothrow @nogc 3097 unittest 3098 { 3099 static struct Foo { 3100 float x; 3101 alias x this; 3102 } 3103 3104 static assert(is(stdevType!(Foo[]) == float)); 3105 } 3106 3107 /++ 3108 Calculates the standard deviation of the input 3109 3110 By default, if `F` is not floating point type, then the result will have a 3111 `double` type if `F` is implicitly convertible to a floating point type. 3112 3113 Params: 3114 F = controls type of output 3115 varianceAlgo = algorithm for calculating variance (default: VarianceAlgo.online) 3116 summation = algorithm for calculating sums (default: Summation.appropriate) 3117 Returns: 3118 The standard deviation of the input, must be floating point type type 3119 +/ 3120 template standardDeviation( 3121 F, 3122 VarianceAlgo varianceAlgo = VarianceAlgo.online, 3123 Summation summation = Summation.appropriate) 3124 { 3125 import mir.math.common: sqrt; 3126 3127 /++ 3128 Params: 3129 r = range, must be finite iterable 3130 isPopulation = true if population standard deviation, false if sample standard deviation (default) 3131 +/ 3132 @fmamath stdevType!F standardDeviation(Range)(Range r, bool isPopulation = false) 3133 if (isIterable!Range) 3134 { 3135 import core.lifetime: move; 3136 alias G = typeof(return); 3137 return r.move.variance!(G, varianceAlgo, ResolveSummationType!(summation, Range, G))(isPopulation).sqrt; 3138 } 3139 3140 /++ 3141 Params: 3142 ar = values 3143 +/ 3144 @fmamath stdevType!F standardDeviation(scope const F[] ar...) 3145 { 3146 alias G = typeof(return); 3147 return ar.variance!(G, varianceAlgo, ResolveSummationType!(summation, const(G)[], G)).sqrt; 3148 } 3149 } 3150 3151 /// ditto 3152 template standardDeviation( 3153 VarianceAlgo varianceAlgo = VarianceAlgo.online, 3154 Summation summation = Summation.appropriate) 3155 { 3156 /++ 3157 Params: 3158 r = range, must be finite iterable 3159 isPopulation = true if population standard deviation, false if sample standard deviation (default) 3160 +/ 3161 @fmamath stdevType!Range standardDeviation(Range)(Range r, bool isPopulation = false) 3162 if(isIterable!Range) 3163 { 3164 import core.lifetime: move; 3165 3166 alias F = typeof(return); 3167 return .standardDeviation!(F, varianceAlgo, summation)(r.move, isPopulation); 3168 } 3169 3170 /++ 3171 Params: 3172 ar = values 3173 +/ 3174 @fmamath stdevType!T standardDeviation(T)(scope const T[] ar...) 3175 { 3176 alias F = typeof(return); 3177 return .standardDeviation!(F, varianceAlgo, summation)(ar); 3178 } 3179 } 3180 3181 /// ditto 3182 template standardDeviation(F, string varianceAlgo, string summation = "appropriate") 3183 { 3184 mixin("alias standardDeviation = .standardDeviation!(F, VarianceAlgo." ~ varianceAlgo ~ ", Summation." ~ summation ~ ");"); 3185 } 3186 3187 /// ditto 3188 template standardDeviation(string varianceAlgo, string summation = "appropriate") 3189 { 3190 mixin("alias standardDeviation = .standardDeviation!(VarianceAlgo." ~ varianceAlgo ~ ", Summation." ~ summation ~ ");"); 3191 } 3192 3193 /// 3194 version(mir_test) 3195 @safe pure nothrow 3196 unittest 3197 { 3198 import mir.math.common: approxEqual, sqrt; 3199 import mir.ndslice.slice: sliced; 3200 3201 assert(standardDeviation([1.0, 2, 3]).approxEqual(sqrt(2.0 / 2))); 3202 assert(standardDeviation([1.0, 2, 3], true).approxEqual(sqrt(2.0 / 3))); 3203 3204 assert(standardDeviation!float([0, 1, 2, 3, 4, 5].sliced(3, 2)).approxEqual(sqrt(17.5 / 5))); 3205 3206 static assert(is(typeof(standardDeviation!float([1, 2, 3])) == float)); 3207 } 3208 3209 /// Standard deviation of vector 3210 version(mir_test) 3211 @safe pure nothrow 3212 unittest 3213 { 3214 import mir.math.common: approxEqual, sqrt; 3215 import mir.ndslice.slice: sliced; 3216 3217 auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 3218 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 3219 3220 assert(x.standardDeviation.approxEqual(sqrt(54.76562 / 11))); 3221 } 3222 3223 /// Standard deviation of matrix 3224 version(mir_test) 3225 @safe pure 3226 unittest 3227 { 3228 import mir.math.common: approxEqual, sqrt; 3229 import mir.ndslice.fuse: fuse; 3230 3231 auto x = [ 3232 [0.0, 1.0, 1.5, 2.0, 3.5, 4.25], 3233 [2.0, 7.5, 5.0, 1.0, 1.5, 0.0] 3234 ].fuse; 3235 3236 assert(x.standardDeviation.approxEqual(sqrt(54.76562 / 11))); 3237 } 3238 3239 /// Column standard deviation of matrix 3240 version(mir_test) 3241 @safe pure 3242 unittest 3243 { 3244 import mir.algorithm.iteration: all; 3245 import mir.math.common: approxEqual, sqrt; 3246 import mir.ndslice.fuse: fuse; 3247 import mir.ndslice.topology: alongDim, byDim, map; 3248 3249 auto x = [ 3250 [0.0, 1.0, 1.5, 2.0], 3251 [3.5, 4.25, 2.0, 7.5], 3252 [5.0, 1.0, 1.5, 0.0] 3253 ].fuse; 3254 auto result = [13.16667 / 2, 7.041667 / 2, 0.1666667 / 2, 30.16667 / 2].map!sqrt; 3255 3256 // Use byDim or alongDim with map to compute standardDeviation of row/column. 3257 assert(x.byDim!1.map!standardDeviation.all!approxEqual(result)); 3258 assert(x.alongDim!0.map!standardDeviation.all!approxEqual(result)); 3259 3260 // FIXME 3261 // Without using map, computes the standardDeviation of the whole slice 3262 // assert(x.byDim!1.standardDeviation == x.sliced.standardDeviation); 3263 // assert(x.alongDim!0.standardDeviation == x.sliced.standardDeviation); 3264 } 3265 3266 /// Can also set algorithm type 3267 version(mir_test) 3268 @safe pure nothrow 3269 unittest 3270 { 3271 import mir.math.common: approxEqual, sqrt; 3272 import mir.ndslice.slice: sliced; 3273 3274 auto a = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 3275 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 3276 3277 auto x = a + 1_000_000_000; 3278 3279 auto y = x.standardDeviation; 3280 assert(y.approxEqual(sqrt(54.76562 / 11))); 3281 3282 // The naive algorithm is numerically unstable in this case 3283 auto z0 = x.standardDeviation!"naive"; 3284 assert(!z0.approxEqual(y)); 3285 3286 // But the two-pass algorithm provides a consistent answer 3287 auto z1 = x.standardDeviation!"twoPass"; 3288 assert(z1.approxEqual(y)); 3289 } 3290 3291 /// Can also set algorithm or output type 3292 version(mir_test) 3293 @safe pure nothrow 3294 unittest 3295 { 3296 import mir.math.common: approxEqual, sqrt; 3297 import mir.ndslice.slice: sliced; 3298 import mir.ndslice.topology: repeat; 3299 3300 //Set population standard deviation, standardDeviation algorithm, sum algorithm or output type 3301 3302 auto a = [1.0, 1e100, 1, -1e100].sliced; 3303 auto x = a * 10_000; 3304 3305 bool populationTrueRT = true; 3306 bool populationFalseRT = false; 3307 enum PopulationTrueCT = true; 3308 3309 /++ 3310 Due to Floating Point precision, when centering `x`, subtracting the mean 3311 from the second and fourth numbers has no effect. Further, after centering 3312 and squaring `x`, the first and third numbers in the slice have precision 3313 too low to be included in the centered sum of squares. 3314 +/ 3315 assert(x.standardDeviation(populationFalseRT).approxEqual(sqrt(2.0e208 / 3))); 3316 assert(x.standardDeviation(populationTrueRT).approxEqual(sqrt(2.0e208 / 4))); 3317 assert(x.standardDeviation(PopulationTrueCT).approxEqual(sqrt(2.0e208 / 4))); 3318 3319 assert(x.standardDeviation!("online").approxEqual(sqrt(2.0e208 / 3))); 3320 assert(x.standardDeviation!("online", "kbn").approxEqual(sqrt(2.0e208 / 3))); 3321 assert(x.standardDeviation!("online", "kb2").approxEqual(sqrt(2.0e208 / 3))); 3322 assert(x.standardDeviation!("online", "precise").approxEqual(sqrt(2.0e208 / 3))); 3323 assert(x.standardDeviation!(double, "online", "precise").approxEqual(sqrt(2.0e208 / 3))); 3324 assert(x.standardDeviation!(double, "online", "precise")(populationTrueRT).approxEqual(sqrt(2.0e208 / 4))); 3325 3326 auto y = uint.max.repeat(3); 3327 auto z = y.standardDeviation!ulong; 3328 assert(z == 0.0); 3329 static assert(is(typeof(z) == double)); 3330 } 3331 3332 /++ 3333 For integral slices, pass output type as template parameter to ensure output 3334 type is correct. 3335 +/ 3336 version(mir_test) 3337 @safe pure nothrow 3338 unittest 3339 { 3340 import mir.math.common: approxEqual, sqrt; 3341 import mir.ndslice.slice: sliced; 3342 3343 auto x = [0, 1, 1, 2, 4, 4, 3344 2, 7, 5, 1, 2, 0].sliced; 3345 3346 auto y = x.standardDeviation; 3347 assert(y.approxEqual(sqrt(50.91667 / 11))); 3348 static assert(is(typeof(y) == double)); 3349 3350 assert(x.standardDeviation!float.approxEqual(sqrt(50.91667 / 11))); 3351 } 3352 3353 /++ 3354 Variance works for other user-defined types (provided they 3355 can be converted to a floating point) 3356 +/ 3357 version(mir_test) 3358 @safe pure nothrow 3359 unittest 3360 { 3361 import mir.ndslice.slice: sliced; 3362 3363 static struct Foo { 3364 float x; 3365 alias x this; 3366 } 3367 3368 Foo[] foo = [Foo(1f), Foo(2f), Foo(3f)]; 3369 assert(foo.standardDeviation == 1f); 3370 } 3371 3372 /// Compute standard deviation along specified dimention of tensors 3373 version(mir_test) 3374 @safe pure 3375 unittest 3376 { 3377 import mir.algorithm.iteration: all; 3378 import mir.math.common: approxEqual, sqrt; 3379 import mir.ndslice.fuse: fuse; 3380 import mir.ndslice.topology: as, iota, alongDim, map, repeat; 3381 3382 auto x = [ 3383 [0.0, 1, 2], 3384 [3.0, 4, 5] 3385 ].fuse; 3386 3387 assert(x.standardDeviation.approxEqual(sqrt(17.5 / 5))); 3388 3389 auto m0 = repeat(sqrt(4.5), 3); 3390 assert(x.alongDim!0.map!standardDeviation.all!approxEqual(m0)); 3391 assert(x.alongDim!(-2).map!standardDeviation.all!approxEqual(m0)); 3392 3393 auto m1 = [1.0, 1.0]; 3394 assert(x.alongDim!1.map!standardDeviation.all!approxEqual(m1)); 3395 assert(x.alongDim!(-1).map!standardDeviation.all!approxEqual(m1)); 3396 3397 assert(iota(2, 3, 4, 5).as!double.alongDim!0.map!standardDeviation.all!approxEqual(repeat(sqrt(3600.0 / 2), 3, 4, 5))); 3398 } 3399 3400 /// Arbitrary standard deviation 3401 version(mir_test) 3402 @safe pure nothrow @nogc 3403 unittest 3404 { 3405 import mir.math.common: sqrt; 3406 3407 assert(standardDeviation(1.0, 2, 3) == 1.0); 3408 assert(standardDeviation!float(1, 2, 3) == 1f); 3409 } 3410 3411 version(mir_test) 3412 @safe pure nothrow 3413 unittest 3414 { 3415 import mir.math.common: approxEqual, sqrt; 3416 assert([1.0, 2, 3, 4].standardDeviation.approxEqual(sqrt(5.0 / 3))); 3417 } 3418 3419 version(mir_test) 3420 @safe pure nothrow 3421 unittest 3422 { 3423 import mir.algorithm.iteration: all; 3424 import mir.math.common: approxEqual, sqrt; 3425 import mir.ndslice.topology: iota, alongDim, map; 3426 3427 auto x = iota([2, 2], 1); 3428 auto y = x.alongDim!1.map!standardDeviation; 3429 assert(y.all!approxEqual([sqrt(0.5), sqrt(0.5)])); 3430 static assert(is(meanType!(typeof(y)) == double)); 3431 } 3432 3433 version(mir_test) 3434 @safe pure @nogc nothrow 3435 unittest 3436 { 3437 import mir.math.common: approxEqual, sqrt; 3438 import mir.ndslice.slice: sliced; 3439 3440 static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 3441 2.0, 7.5, 5.0, 1.0, 1.5, 0.0]; 3442 3443 assert(x.sliced.standardDeviation.approxEqual(sqrt(54.76562 / 11))); 3444 assert(x.sliced.standardDeviation!float.approxEqual(sqrt(54.76562 / 11))); 3445 } 3446 3447 /++ 3448 A linear regression model with a single explanatory variable. 3449 +/ 3450 template simpleLinearRegression(Summation summation = Summation.kbn) 3451 { 3452 import mir.ndslice.slice; 3453 import mir.primitives: isInputRange; 3454 3455 /++ 3456 Params: 3457 x = `x[i]` points 3458 y = `f(x[i])` values 3459 Returns: 3460 The pair of shift and slope of the linear curve. 3461 +/ 3462 @fmamath 3463 sumType!YRange[2] 3464 simpleLinearRegression(XRange, YRange)(XRange x, YRange y) @safe 3465 if (isInputRange!XRange && isInputRange!YRange && !(isArray!XRange && isArray!YRange) && isFloatingPoint!(sumType!YRange)) 3466 in { 3467 static if (hasLength!XRange && hasLength!YRange) 3468 assert(x.length == y.length); 3469 } 3470 do { 3471 alias X = typeof(sumType!XRange.init * sumType!XRange.init); 3472 alias Y = sumType!YRange; 3473 enum summationX = !__traits(isIntegral, X) ? summation: Summation.naive; 3474 Summator!(X, summationX) xms = 0; 3475 Summator!(Y, summation) yms = 0; 3476 Summator!(X, summationX) xxms = 0; 3477 Summator!(Y, summation) xyms = 0; 3478 3479 static if (hasLength!XRange) 3480 sizediff_t n = x.length; 3481 else 3482 sizediff_t n = 0; 3483 3484 while (!x.empty) 3485 { 3486 static if (!(hasLength!XRange && hasLength!YRange)) 3487 assert(!y.empty); 3488 3489 static if (!hasLength!XRange) 3490 n++; 3491 3492 auto xi = x.front; 3493 auto yi = y.front; 3494 xms.put(xi); 3495 yms.put(yi); 3496 xxms.put(xi * xi); 3497 xyms.put(xi * yi); 3498 3499 y.popFront; 3500 x.popFront; 3501 } 3502 3503 static if (!(hasLength!XRange && hasLength!YRange)) 3504 assert(y.empty); 3505 3506 auto xm = xms.sum; 3507 auto ym = yms.sum; 3508 auto xxm = xxms.sum; 3509 auto xym = xyms.sum; 3510 3511 auto slope = (xym * n - xm * ym) / (xxm * n - xm * xm); 3512 3513 return [(ym - slope * xm) / n, slope]; 3514 } 3515 3516 /// ditto 3517 @fmamath 3518 sumType!(Y[])[2] 3519 simpleLinearRegression(X, Y)(scope const X[] x, scope const Y[] y) @safe 3520 { 3521 return .simpleLinearRegression!summation(x.sliced, y.sliced); 3522 } 3523 } 3524 3525 /// ditto 3526 template simpleLinearRegression(string summation) 3527 { 3528 mixin("alias simpleLinearRegression = .simpleLinearRegression!(Summation." ~ summation ~ ");"); 3529 } 3530 3531 /// 3532 version(mir_test) 3533 @safe pure nothrow @nogc 3534 unittest 3535 { 3536 import mir.math.common: approxEqual; 3537 static immutable x = [0, 1, 2, 3]; 3538 static immutable y = [-1, 0.2, 0.9, 2.1]; 3539 auto params = x.simpleLinearRegression(y); 3540 assert(params[0].approxEqual(-0.95)); // shift 3541 assert(params[1].approxEqual(1)); // slope 3542 }