diff --git a/README.md b/README.md index f6c9adf..68034d5 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,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 +141,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 29fc334..7759f6d 100644 --- a/src/index.js +++ b/src/index.js @@ -18,7 +18,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"; @@ -26,7 +26,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 fca6a82..2604df8 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 09ddac7..163343a 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)); @@ -27,3 +31,15 @@ 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) { + 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 b4ed884..e4b6c84 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 b13c234..6537c57 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.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); +}); + + function box(value) { return {value: value}; } diff --git a/test/quantile-test.js b/test/quantile-test.js index a00a07b..adebec1 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,33 @@ it("quantile(array, p, f) observes the specified accessor", () => { assert.strictEqual(quantile([], 1, unbox), 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) { return {value: value}; }