From 142f4e6e2ac7dbeb2e74cc2b00170ffaa9f13319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 25 Jun 2020 12:17:39 +0200 Subject: [PATCH 1/3] quantileIndex, medianIndex closes #140 --- README.md | 26 +++++++++++++++++++------- src/index.js | 4 ++-- src/median.js | 6 +++++- src/quantile.js | 14 ++++++++++++++ test/median-test.js | 12 +++++++++++- test/quantile-test.js | 11 ++++++++++- 6 files changed, 61 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1bcab860..ddb72fa4 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,17 @@ const m = d3.min(array); ## API Reference -* [Statistics](#statistics) -* [Search](#search) -* [Transformations](#transformations) -* [Iterables](#iterables) -* [Sets](#sets) -* [Bins](#bins) -* [Interning](#interning) +- [d3-array](#d3-array) + - [Installing](#installing) + - [API Reference](#api-reference) + - [Statistics](#statistics) + - [Search](#search) + - [Transformations](#transformations) + - [Iterables](#iterables) + - [Sets](#sets) + - [Bins](#bins) + - [Bin Thresholds](#bin-thresholds) + - [Interning](#interning) ### Statistics @@ -117,6 +121,10 @@ Returns the mean of the given *iterable* of numbers. If the iterable contains no Returns the median of the given *iterable* of numbers using the [R-7 method](https://en.wikipedia.org/wiki/Quantile#Estimating_quantiles_from_a_sample). If the iterable contains no numbers, returns undefined. An optional *accessor* function may be specified, which is equivalent to calling Array.from before computing the median. This method ignores undefined and NaN values; this is useful for ignoring missing data. +# d3.medianIndex(array, p[, accessor]) [Source](https://github.com/d3/d3-array/blob/main/src/median.js "Source") + +Similar to *median*, but returns the index of the element to the left of the median. + # d3.cumsum(iterable[, accessor]) · [Source](https://github.com/d3/d3-array/blob/main/src/cumsum.js), [Examples](https://observablehq.com/@d3/d3-cumsum) Returns the cumulative sum of the given *iterable* of numbers, as a Float64Array of the same length. If the iterable contains no numbers, returns zeros. An optional *accessor* function may be specified, which is equivalent to calling Array.from before computing the cumulative sum. This method ignores undefined and NaN values; this is useful for ignoring missing data. @@ -137,6 +145,10 @@ d3.quantile(a, 0.1); // 2 An optional *accessor* function may be specified, which is equivalent to calling *array*.map(*accessor*) before computing the quantile. +# d3.quantileIndex(array, p[, accessor]) [Source](https://github.com/d3/d3-array/blob/main/src/quantile.js "Source") + +Similar to *quantile*, but returns the index to the left of *p*. + # d3.quantileSorted(array, p[, accessor]) · [Source](https://github.com/d3/d3-array/blob/main/src/quantile.js), [Examples](https://observablehq.com/@d3/d3-mean-d3-median-and-friends) Similar to *quantile*, but expects the input to be a **sorted** *array* of values. In contrast with *quantile*, the accessor is only called on the elements needed to compute the quantile. diff --git a/src/index.js b/src/index.js index 5fb58e8b..488b8178 100644 --- a/src/index.js +++ b/src/index.js @@ -17,7 +17,7 @@ export {default as thresholdSturges} from "./threshold/sturges.js"; export {default as max} from "./max.js"; export {default as maxIndex} from "./maxIndex.js"; export {default as mean} from "./mean.js"; -export {default as median} from "./median.js"; +export {default as median, medianIndex} from "./median.js"; export {default as merge} from "./merge.js"; export {default as min} from "./min.js"; export {default as minIndex} from "./minIndex.js"; @@ -25,7 +25,7 @@ export {default as mode} from "./mode.js"; export {default as nice} from "./nice.js"; export {default as pairs} from "./pairs.js"; export {default as permute} from "./permute.js"; -export {default as quantile, quantileSorted} from "./quantile.js"; +export {default as quantile, quantileIndex, quantileSorted} from "./quantile.js"; export {default as quickselect} from "./quickselect.js"; export {default as range} from "./range.js"; export {default as rank} from "./rank.js"; diff --git a/src/median.js b/src/median.js index fca6a829..2604df87 100644 --- a/src/median.js +++ b/src/median.js @@ -1,5 +1,9 @@ -import quantile from "./quantile.js"; +import quantile, {quantileIndex} from "./quantile.js"; export default function median(values, valueof) { return quantile(values, 0.5, valueof); } + +export function medianIndex(values, valueof) { + return quantileIndex(values, 0.5, valueof); +} diff --git a/src/quantile.js b/src/quantile.js index 09ddac7c..8628df26 100644 --- a/src/quantile.js +++ b/src/quantile.js @@ -27,3 +27,17 @@ export function quantileSorted(values, p, valueof = number) { value1 = +valueof(values[i0 + 1], i0 + 1, values); return value0 + (value1 - value0) * (i - i0); } + +export function quantileIndex(values, p, valueof) { + if (valueof) values = Float64Array.from(numbers(values, valueof)); + const q = quantile(values, p); + + let index, v = -Infinity; + for (let i = 0; i < values.length; i++) { + const x = values[i]; + if (x <= q) { + if (x > v) index = i, v = x; + } + } + return index; +} diff --git a/test/median-test.js b/test/median-test.js index b13c2342..900967e7 100644 --- a/test/median-test.js +++ b/test/median-test.js @@ -1,5 +1,5 @@ import assert from "assert"; -import {median} from "../src/index.js"; +import {median, medianIndex} from "../src/index.js"; import {OneTimeNumber} from "./OneTimeNumber.js"; it("median(array) returns the median value for numbers", () => { @@ -98,6 +98,16 @@ it("median(array, f) uses the undefined context", () => { assert.deepStrictEqual(results, [undefined, undefined]); }); +it("medianIndex(array) returns the index", () => { + assert.deepEqual(medianIndex([1, 2]), 0); + assert.deepEqual(medianIndex([1, 2, 3]), 1); + assert.deepEqual(medianIndex([1, 3, 2]), 2); + assert.deepEqual(medianIndex([2, 3, 1]), 0); + assert.deepEqual(medianIndex([1]), 0); + assert.deepEqual(medianIndex([]), undefined); +}); + + function box(value) { return {value: value}; } diff --git a/test/quantile-test.js b/test/quantile-test.js index a00a07b3..eff47002 100644 --- a/test/quantile-test.js +++ b/test/quantile-test.js @@ -1,5 +1,5 @@ import assert from "assert"; -import {quantile, quantileSorted} from "../src/index.js"; +import {quantile, quantileIndex, quantileSorted} from "../src/index.js"; it("quantileSorted(array, p) requires sorted numeric input, quantile doesn't", () => { assert.strictEqual(quantileSorted([1, 2, 3, 4], 0), 1); @@ -83,6 +83,15 @@ it("quantile(array, p, f) observes the specified accessor", () => { assert.strictEqual(quantile([], 1, unbox), undefined); }); +it("quantileIndex(array) returns the index", () => { + assert.deepEqual(quantileIndex([1, 2], 0.2), 0); + assert.deepEqual(quantileIndex([1, 2, 3], 0.2), 0); + assert.deepEqual(quantileIndex([1, 3, 2], 0.2), 0); + assert.deepEqual(quantileIndex([2, 3, 1], 0.2), 2); + assert.deepEqual(quantileIndex([1], 0.2), 0); + assert.deepEqual(quantileIndex([], 0.2), undefined); +}); + function box(value) { return {value: value}; } From 082179c78e87db5e5cd000185598420ba91ef75b Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 3 Jul 2022 08:30:36 -0400 Subject: [PATCH 2/3] revert README edit --- README.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ddb72fa4..ef83ff92 100644 --- a/README.md +++ b/README.md @@ -57,17 +57,13 @@ const m = d3.min(array); ## API Reference -- [d3-array](#d3-array) - - [Installing](#installing) - - [API Reference](#api-reference) - - [Statistics](#statistics) - - [Search](#search) - - [Transformations](#transformations) - - [Iterables](#iterables) - - [Sets](#sets) - - [Bins](#bins) - - [Bin Thresholds](#bin-thresholds) - - [Interning](#interning) +* [Statistics](#statistics) +* [Search](#search) +* [Transformations](#transformations) +* [Iterables](#iterables) +* [Sets](#sets) +* [Bins](#bins) +* [Interning](#interning) ### Statistics From 11fa241aa50049dfe6a9d4cbf0a74837b772ff22 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 3 Jul 2022 08:57:06 -0400 Subject: [PATCH 3/3] rewrite quantileIndex --- src/quantile.js | 24 +++++++++++++----------- src/quickselect.js | 1 + test/median-test.js | 12 ++++++------ test/quantile-test.js | 32 +++++++++++++++++++++++++------- 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/quantile.js b/src/quantile.js index 8628df26..163343ae 100644 --- a/src/quantile.js +++ b/src/quantile.js @@ -1,7 +1,11 @@ import max from "./max.js"; +import maxIndex from "./maxIndex.js"; import min from "./min.js"; +import minIndex from "./minIndex.js"; import quickselect from "./quickselect.js"; import number, {numbers} from "./number.js"; +import {ascendingDefined} from "./sort.js"; +import greatest from "./greatest.js"; export default function quantile(values, p, valueof) { values = Float64Array.from(numbers(values, valueof)); @@ -29,15 +33,13 @@ export function quantileSorted(values, p, valueof = number) { } export function quantileIndex(values, p, valueof) { - if (valueof) values = Float64Array.from(numbers(values, valueof)); - const q = quantile(values, p); - - let index, v = -Infinity; - for (let i = 0; i < values.length; i++) { - const x = values[i]; - if (x <= q) { - if (x > v) index = i, v = x; - } - } - return index; + values = Float64Array.from(numbers(values, valueof)); + if (!(n = values.length)) return; + if ((p = +p) <= 0 || n < 2) return minIndex(values); + if (p >= 1) return maxIndex(values); + var n, + i = Math.floor((n - 1) * p), + order = (i, j) => ascendingDefined(values[i], values[j]), + index = quickselect(Uint32Array.from(values, (_, i) => i), i, 0, n - 1, order); + return greatest(index.subarray(0, i + 1), i => values[i]); } diff --git a/src/quickselect.js b/src/quickselect.js index b4ed8846..e4b6c841 100644 --- a/src/quickselect.js +++ b/src/quickselect.js @@ -36,6 +36,7 @@ export default function quickselect(array, k, left = 0, right = array.length - 1 if (j <= k) left = j + 1; if (k <= j) right = j - 1; } + return array; } diff --git a/test/median-test.js b/test/median-test.js index 900967e7..6537c579 100644 --- a/test/median-test.js +++ b/test/median-test.js @@ -99,12 +99,12 @@ it("median(array, f) uses the undefined context", () => { }); it("medianIndex(array) returns the index", () => { - assert.deepEqual(medianIndex([1, 2]), 0); - assert.deepEqual(medianIndex([1, 2, 3]), 1); - assert.deepEqual(medianIndex([1, 3, 2]), 2); - assert.deepEqual(medianIndex([2, 3, 1]), 0); - assert.deepEqual(medianIndex([1]), 0); - assert.deepEqual(medianIndex([]), undefined); + assert.deepStrictEqual(medianIndex([1, 2]), 0); + assert.deepStrictEqual(medianIndex([1, 2, 3]), 1); + assert.deepStrictEqual(medianIndex([1, 3, 2]), 2); + assert.deepStrictEqual(medianIndex([2, 3, 1]), 0); + assert.deepStrictEqual(medianIndex([1]), 0); + assert.deepStrictEqual(medianIndex([]), undefined); }); diff --git a/test/quantile-test.js b/test/quantile-test.js index eff47002..adebec11 100644 --- a/test/quantile-test.js +++ b/test/quantile-test.js @@ -83,13 +83,31 @@ it("quantile(array, p, f) observes the specified accessor", () => { assert.strictEqual(quantile([], 1, unbox), undefined); }); -it("quantileIndex(array) returns the index", () => { - assert.deepEqual(quantileIndex([1, 2], 0.2), 0); - assert.deepEqual(quantileIndex([1, 2, 3], 0.2), 0); - assert.deepEqual(quantileIndex([1, 3, 2], 0.2), 0); - assert.deepEqual(quantileIndex([2, 3, 1], 0.2), 2); - assert.deepEqual(quantileIndex([1], 0.2), 0); - assert.deepEqual(quantileIndex([], 0.2), undefined); +it("quantileIndex(array, p) returns the index", () => { + assert.deepStrictEqual(quantileIndex([1, 2], 0.2), 0); + assert.deepStrictEqual(quantileIndex([1, 2, 3], 0.2), 0); + assert.deepStrictEqual(quantileIndex([1, 3, 2], 0.2), 0); + assert.deepStrictEqual(quantileIndex([2, 3, 1], 0.2), 2); + assert.deepStrictEqual(quantileIndex([1], 0.2), 0); + assert.deepStrictEqual(quantileIndex([], 0.2), undefined); +}); + +it("quantileIndex(array, 0) returns the minimum index", () => { + assert.deepStrictEqual(quantileIndex([1, 2], 0), 0); + assert.deepStrictEqual(quantileIndex([1, 2, 3], 0), 0); + assert.deepStrictEqual(quantileIndex([1, 3, 2], 0), 0); + assert.deepStrictEqual(quantileIndex([2, 3, 1], 0), 2); + assert.deepStrictEqual(quantileIndex([1], 0), 0); + assert.deepStrictEqual(quantileIndex([], 0), undefined); +}); + +it("quantileIndex(array, 1) returns the maxium index", () => { + assert.deepStrictEqual(quantileIndex([1, 2], 1), 1); + assert.deepStrictEqual(quantileIndex([1, 2, 3], 1), 2); + assert.deepStrictEqual(quantileIndex([1, 3, 2], 1), 1); + assert.deepStrictEqual(quantileIndex([2, 3, 1], 1), 1); + assert.deepStrictEqual(quantileIndex([1], 1), 0); + assert.deepStrictEqual(quantileIndex([], 1), undefined); }); function box(value) {