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