1 /++ 2 This module contains algorithms for descriptive statistics with weights. 3 4 License: $(HTTP www.apache.org/licenses/LICENSE-2.0, Apache-2.0) 5 6 Authors: John Michael Hall 7 8 Copyright: 2022 Mir Stat Authors. 9 10 Macros: 11 SUBREF = $(REF_ALTTEXT $(TT $2), $2, mir, stat, $1)$(NBSP) 12 SUB2REF = $(REF_ALTTEXT $(TT $2), $2, mir, stat, descriptive, $1)$(NBSP) 13 MATHREF = $(GREF_ALTTEXT mir-algorithm, $(TT $2), $2, mir, math, $1)$(NBSP) 14 NDSLICEREF = $(GREF_ALTTEXT mir-algorithm, $(TT $2), $2, mir, ndslice, $1)$(NBSP) 15 T2=$(TR $(TDNW $(LREF $1)) $(TD $+)) 16 T3=$(TR $(TDNW $(LREF $1)) $(TD $2) $(TD $+)) 17 T4=$(TR $(TDNW $(LREF $1)) $(TD $2) $(TD $3) $(TD $4)) 18 19 +/ 20 21 module mir.stat.descriptive.weighted; 22 23 import mir.math.sum: ResolveSummationType, Summation, Summator; 24 25 private void putter2(Slices, T, U, Summation summation1, Summation summation2) 26 (scope Slices slices, ref Summator!(T, summation1) seed1, ref Summator!(U, summation2) seed2) 27 { 28 import mir.functional: Tuple; 29 static if (is(Slices == Tuple!(V1, V2), V1, V2)) { 30 seed1.put(slices[0]); 31 seed2.put(slices[1]); 32 } else { 33 import mir.ndslice.internal: frontOf2; 34 do 35 { 36 frontOf2!(slices)[0].putter2(seed1, seed2); 37 slices.popFront; 38 } 39 while(!slices.empty); 40 } 41 } 42 43 /++ 44 Assumptions used for weighted moments 45 +/ 46 enum AssumeWeights : bool 47 { 48 /++ 49 Primary, does not assume weights sum to one 50 +/ 51 primary, 52 53 /++ 54 Assumes weights sum to one 55 +/ 56 sumToOne 57 } 58 59 /++ 60 Output range for wmean. 61 +/ 62 struct WMeanAccumulator(T, Summation summation, AssumeWeights assumeWeights, 63 U = T, Summation weightsSummation = summation) 64 { 65 import mir.ndslice.slice: isConvertibleToSlice, isSlice, kindOf; 66 import std.range.primitives: isInputRange; 67 import std.traits: isIterable; 68 69 /// 70 Summator!(T, summation) wsummator; 71 72 static if (!assumeWeights) { 73 /// 74 Summator!(U, weightsSummation) weights; 75 } 76 77 /// 78 F wmean(F = T)() const @safe @property pure nothrow @nogc 79 { 80 static if (assumeWeights) { 81 return this.wsum!F; 82 } else { 83 assert(this.weight!F != 0, "weight must not equal zero"); 84 return this.wsum!F / this.weight!F; 85 } 86 } 87 88 /// 89 F wsum(F = T)() const @safe @property pure nothrow @nogc 90 { 91 return cast(F) wsummator.sum; 92 } 93 94 /// 95 F weight(F = U)() const @safe @property pure nothrow @nogc 96 { 97 return cast(F) weights.sum; 98 } 99 100 /// 101 void put(Slice1, Slice2)(Slice1 s, Slice2 w) 102 if (isSlice!Slice1 && isSlice!Slice2) 103 { 104 static assert (Slice1.N == Slice2.N, "s and w must have the same number of dimensions"); 105 static assert (kindOf!Slice1 == kindOf!Slice2, "s and w must have the same kind"); 106 107 import mir.ndslice.slice: Contiguous; 108 import mir.ndslice.topology: zip, map; 109 110 assert(s._lengths == w._lengths, "WMeanAcumulator.put: both slices must have the same lengths"); 111 112 static if (kindOf!Slice1 != Contiguous && Slice1.N > 1) { 113 assert(s.strides == w.strides, "WMeanAccumulator.put: cannot put canonical and universal slices when strides do not match"); 114 auto combine = s.zip!true(w); 115 } else { 116 auto combine = s.zip!false(w); 117 } 118 119 static if (assumeWeights) { 120 auto combine2 = combine.map!"a * b"; 121 wsummator.put(combine2); 122 } else { 123 auto combine2 = combine.map!("b", "a * b"); 124 combine2.putter2(weights, wsummator); 125 } 126 } 127 128 /// 129 void put(SliceLike1, SliceLike2)(SliceLike1 s, SliceLike2 w) 130 if (isConvertibleToSlice!SliceLike1 && !isSlice!SliceLike1 && 131 isConvertibleToSlice!SliceLike2 && !isSlice!SliceLike2) 132 { 133 import mir.ndslice.slice: toSlice; 134 this.put(s.toSlice, w.toSlice); 135 } 136 137 /// 138 void put(Range)(Range r) 139 if (isIterable!Range && !assumeWeights) 140 { 141 import mir.primitives: hasShape, elementCount; 142 static if (hasShape!Range) { 143 wsummator.put(r); 144 weights.put(cast(U) r.elementCount); 145 } else { 146 foreach(x; r) 147 { 148 this.put(x); 149 } 150 } 151 } 152 153 /// 154 void put(RangeA, RangeB)(RangeA r, RangeB w) 155 if (isInputRange!RangeA && !isConvertibleToSlice!RangeA && 156 isInputRange!RangeB && !isConvertibleToSlice!RangeB) 157 { 158 do 159 { 160 assert(!(!r.empty && w.empty) && !(r.empty && !w.empty), 161 "r and w must both be empty at the same time, one cannot be empty while the other has remaining items"); 162 this.put(r.front, w.front); 163 r.popFront; 164 w.popFront; 165 } while(!r.empty || !w.empty); // Using an || instead of && so that the loop does not end early. mis-matched lengths of r and w sould be caught by above assert 166 } 167 168 /// 169 void put()(T x, U w) 170 { 171 static if (!assumeWeights) { 172 weights.put(w); 173 } 174 wsummator.put(x * w); 175 } 176 177 /// 178 void put()(T x) 179 if (!assumeWeights) 180 { 181 weights.put(cast(U) 1); 182 wsummator.put(x); 183 } 184 185 /// 186 void put(F = T, G = U)(WMeanAccumulator!(F, summation, assumeWeights, G, weightsSummation) wm) 187 if (!assumeWeights) // because calculating is easier. When assumeWeightsSumtoOne = true, need to divide original wsummator and wm by 2. 188 { 189 weights.put(cast(U) wm.weights); 190 wsummator.put(cast(T) wm.wsummator); 191 } 192 } 193 194 /// Assume weights sum to 1 195 version(mir_stat_test) 196 @safe pure nothrow 197 unittest 198 { 199 import mir.math.sum: Summation; 200 import mir.ndslice.slice: sliced; 201 import mir.test: should; 202 203 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.sumToOne) x; 204 x.put([0.0, 1, 2, 3, 4].sliced, [0.2, 0.2, 0.2, 0.2, 0.2].sliced); 205 x.wmean.should == 2; 206 x.put(5, 0.0); 207 x.wmean.should == 2; 208 } 209 210 // dynamic array test, assume weights sum to 1 211 version(mir_stat_test) 212 @safe pure nothrow 213 unittest 214 { 215 import mir.math.sum: Summation; 216 import mir.test: should; 217 218 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.sumToOne) x; 219 x.put([0.0, 1, 2, 3, 4], [0.2, 0.2, 0.2, 0.2, 0.2]); 220 x.wmean.should == 2; 221 } 222 223 // static array test, assume weights sum to 1 224 version(mir_stat_test) 225 @safe pure nothrow @nogc 226 unittest 227 { 228 import mir.math.sum: Summation; 229 import mir.test: should; 230 231 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.sumToOne) x; 232 static immutable y = [0.0, 1, 2, 3, 4]; 233 static immutable w = [0.2, 0.2, 0.2, 0.2, 0.2]; 234 x.put(y, w); 235 x.wmean.should == 2; 236 } 237 238 // 2-d slice test, assume weights sum to 1 239 version(mir_stat_test) 240 @safe pure 241 unittest 242 { 243 import mir.math.sum: Summation; 244 import mir.ndslice.fuse: fuse; 245 import mir.test: shouldApprox; 246 247 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.sumToOne) x; 248 auto y = [ 249 [0.0, 1, 2], 250 [3.0, 4, 5] 251 ].fuse; 252 auto w = [ 253 [1.0 / 21, 2.0 / 21, 3.0 / 21], 254 [4.0 / 21, 5.0 / 21, 6.0 / 21] 255 ].fuse; 256 x.put(y, w); 257 x.wmean.shouldApprox == 70.0 / 21; 258 } 259 260 // universal 2-d slice test, assume weights sum to 1, using map 261 version(mir_stat_test) 262 @safe pure nothrow 263 unittest 264 { 265 import mir.math.sum: Summation; 266 import mir.ndslice.topology: iota, map, universal; 267 import mir.test: shouldApprox; 268 269 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.sumToOne) x; 270 auto y = iota([2, 3]).universal; 271 auto w = iota([2, 3], 1).map!(a => a / 21.0).universal; 272 x.put(y, w); 273 x.wmean.shouldApprox == 70.0 / 21; 274 } 275 276 // 2-d canonical slice test, assume weights sum to 1, using map 277 version(mir_stat_test) 278 @safe pure nothrow 279 unittest 280 { 281 import mir.math.sum: Summation; 282 import mir.ndslice.topology: canonical, iota, map; 283 import mir.test: shouldApprox; 284 285 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.sumToOne) x; 286 auto y = iota([2, 3]).canonical; 287 auto w = iota([2, 3], 1).map!(a => a / 21.0).canonical; 288 x.put(y, w); 289 x.wmean.shouldApprox == 70.0 / 21; 290 } 291 292 /// Do not assume weights sum to 1 293 version(mir_stat_test) 294 @safe pure nothrow 295 unittest 296 { 297 import mir.math.sum: Summation; 298 import mir.ndslice.slice: sliced; 299 import mir.test: shouldApprox; 300 301 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x; 302 x.put([0.0, 1, 2, 3, 4].sliced, [1, 2, 3, 4, 5].sliced); 303 x.wmean.shouldApprox == 40.0 / 15; 304 x.put(5, 6); 305 x.wmean.shouldApprox == 70.0 / 21; 306 } 307 308 // dynamic array test, do not assume weights sum to 1 309 version(mir_stat_test) 310 @safe pure nothrow 311 unittest 312 { 313 import mir.math.sum: Summation; 314 import mir.test: shouldApprox; 315 316 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x; 317 x.put([0.0, 1, 2, 3, 4], [1, 2, 3, 4, 5]); 318 x.wmean.shouldApprox == 40.0 / 15; 319 } 320 321 // static array test, do not assume weights sum to 1 322 version(mir_stat_test) 323 @safe pure nothrow @nogc 324 unittest 325 { 326 import mir.math.sum: Summation; 327 import mir.test: shouldApprox; 328 329 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x; 330 static immutable y = [0.0, 1, 2, 3, 4]; 331 static immutable w = [1, 2, 3, 4, 5]; 332 x.put(y, w); 333 x.wmean.shouldApprox == 40.0 / 15; 334 } 335 336 // 2-d slice test, do not assume weights sum to 1 337 version(mir_stat_test) 338 @safe pure 339 unittest 340 { 341 import mir.math.sum: Summation; 342 import mir.ndslice.fuse: fuse; 343 import mir.test: shouldApprox; 344 345 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x; 346 auto y = [ 347 [0.0, 1, 2], 348 [3.0, 4, 5] 349 ].fuse; 350 auto w = [ 351 [1.0, 2, 3], 352 [4.0, 5, 6] 353 ].fuse; 354 x.put(y, w); 355 x.wmean.shouldApprox == 70.0 / 21; 356 } 357 358 // universal slice test, do not assume weights sum to 1 359 version(mir_stat_test) 360 @safe pure nothrow 361 unittest 362 { 363 import mir.math.sum: Summation; 364 import mir.ndslice.topology: iota, universal; 365 import mir.test: shouldApprox; 366 367 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x; 368 auto y = iota(6).universal; 369 auto w = iota([6], 1).universal; 370 x.put(y, w); 371 x.wmean.shouldApprox == 70.0 / 21; 372 } 373 374 // canonical slice test, do not assume weights sum to 1 375 version(mir_stat_test) 376 @safe pure nothrow 377 unittest 378 { 379 import mir.math.sum: Summation; 380 import mir.ndslice.topology: canonical, iota; 381 import mir.test: shouldApprox; 382 383 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x; 384 auto y = iota(6).canonical; 385 auto w = iota([6], 1).canonical; 386 x.put(y, w); 387 x.wmean.shouldApprox == 70.0 / 21; 388 } 389 390 // 2-d universal slice test, do not assume weights sum to 1 391 version(mir_stat_test) 392 @safe pure nothrow 393 unittest 394 { 395 import mir.math.sum: Summation; 396 import mir.ndslice.topology: iota, universal; 397 import mir.test: shouldApprox; 398 399 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x; 400 auto y = iota([2, 3]).universal; 401 auto w = iota([2, 3], 1).universal; 402 x.put(y, w); 403 x.wmean.shouldApprox == 70.0 / 21; 404 } 405 406 // 2-d canonical slice test, do not assume weights sum to 1 407 version(mir_stat_test) 408 @safe pure nothrow 409 unittest 410 { 411 import mir.math.sum: Summation; 412 import mir.ndslice.topology: canonical, iota; 413 import mir.test: shouldApprox; 414 415 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x; 416 auto y = iota([2, 3]).canonical; 417 auto w = iota([2, 3], 1).canonical; 418 x.put(y, w); 419 x.wmean.shouldApprox == 70.0 / 21; 420 } 421 422 /// Assume no weights, like MeanAccumulator 423 version(mir_stat_test) 424 @safe pure nothrow 425 unittest 426 { 427 import mir.math.sum: Summation; 428 import mir.ndslice.slice: sliced; 429 import mir.test: shouldApprox; 430 431 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x; 432 x.put([0.0, 1, 2, 3, 4].sliced); 433 x.wmean.shouldApprox == 2; 434 x.put(5); 435 x.wmean.shouldApprox == 2.5; 436 } 437 438 // dynamic array test, assume no weights 439 version(mir_stat_test) 440 @safe pure nothrow 441 unittest 442 { 443 import mir.math.sum: Summation; 444 import mir.ndslice.slice: sliced; 445 import mir.test: should; 446 447 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x; 448 x.put([0.0, 1, 2, 3, 4]); 449 x.wmean.should == 2; 450 } 451 452 // static array test, assume no weights 453 version(mir_stat_test) 454 @safe pure nothrow @nogc 455 unittest 456 { 457 import mir.math.sum: Summation; 458 import mir.test: should; 459 460 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x; 461 static immutable y = [0.0, 1, 2, 3, 4]; 462 x.put(y); 463 x.wmean.should == 2; 464 } 465 466 // Adding WMeanAccmulators 467 version(mir_stat_test) 468 @safe pure nothrow 469 unittest 470 { 471 import mir.math.sum: Summation; 472 import mir.test: shouldApprox; 473 474 double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25]; 475 double[] y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]; 476 477 WMeanAccumulator!(float, Summation.pairwise, AssumeWeights.primary) m0; 478 m0.put(x); 479 WMeanAccumulator!(float, Summation.pairwise, AssumeWeights.primary) m1; 480 m1.put(y); 481 m0.put(m1); 482 m0.wmean.shouldApprox == 29.25 / 12; 483 } 484 485 // repeat test, assume weights sum to 1 486 version(mir_stat_test) 487 @safe pure nothrow 488 unittest 489 { 490 import mir.math.sum: Summation; 491 import mir.ndslice.slice: slicedField; 492 import mir.ndslice.topology: iota, map, repeat; 493 import mir.test: shouldApprox; 494 495 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x; 496 auto y = iota(6); 497 auto w = repeat(1.0, 6).map!(a => a / 6.0).slicedField; 498 x.put(y, w); 499 x.wmean.shouldApprox == 15.0 / 6; 500 } 501 502 // repeat test, do not assume weights sum to 1 503 version(mir_stat_test) 504 @safe pure nothrow 505 unittest 506 { 507 import mir.math.sum: Summation; 508 import mir.ndslice.slice: slicedField; 509 import mir.ndslice.topology: iota, repeat; 510 import mir.test: shouldApprox; 511 512 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x; 513 auto y = iota(6); 514 auto w = repeat(1.0, 6).slicedField; 515 x.put(y, w); 516 x.wmean.shouldApprox == 15.0 / 6; 517 } 518 519 // range test without shape, assume weights sum to 1 520 version(mir_stat_test) 521 @safe pure nothrow 522 unittest 523 { 524 import mir.math.sum: Summation; 525 import std.algorithm: map; 526 import std.range: iota; 527 import mir.test: shouldApprox; 528 529 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.sumToOne) x; 530 auto y = iota(6); 531 auto w = iota(1, 7).map!(a => a / 21.0); 532 x.put(y, w); 533 x.wmean.shouldApprox == 70.0 / 21; 534 } 535 536 // range test without shape, do not assume weights sum to 1 537 version(mir_stat_test) 538 @safe pure nothrow 539 unittest 540 { 541 import mir.math.sum: Summation; 542 import mir.test: shouldApprox; 543 import std.range: iota; 544 545 WMeanAccumulator!(double, Summation.pairwise, AssumeWeights.primary) x; 546 auto y = iota(6); 547 auto w = iota(1, 7); 548 x.put(y, w); 549 x.wmean.shouldApprox == 70.0 / 21; 550 } 551 552 // complex test, do not assume weights sum to 1 553 version(mir_stat_test) 554 @safe pure nothrow 555 unittest 556 { 557 import mir.complex; 558 import mir.math.sum: Summation; 559 import mir.test: should; 560 alias C = Complex!double; 561 562 WMeanAccumulator!(C, Summation.pairwise, AssumeWeights.primary, double) x; 563 x.put([C(1, 3), C(2), C(3)]); 564 x.wmean.should == C(2, 1); 565 } 566 567 /++ 568 Computes the weighted mean of the input. 569 570 By default, if `F` is not floating point type or complex type, then the result 571 will have a `double` type if `F` is implicitly convertible to a floating point 572 type or a type for which `isComplex!F` is true. 573 574 Params: 575 F = controls type of output 576 summation = algorithm for calculating sums (default: Summation.appropriate) 577 assumeWeights = true if weights are assumed to add to 1 (default = AssumeWeights.primary) 578 G = controls the type of weights 579 Returns: 580 The weighted mean of all the elements in the input, must be floating point or complex type 581 582 See_also: 583 $(MATHREF sum, Summation), 584 $(SUB2REF univariate, mean), 585 $(SUB2REF univariate, meanType) 586 +/ 587 template wmean(F, Summation summation = Summation.appropriate, 588 AssumeWeights assumeWeights = AssumeWeights.primary, 589 G = F, Summation weightsSummation = Summation.appropriate) 590 if (!is(F : AssumeWeights)) 591 { 592 import mir.math.common: fmamath; 593 import mir.ndslice.slice: isConvertibleToSlice; 594 import mir.stat.descriptive.univariate: meanType; 595 import std.traits: isIterable; 596 597 /++ 598 Params: 599 s = slice-like 600 w = weights 601 +/ 602 @fmamath meanType!F wmean(SliceA, SliceB)(SliceA s, SliceB w) 603 if (isConvertibleToSlice!SliceA && isConvertibleToSlice!SliceB) 604 { 605 import core.lifetime: move; 606 607 alias H = typeof(return); 608 WMeanAccumulator!(H, ResolveSummationType!(summation, SliceA, H), assumeWeights, 609 G, ResolveSummationType!(weightsSummation, SliceB, G)) wmean; 610 wmean.put(s.move, w.move); 611 return wmean.wmean; 612 } 613 614 /++ 615 Params: 616 r = range, must be finite iterable 617 +/ 618 @fmamath meanType!F wmean(Range)(Range r) 619 if (isIterable!Range) 620 { 621 import core.lifetime: move; 622 623 alias H = typeof(return); 624 WMeanAccumulator!(H, ResolveSummationType!(summation, Range, H), assumeWeights, G, ResolveSummationType!(weightsSummation, Range, G)) wmean; 625 wmean.put(r.move); 626 return wmean.wmean; 627 } 628 } 629 630 /// ditto 631 template wmean(Summation summation = Summation.appropriate, 632 AssumeWeights assumeWeights = AssumeWeights.primary, 633 Summation weightsSummation = Summation.appropriate) 634 { 635 import mir.math.common: fmamath; 636 import mir.ndslice.slice: isConvertibleToSlice; 637 import mir.stat.descriptive.univariate: meanType; 638 import std.traits: isIterable; 639 640 /++ 641 Params: 642 s = slice-like 643 w = weights 644 +/ 645 @fmamath meanType!SliceA wmean(SliceA, SliceB)(SliceA s, SliceB w) 646 if (isConvertibleToSlice!SliceA && isConvertibleToSlice!SliceB) 647 { 648 import core.lifetime: move; 649 import mir.math.sum: sumType; 650 651 alias F = typeof(return); 652 return .wmean!(F, summation, assumeWeights, sumType!SliceB, weightsSummation)(s.move, w.move); 653 } 654 655 /++ 656 Params: 657 r = range, must be finite iterable 658 +/ 659 @fmamath meanType!Range wmean(Range)(Range r) 660 if (isIterable!Range) 661 { 662 import core.lifetime: move; 663 664 alias F = typeof(return); 665 return .wmean!(F, summation, assumeWeights, F, weightsSummation)(r.move); 666 } 667 } 668 669 /// ditto 670 template wmean(F, AssumeWeights assumeWeights, Summation summation = Summation.appropriate, 671 G = F, Summation weightsSummation = Summation.appropriate) 672 if (!is(F : AssumeWeights)) 673 { 674 import mir.math.common: fmamath; 675 import mir.ndslice.slice: isConvertibleToSlice; 676 import mir.stat.descriptive.univariate: meanType; 677 import std.traits: isIterable; 678 679 /++ 680 Params: 681 s = slice-like 682 w = weights 683 +/ 684 @fmamath meanType!F wmean(SliceA, SliceB)(SliceA s, SliceB w) 685 if (isConvertibleToSlice!SliceA && isConvertibleToSlice!SliceB) 686 { 687 import core.lifetime: move; 688 import mir.math.sum: sumType; 689 690 alias H = typeof(return); 691 return .wmean!(H, summation, assumeWeights, G, weightsSummation)(s.move, w.move); 692 } 693 694 /++ 695 Params: 696 r = range, must be finite iterable 697 +/ 698 @fmamath meanType!Range wmean(Range)(Range r) 699 if (isIterable!Range) 700 { 701 import core.lifetime: move; 702 703 alias F = typeof(return); 704 return .wmean!(F, summation, assumeWeights, G, weightsSummation)(r.move); 705 } 706 } 707 708 /// ditto 709 template wmean(F, bool assumeWeights, string summation = "appropriate", 710 G = F, string weightsSummation = "appropriate") 711 if (!is(F : AssumeWeights)) 712 { 713 mixin("alias wmean = .wmean!(F, Summation." ~ summation ~ ", cast(AssumeWeights) assumeWeights, G, Summation." ~ weightsSummation ~ ");"); 714 } 715 716 /// ditto 717 template wmean(bool assumeWeights, string summation = "appropriate", 718 string weightsSummation = "appropriate") 719 { 720 mixin("alias wmean = .wmean!(Summation." ~ summation ~ ", cast(AssumeWeights) assumeWeights, Summation." ~ weightsSummation ~ ");"); 721 } 722 723 /// ditto 724 template wmean(F, string summation, bool assumeWeights = false, 725 G = F, string weightsSummation = "appropriate") 726 if (!is(F : AssumeWeights)) 727 { 728 mixin("alias wmean = .wmean!(F, Summation." ~ summation ~ ", cast(AssumeWeights) assumeWeights, G, Summation." ~ weightsSummation ~ ");"); 729 } 730 731 /// ditto 732 template wmean(string summation, bool assumeWeights = false, 733 string weightsSummation = "appropriate") 734 { 735 mixin("alias wmean = .wmean!(Summation." ~ summation ~ ", cast(AssumeWeights) assumeWeights, Summation." ~ weightsSummation ~ ");"); 736 } 737 738 /// ditto 739 template wmean(F, string summation, G, string weightsSummation, bool assumeWeights) 740 if (!is(F : AssumeWeights)) 741 { 742 mixin("alias wmean = .wmean!(F, Summation." ~ summation ~ ", cast(AssumeWeights) assumeWeights, G, Summation." ~ weightsSummation ~ ");"); 743 } 744 745 /// ditto 746 template wmean(string summation, string weightsSummation, bool assumeWeights = false) 747 { 748 mixin("alias wmean = .wmean!(Summation." ~ summation ~ ", cast(AssumeWeights) assumeWeights, Summation." ~ weightsSummation ~ ");"); 749 } 750 751 /// 752 version(mir_stat_test) 753 @safe pure nothrow 754 unittest 755 { 756 import mir.complex; 757 import mir.ndslice.slice: sliced; 758 import mir.test: should, shouldApprox; 759 alias C = Complex!double; 760 761 wmean([1.0, 2, 3], [1, 2, 3]).shouldApprox == (1.0 + 4.0 + 9.0) / 6; 762 wmean!true([1.0, 2, 3], [1.0 / 6, 2.0 / 6, 3.0 / 6]).shouldApprox == (1.0 + 4.0 + 9.0) / 6; 763 wmean([C(1, 3), C(2), C(3)], [1, 2, 3]).should == C(14.0 / 6, 3.0 / 6); 764 765 wmean!float([0, 1, 2, 3, 4, 5].sliced(3, 2), [1, 2, 3, 4, 5, 6].sliced(3, 2)).shouldApprox == 70.0 / 21; 766 767 static assert(is(typeof(wmean!float([1, 2, 3], [1, 2, 3])) == float)); 768 } 769 770 /// If weights are not provided, then behaves like mean 771 version(mir_stat_test) 772 @safe pure nothrow 773 unittest 774 { 775 import mir.complex; 776 import mir.ndslice.slice: sliced; 777 import mir.test: should; 778 alias C = Complex!double; 779 780 wmean([1.0, 2, 3]).should == 2; 781 wmean([C(1, 3), C(2), C(3)]).should == C(2, 1); 782 783 wmean!float([0, 1, 2, 3, 4, 5].sliced(3, 2)).should == 2.5; 784 785 static assert(is(typeof(wmean!float([1, 2, 3])) == float)); 786 } 787 788 /// Weighted mean of vector 789 version(mir_stat_test) 790 @safe pure nothrow 791 unittest 792 { 793 import mir.ndslice.slice: sliced; 794 import mir.ndslice.topology: iota, map; 795 import mir.test: shouldApprox; 796 797 auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 798 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 799 auto w = iota([12], 1); 800 auto w_SumToOne = w.map!(a => a / 78.0); 801 802 x.wmean.shouldApprox == 29.25 / 12; 803 x.wmean(w).shouldApprox == 203.0 / 78; 804 x.wmean!true(w_SumToOne).shouldApprox == 203.0 / 78; 805 } 806 807 /// Weighted mean of matrix 808 version(mir_stat_test) 809 @safe pure 810 unittest 811 { 812 import mir.ndslice.fuse: fuse; 813 import mir.ndslice.topology: iota, map; 814 import mir.test: shouldApprox; 815 816 auto x = [ 817 [0.0, 1.0, 1.5, 2.0, 3.5, 4.25], 818 [2.0, 7.5, 5.0, 1.0, 1.5, 0.0] 819 ].fuse; 820 auto w = iota([2, 6], 1); 821 auto w_SumToOne = w.map!(a => a / 78.0); 822 823 x.wmean.shouldApprox == 29.25 / 12; 824 x.wmean(w).shouldApprox == 203.0 / 78; 825 x.wmean!true(w_SumToOne).shouldApprox == 203.0 / 78; 826 } 827 828 /// Column mean of matrix 829 version(mir_stat_test) 830 @safe pure 831 unittest 832 { 833 import mir.algorithm.iteration: all; 834 import mir.math.common: approxEqual; 835 import mir.ndslice.fuse: fuse; 836 import mir.ndslice.topology: alongDim, byDim, iota, map, universal; 837 838 auto x = [ 839 [0.0, 1.0, 1.5, 2.0, 3.5, 4.25], 840 [2.0, 7.5, 5.0, 1.0, 1.5, 0.0] 841 ].fuse; 842 auto w = iota([2], 1).universal; 843 auto result = [4.0 / 3, 16.0 / 3, 11.5 / 3, 4.0 / 3, 6.5 / 3, 4.25 / 3]; 844 845 // Use byDim or alongDim with map to compute mean of row/column. 846 assert(x.byDim!1.map!(a => a.wmean(w)).all!approxEqual(result)); 847 assert(x.alongDim!0.map!(a => a.wmean(w)).all!approxEqual(result)); 848 849 // FIXME 850 // Without using map, computes the mean of the whole slice 851 // assert(x.byDim!1.wmean(w) == x.sliced.wmean); 852 // assert(x.alongDim!0.wmean(w) == x.sliced.wmean); 853 } 854 855 /// Can also set algorithm or output type 856 version(mir_stat_test) 857 @safe pure nothrow 858 unittest 859 { 860 import mir.ndslice.slice: sliced; 861 import mir.ndslice.topology: repeat, universal; 862 import mir.test: shouldApprox; 863 864 //Set sum algorithm (also for weights) or output type 865 866 auto a = [1, 1e100, 1, -1e100].sliced; 867 868 auto x = a * 10_000; 869 auto w1 = [1, 1, 1, 1].sliced; 870 auto w2 = [0.25, 0.25, 0.25, 0.25].sliced; 871 872 x.wmean!"kbn"(w1).shouldApprox == 20_000 / 4; 873 x.wmean!(true, "kbn")(w2).shouldApprox == 20_000 / 4; 874 x.wmean!("kbn", true)(w2).shouldApprox == 20_000 / 4; 875 x.wmean!("kbn", true, "pairwise")(w2).shouldApprox == 20_000 / 4; 876 x.wmean!(true, "kbn", "pairwise")(w2).shouldApprox == 20_000 / 4; 877 x.wmean!"kb2"(w1).shouldApprox == 20_000 / 4; 878 x.wmean!"precise"(w1).shouldApprox == 20_000 / 4; 879 x.wmean!(double, "precise")(w1).shouldApprox == 20_000.0 / 4; 880 881 auto y = uint.max.repeat(3); 882 y.wmean!ulong([1, 1, 1].sliced.universal).shouldApprox == 12884901885 / 3; 883 } 884 885 /++ 886 For integral slices, can pass output type as template parameter to ensure output 887 type is correct. 888 +/ 889 version(mir_stat_test) 890 @safe pure nothrow 891 unittest 892 { 893 import mir.math.common: approxEqual; 894 import mir.ndslice.slice: sliced; 895 import mir.test: shouldApprox; 896 897 auto x = [0, 1, 1, 2, 4, 4, 898 2, 7, 5, 1, 2, 0].sliced; 899 auto w = [1, 2, 3, 4, 5, 6, 900 7, 8, 9, 10, 11, 12].sliced; 901 902 auto y = x.wmean(w); 903 y.shouldApprox(1.0e-10) == 204.0 / 78; 904 static assert(is(typeof(y) == double)); 905 906 x.wmean!float(w).shouldApprox(1.0e-10) == 204f / 78; 907 } 908 909 /++ 910 Mean works for complex numbers and other user-defined types (provided they 911 can be converted to a floating point or complex type) 912 +/ 913 version(mir_test_weighted) 914 @safe pure nothrow 915 unittest 916 { 917 import mir.complex; 918 import mir.ndslice.slice: sliced; 919 import mir.test: should; 920 alias C = Complex!double; 921 922 auto x = [C(1.0, 2), C(2, 3), C(3, 4), C(4, 5)].sliced; 923 auto w = [1, 2, 3, 4].sliced; 924 x.wmean(w).should == C(3, 4); 925 } 926 927 /// Compute weighted mean tensors along specified dimention of tensors 928 version(mir_stat_test) 929 @safe pure 930 unittest 931 { 932 import mir.ndslice.fuse: fuse; 933 import mir.ndslice.slice: sliced; 934 import mir.ndslice.topology: alongDim, as, iota, map, universal; 935 /++ 936 [[0,1,2], 937 [3,4,5]] 938 +/ 939 auto x = [ 940 [0, 1, 2], 941 [3, 4, 5] 942 ].fuse.as!double; 943 auto w = [ 944 [1, 2, 3], 945 [4, 5, 6] 946 ].fuse; 947 auto w1 = [1, 2].sliced.universal; 948 auto w2 = [1, 2, 3].sliced; 949 950 assert(x.wmean(w) == (70.0 / 21)); 951 952 auto m0 = [(0.0 + 6.0) / 3, (1.0 + 8.0) / 3, (2.0 + 10.0) / 3]; 953 assert(x.alongDim!0.map!(a => a.wmean(w1)) == m0); 954 assert(x.alongDim!(-2).map!(a => a.wmean(w1)) == m0); 955 956 auto m1 = [(0.0 + 2.0 + 6.0) / 6, (3.0 + 8.0 + 15.0) / 6]; 957 assert(x.alongDim!1.map!(a => a.wmean(w2)) == m1); 958 assert(x.alongDim!(-1).map!(a => a.wmean(w2)) == m1); 959 960 assert(iota(2, 3, 4, 5).as!double.alongDim!0.map!wmean == iota([3, 4, 5], 3 * 4 * 5 / 2)); 961 } 962 963 // test chaining 964 version(mir_stat_test) 965 @safe pure nothrow 966 unittest 967 { 968 import mir.test: should; 969 [1.0, 2, 3, 4].wmean.should == 2.5; 970 } 971 972 // additional alongDim tests 973 version(mir_stat_test) 974 @safe pure nothrow 975 unittest 976 { 977 import mir.algorithm.iteration: all; 978 import mir.stat.descriptive.univariate: meanType; 979 import mir.ndslice.topology: iota, alongDim, map; 980 981 auto x = iota([2, 2], 1); 982 auto w = iota([2], 2); 983 auto y = x.alongDim!1.map!(a => a.wmean(w)); 984 static assert(is(meanType!(typeof(y)) == double)); 985 } 986 987 // @nogc test 988 version(mir_stat_test) 989 @safe pure nothrow @nogc 990 unittest 991 { 992 import mir.ndslice.slice: sliced; 993 import mir.test: shouldApprox; 994 995 static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 996 2.0, 7.5, 5.0, 1.0, 1.5, 0.0]; 997 static immutable w = [1.0, 2, 3, 4, 5, 6, 998 7, 8, 9, 10, 11, 12]; 999 1000 x.wmean.shouldApprox == 29.25 / 12; 1001 x.wmean(w).shouldApprox == 203.0 / 78; 1002 } 1003 1004 /++ 1005 Output range for wsum. 1006 +/ 1007 struct WSummator(T, Summation summation, U = T) 1008 { 1009 import mir.ndslice.slice: isConvertibleToSlice, isSlice, kindOf; 1010 import std.range.primitives: isInputRange; 1011 import std.traits: isIterable; 1012 1013 /// 1014 Summator!(T, summation) wsummator; 1015 1016 /// 1017 F wsum(F = T)() const @safe @property pure nothrow @nogc 1018 { 1019 return cast(F) wsummator.sum; 1020 } 1021 1022 /// 1023 void put(Slice1, Slice2)(Slice1 s, Slice2 w) 1024 if (isSlice!Slice1 && isSlice!Slice2) 1025 { 1026 static assert (Slice1.N == Slice2.N, "s and w must have the same number of dimensions"); 1027 static assert (kindOf!Slice1 == kindOf!Slice2, "s and w must have the same kind"); 1028 1029 import mir.ndslice.slice: Contiguous; 1030 import mir.ndslice.topology: zip, map; 1031 1032 assert(s._lengths == w._lengths, "WMeanAcumulator.put: both slices must have the same lengths"); 1033 1034 static if (kindOf!Slice1 != Contiguous && Slice1.N > 1) { 1035 assert(s.strides == w.strides, "WMeanAccumulator.put: cannot put canonical and universal slices when strides do not match"); 1036 auto combine = s.zip!true(w); 1037 } else { 1038 auto combine = s.zip!false(w); 1039 } 1040 1041 auto combine2 = combine.map!"a * b"; 1042 wsummator.put(combine2); 1043 } 1044 1045 /// 1046 void put(SliceLike1, SliceLike2)(SliceLike1 s, SliceLike2 w) 1047 if (isConvertibleToSlice!SliceLike1 && !isSlice!SliceLike1 && 1048 isConvertibleToSlice!SliceLike2 && !isSlice!SliceLike2) 1049 { 1050 import mir.ndslice.slice: toSlice; 1051 this.put(s.toSlice, w.toSlice); 1052 } 1053 1054 /// 1055 void put(Range)(Range r) 1056 if (isIterable!Range) 1057 { 1058 wsummator.put(r); 1059 } 1060 1061 /// 1062 void put(RangeA, RangeB)(RangeA r, RangeB w) 1063 if (isInputRange!RangeA && !isConvertibleToSlice!RangeA && 1064 isInputRange!RangeB && !isConvertibleToSlice!RangeB) 1065 { 1066 do 1067 { 1068 assert(!(!r.empty && w.empty) && !(r.empty && !w.empty), 1069 "r and w must both be empty at the same time, one cannot be empty while the other has remaining items"); 1070 this.put(r.front, w.front); 1071 r.popFront; 1072 w.popFront; 1073 } while(!r.empty || !w.empty); // Using an || instead of && so that the loop does not end early. mis-matched lengths of r and w sould be caught by above assert 1074 } 1075 1076 /// 1077 void put()(T x, U w) 1078 { 1079 wsummator.put(x * w); 1080 } 1081 1082 /// 1083 void put()(T x) 1084 { 1085 wsummator.put(x); 1086 } 1087 1088 /// 1089 void put(F = T, G = U)(WSummator!(F, summation, G) wm) 1090 { 1091 wsummator.put(cast(T) wm.wsummator); 1092 } 1093 } 1094 1095 /// 1096 version(mir_stat_test) 1097 @safe pure nothrow 1098 unittest 1099 { 1100 import mir.math.sum: Summation; 1101 import mir.ndslice.slice: sliced; 1102 import mir.test: should; 1103 1104 WSummator!(double, Summation.pairwise) x; 1105 x.put([0.0, 1, 2, 3, 4].sliced, [1, 2, 3, 4, 5].sliced); 1106 x.wsum.should == 40; 1107 x.put(5, 6); 1108 x.wsum.should == 70; 1109 } 1110 1111 // dynamic array test 1112 version(mir_stat_test) 1113 @safe pure nothrow 1114 unittest 1115 { 1116 import mir.math.sum: Summation; 1117 import mir.test: should; 1118 1119 WSummator!(double, Summation.pairwise) x; 1120 x.put([0.0, 1, 2, 3, 4], [1, 2, 3, 4, 5]); 1121 x.wsum.should == 40; 1122 } 1123 1124 // static array test 1125 version(mir_stat_test) 1126 @safe pure nothrow @nogc 1127 unittest 1128 { 1129 import mir.math.sum: Summation; 1130 import mir.test: should; 1131 1132 WSummator!(double, Summation.pairwise) x; 1133 static immutable y = [0.0, 1, 2, 3, 4]; 1134 static immutable w = [1, 2, 3, 4, 5]; 1135 x.put(y, w); 1136 x.wsum.should == 40; 1137 } 1138 1139 // 2-d slice test 1140 version(mir_stat_test) 1141 @safe pure 1142 unittest 1143 { 1144 import mir.math.sum: Summation; 1145 import mir.ndslice.fuse: fuse; 1146 import mir.test: should; 1147 1148 WSummator!(double, Summation.pairwise) x; 1149 auto y = [ 1150 [0.0, 1, 2], 1151 [3.0, 4, 5] 1152 ].fuse; 1153 auto w = [ 1154 [1.0, 2, 3], 1155 [4.0, 5, 6] 1156 ].fuse; 1157 x.put(y, w); 1158 x.wsum.should == 70; 1159 } 1160 1161 // universal slice test 1162 version(mir_stat_test) 1163 @safe pure nothrow 1164 unittest 1165 { 1166 import mir.math.sum: Summation; 1167 import mir.ndslice.topology: iota, universal; 1168 import mir.test: should; 1169 1170 WSummator!(double, Summation.pairwise) x; 1171 auto y = iota(6).universal; 1172 auto w = iota([6], 1).universal; 1173 x.put(y, w); 1174 x.wsum.should == 70; 1175 } 1176 1177 // canonical slice test 1178 version(mir_stat_test) 1179 @safe pure nothrow 1180 unittest 1181 { 1182 import mir.math.sum: Summation; 1183 import mir.ndslice.topology: canonical, iota; 1184 import mir.test: should; 1185 1186 WSummator!(double, Summation.pairwise) x; 1187 auto y = iota(6).canonical; 1188 auto w = iota([6], 1).canonical; 1189 x.put(y, w); 1190 x.wsum.should == 70; 1191 } 1192 1193 // 2-d universal slice test 1194 version(mir_stat_test) 1195 @safe pure nothrow 1196 unittest 1197 { 1198 import mir.math.sum: Summation; 1199 import mir.ndslice.topology: iota, universal; 1200 import mir.test: should; 1201 1202 WSummator!(double, Summation.pairwise) x; 1203 auto y = iota([2, 3]).universal; 1204 auto w = iota([2, 3], 1).universal; 1205 x.put(y, w); 1206 x.wsum.should == 70; 1207 } 1208 1209 // 2-d canonical slice test 1210 version(mir_stat_test) 1211 @safe pure nothrow 1212 unittest 1213 { 1214 import mir.math.sum: Summation; 1215 import mir.ndslice.topology: canonical, iota; 1216 import mir.test: should; 1217 1218 WSummator!(double, Summation.pairwise) x; 1219 auto y = iota([2, 3]).canonical; 1220 auto w = iota([2, 3], 1).canonical; 1221 x.put(y, w); 1222 x.wsum.should == 70; 1223 } 1224 1225 /// Assume no weights, like Summator 1226 version(mir_stat_test) 1227 @safe pure nothrow 1228 unittest 1229 { 1230 import mir.math.sum: Summation; 1231 import mir.ndslice.slice: sliced; 1232 import mir.test: should; 1233 1234 WSummator!(double, Summation.pairwise) x; 1235 x.put([0.0, 1, 2, 3, 4].sliced); 1236 x.wsum.should == 10; 1237 x.put(5); 1238 x.wsum.should == 15; 1239 } 1240 1241 // dynamic array test, assume no weights 1242 version(mir_stat_test) 1243 @safe pure nothrow 1244 unittest 1245 { 1246 import mir.math.sum: Summation; 1247 import mir.test: should; 1248 1249 WSummator!(double, Summation.pairwise) x; 1250 x.put([0.0, 1, 2, 3, 4]); 1251 x.wsum.should == 10; 1252 } 1253 1254 // static array test, assume no weights 1255 version(mir_stat_test) 1256 @safe pure nothrow @nogc 1257 unittest 1258 { 1259 import mir.math.sum: Summation; 1260 import mir.test: should; 1261 1262 WSummator!(double, Summation.pairwise) x; 1263 static immutable y = [0.0, 1, 2, 3, 4]; 1264 x.put(y); 1265 x.wsum.should == 10; 1266 } 1267 1268 // Adding WSummators 1269 version(mir_stat_test) 1270 @safe pure nothrow 1271 unittest 1272 { 1273 import mir.math.sum: Summation; 1274 import mir.test: should; 1275 1276 double[] x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25]; 1277 double[] y = [2.0, 7.5, 5.0, 1.0, 1.5, 0.0]; 1278 1279 WSummator!(float, Summation.pairwise) m0; 1280 m0.put(x); 1281 WSummator!(float, Summation.pairwise) m1; 1282 m1.put(y); 1283 m0.put(m1); 1284 m0.wsum.should == 29.25; 1285 } 1286 1287 // repeat test 1288 version(mir_stat_test) 1289 @safe pure nothrow 1290 unittest 1291 { 1292 import mir.math.sum: Summation; 1293 import mir.ndslice.slice: slicedField; 1294 import mir.ndslice.topology: iota, repeat; 1295 import mir.test: should; 1296 1297 WSummator!(double, Summation.pairwise) x; 1298 auto y = iota(6); 1299 auto w = repeat(1.0, 6).slicedField; 1300 x.put(y, w); 1301 x.wsum.should == 15; 1302 } 1303 1304 // range test without shape 1305 version(mir_stat_test) 1306 @safe pure nothrow 1307 unittest 1308 { 1309 import mir.math.sum: Summation; 1310 import mir.test: should; 1311 import std.range: iota; 1312 1313 WSummator!(double, Summation.pairwise) x; 1314 auto y = iota(6); 1315 auto w = iota(1, 7); 1316 x.put(y, w); 1317 x.wsum.should == 70; 1318 } 1319 1320 // complex test 1321 version(mir_stat_test) 1322 @safe pure nothrow 1323 unittest 1324 { 1325 import mir.math.sum: Summation; 1326 import mir.complex; 1327 import mir.test: should; 1328 alias C = Complex!double; 1329 1330 WSummator!(C, Summation.pairwise, double) x; 1331 x.put([C(1, 3), C(2), C(3)]); 1332 x.wsum.should == C(6, 3); 1333 } 1334 1335 /++ 1336 Computes the weighted sum of the input. 1337 1338 Params: 1339 F = controls type of output 1340 summation = algorithm for calculating sums (default: Summation.appropriate) 1341 G = controls the type of weights 1342 Returns: 1343 The weighted sum of all the elements in the input 1344 1345 See_also: 1346 $(MATHREF sum, Summation) 1347 +/ 1348 template wsum(F, Summation summation = Summation.appropriate, G = F) 1349 { 1350 import mir.math.common: fmamath; 1351 import mir.math.sum: sumType; 1352 import mir.ndslice.slice: isConvertibleToSlice; 1353 import std.traits: isIterable; 1354 1355 /++ 1356 Params: 1357 s = slice-like 1358 w = weights 1359 +/ 1360 @fmamath sumType!F wsum(SliceA, SliceB)(SliceA s, SliceB w) 1361 if (isConvertibleToSlice!SliceA && isConvertibleToSlice!SliceB) 1362 { 1363 import core.lifetime: move; 1364 1365 alias H = typeof(return); 1366 WSummator!(H, ResolveSummationType!(summation, SliceA, H), G) wsum; 1367 wsum.put(s.move, w.move); 1368 return wsum.wsum; 1369 } 1370 1371 /++ 1372 Params: 1373 r = range, must be finite iterable 1374 +/ 1375 @fmamath sumType!F wsum(Range)(Range r) 1376 if (isIterable!Range) 1377 { 1378 import core.lifetime: move; 1379 1380 alias H = typeof(return); 1381 WSummator!(H, ResolveSummationType!(summation, Range, H), G) wsum; 1382 wsum.put(r.move); 1383 return wsum.wsum; 1384 } 1385 } 1386 1387 /// ditto 1388 template wsum(Summation summation = Summation.appropriate) 1389 { 1390 import mir.math.common: fmamath; 1391 import mir.math.sum: sumType; 1392 import mir.ndslice.slice: isConvertibleToSlice; 1393 import std.traits: isIterable; 1394 1395 /++ 1396 Params: 1397 s = slice-like 1398 w = weights 1399 +/ 1400 @fmamath sumType!SliceA wsum(SliceA, SliceB)(SliceA s, SliceB w) 1401 if (isConvertibleToSlice!SliceA && isConvertibleToSlice!SliceB) 1402 { 1403 import core.lifetime: move; 1404 import mir.math.sum: sumType; 1405 1406 alias F = typeof(return); 1407 return .wsum!(F, summation, sumType!SliceB)(s.move, w.move); 1408 } 1409 1410 /++ 1411 Params: 1412 r = range, must be finite iterable 1413 +/ 1414 @fmamath sumType!Range wsum(Range)(Range r) 1415 if (isIterable!Range) 1416 { 1417 import core.lifetime: move; 1418 1419 alias F = typeof(return); 1420 return .wsum!(F, summation, F)(r.move); 1421 } 1422 } 1423 1424 /// ditto 1425 template wsum(F, string summation, G = F) 1426 { 1427 mixin("alias wsum = .wsum!(F, Summation." ~ summation ~ ", G);"); 1428 } 1429 1430 /// ditto 1431 template wsum(string summation) 1432 { 1433 mixin("alias wsum = .wsum!(Summation." ~ summation ~ ");"); 1434 } 1435 1436 /// 1437 version(mir_stat_test) 1438 @safe pure nothrow 1439 unittest 1440 { 1441 import mir.complex; 1442 import mir.math.common: approxEqual; 1443 import mir.ndslice.slice: sliced; 1444 import mir.test: should; 1445 alias C = Complex!double; 1446 1447 wsum([1, 2, 3], [1, 2, 3]).should == (1 + 4 + 9); 1448 wsum([C(1, 3), C(2), C(3)], [1, 2, 3]).should == C((1 + 4 + 9), 3); 1449 1450 wsum!float([0, 1, 2, 3, 4, 5].sliced(3, 2), [1, 2, 3, 4, 5, 6].sliced(3, 2)).should == 70; 1451 1452 static assert(is(typeof(wmean!float([1, 2, 3], [1, 2, 3])) == float)); 1453 } 1454 1455 /// If weights are not provided, then behaves like sum 1456 version(mir_stat_test) 1457 @safe pure nothrow 1458 unittest 1459 { 1460 import mir.complex; 1461 import mir.ndslice.slice: sliced; 1462 import mir.test: should; 1463 alias C = Complex!double; 1464 1465 wsum([1.0, 2, 3]).should == 6; 1466 wsum([C(1, 3), C(2), C(3)]).should == C(6, 3); 1467 1468 wsum!float([0, 1, 2, 3, 4, 5].sliced(3, 2)).should == 15; 1469 1470 static assert(is(typeof(wsum!float([1, 2, 3])) == float)); 1471 } 1472 1473 /// Weighted sum of vector 1474 version(mir_stat_test) 1475 @safe pure nothrow 1476 unittest 1477 { 1478 import mir.ndslice.slice: sliced; 1479 import mir.ndslice.topology: iota, map; 1480 import mir.test: should; 1481 1482 auto x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 1483 2.0, 7.5, 5.0, 1.0, 1.5, 0.0].sliced; 1484 auto w = iota([12], 1); 1485 1486 x.wsum.should == 29.25; 1487 x.wsum(w).should == 203; 1488 } 1489 1490 /// Weighted sum of matrix 1491 version(mir_stat_test) 1492 @safe pure 1493 unittest 1494 { 1495 import mir.ndslice.fuse: fuse; 1496 import mir.ndslice.topology: iota; 1497 import mir.test: should; 1498 1499 auto x = [ 1500 [0.0, 1.0, 1.5, 2.0, 3.5, 4.25], 1501 [2.0, 7.5, 5.0, 1.0, 1.5, 0.0] 1502 ].fuse; 1503 auto w = iota([2, 6], 1); 1504 1505 x.wsum.should == 29.25; 1506 x.wsum(w).should == 203; 1507 } 1508 1509 /// Column sum of matrix 1510 version(mir_stat_test) 1511 @safe pure 1512 unittest 1513 { 1514 import mir.algorithm.iteration: all; 1515 import mir.math.common: approxEqual; 1516 import mir.ndslice.fuse: fuse; 1517 import mir.ndslice.topology: alongDim, byDim, iota, map, universal; 1518 1519 auto x = [ 1520 [0.0, 1.0, 1.5, 2.0, 3.5, 4.25], 1521 [2.0, 7.5, 5.0, 1.0, 1.5, 0.0] 1522 ].fuse; 1523 auto w = iota([2], 1).universal; 1524 auto result = [4, 16, 11.5, 4, 6.5, 4.25]; 1525 1526 // Use byDim or alongDim with map to compute sum of row/column. 1527 assert(x.byDim!1.map!(a => a.wsum(w)).all!approxEqual(result)); 1528 assert(x.alongDim!0.map!(a => a.wsum(w)).all!approxEqual(result)); 1529 1530 // FIXME 1531 // Without using map, computes the sum of the whole slice 1532 // assert(x.byDim!1.wsum(w) == x.sliced.wsum); 1533 // assert(x.alongDim!0.wsum(w) == x.sliced.wsum); 1534 } 1535 1536 /// Can also set algorithm or output type 1537 version(mir_stat_test) 1538 @safe pure nothrow 1539 unittest 1540 { 1541 import mir.ndslice.slice: sliced; 1542 import mir.ndslice.topology: repeat, universal; 1543 import mir.test: should; 1544 1545 //Set sum algorithm (also for weights) or output type 1546 1547 auto a = [1, 1e100, 1, -1e100].sliced; 1548 1549 auto x = a * 10_000; 1550 auto w1 = [1, 1, 1, 1].sliced; 1551 auto w2 = [0.25, 0.25, 0.25, 0.25].sliced; 1552 1553 x.wsum!"kbn"(w1).should == 20_000; 1554 x.wsum!"kbn"(w2).should == 20_000 / 4; 1555 x.wsum!"kb2"(w1).should == 20_000; 1556 x.wsum!"precise"(w1).should == 20_000; 1557 x.wsum!(double, "precise")(w1).should == 20_000; 1558 1559 auto y = uint.max.repeat(3); 1560 y.wsum!ulong([1, 1, 1].sliced.universal).should == 12884901885; 1561 } 1562 1563 /++ 1564 wsum works for complex numbers and other user-defined types 1565 +/ 1566 version(mir_test_weighted) 1567 @safe pure nothrow 1568 unittest 1569 { 1570 import mir.complex; 1571 import mir.ndslice.slice: sliced; 1572 import mir.test: should; 1573 alias C = Complex!double; 1574 1575 auto x = [C(1.0, 2), C(2, 3), C(3, 4), C(4, 5)].sliced; 1576 auto w = [1, 2, 3, 4].sliced; 1577 x.wsum(w).should == C(30, 40); 1578 } 1579 1580 /// Compute weighted sum tensors along specified dimention of tensors 1581 version(mir_stat_test) 1582 @safe pure 1583 unittest 1584 { 1585 import mir.ndslice.fuse: fuse; 1586 import mir.ndslice.slice: sliced; 1587 import mir.ndslice.topology: alongDim, as, iota, map, universal; 1588 /++ 1589 [[0,1,2], 1590 [3,4,5]] 1591 +/ 1592 auto x = [ 1593 [0, 1, 2], 1594 [3, 4, 5] 1595 ].fuse.as!double; 1596 auto w = [ 1597 [1, 2, 3], 1598 [4, 5, 6] 1599 ].fuse; 1600 auto w1 = [1, 2].sliced.universal; 1601 auto w2 = [1, 2, 3].sliced; 1602 1603 assert(x.wsum(w) == 70); 1604 1605 auto m0 = [(0 + 6), (1 + 8), (2 + 10)]; 1606 assert(x.alongDim!0.map!(a => a.wsum(w1)) == m0); 1607 assert(x.alongDim!(-2).map!(a => a.wsum(w1)) == m0); 1608 1609 auto m1 = [(0 + 2 + 6), (3 + 8 + 15)]; 1610 assert(x.alongDim!1.map!(a => a.wsum(w2)) == m1); 1611 assert(x.alongDim!(-1).map!(a => a.wsum(w2)) == m1); 1612 } 1613 1614 // test chaining 1615 version(mir_stat_test) 1616 @safe pure nothrow 1617 unittest 1618 { 1619 import mir.test: should; 1620 [1.0, 2, 3, 4].wsum.should == 10; 1621 } 1622 1623 // additional alongDim tests 1624 version(mir_stat_test) 1625 @safe pure nothrow 1626 unittest 1627 { 1628 import mir.math.sum: sumType; 1629 import mir.ndslice.topology: iota, alongDim, map; 1630 1631 auto x = iota([2, 2], 1); 1632 auto w = iota([2], 2); 1633 auto y = x.alongDim!1.map!(a => a.wsum(w)); 1634 static assert(is(sumType!(typeof(y)) == long)); 1635 } 1636 1637 // @nogc test 1638 version(mir_stat_test) 1639 @safe pure nothrow @nogc 1640 unittest 1641 { 1642 import mir.test: should; 1643 1644 static immutable x = [0.0, 1.0, 1.5, 2.0, 3.5, 4.25, 1645 2.0, 7.5, 5.0, 1.0, 1.5, 0.0]; 1646 static immutable w = [1.0, 2, 3, 4, 5, 6, 1647 7, 8, 9, 10, 11, 12]; 1648 1649 x.wsum.should == 29.25; 1650 x.wsum(w).should == 203.0; 1651 }