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