Skip to content

Commit

Permalink
quantileIndex, medianIndex (#159)
Browse files Browse the repository at this point in the history
* quantileIndex, medianIndex

closes #140

* revert README edit

* rewrite quantileIndex

Co-authored-by: Mike Bostock <mbostock@gmail.com>
  • Loading branch information
Fil and mbostock committed Jul 3, 2022
1 parent d091c4b commit 99fe7ae
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 5 deletions.
8 changes: 8 additions & 0 deletions README.md
Expand Up @@ -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.

<a name="medianIndex" href="#medianIndex">#</a> d3.<b>medianIndex</b>(<i>array</i>, <i>p</i>[, <i>accessor</i>]) [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.

<a name="cumsum" href="#cumsum">#</a> d3.<b>cumsum</b>(<i>iterable</i>[, <i>accessor</i>]) · [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.
Expand All @@ -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.

<a name="quantileIndex" href="#quantileIndex">#</a> d3.<b>quantileIndex</b>(<i>array</i>, <i>p</i>[, <i>accessor</i>]) [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*.

<a name="quantileSorted" href="#quantileSorted">#</a> d3.<b>quantileSorted</b>(<i>array</i>, <i>p</i>[, <i>accessor</i>]) · [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.
Expand Down
4 changes: 2 additions & 2 deletions src/index.js
Expand Up @@ -18,15 +18,15 @@ 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";
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";
Expand Down
6 changes: 5 additions & 1 deletion 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);
}
16 changes: 16 additions & 0 deletions 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));
Expand All @@ -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]);
}
1 change: 1 addition & 0 deletions src/quickselect.js
Expand Up @@ -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;
}

Expand Down
12 changes: 11 additions & 1 deletion 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", () => {
Expand Down Expand Up @@ -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};
}
Expand Down
29 changes: 28 additions & 1 deletion 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);
Expand Down Expand Up @@ -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};
}
Expand Down

0 comments on commit 99fe7ae

Please sign in to comment.