Skip to content

Commit

Permalink
rank
Browse files Browse the repository at this point in the history
  • Loading branch information
Fil committed Aug 17, 2021
1 parent 61cc940 commit a30f720
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 0 deletions.
18 changes: 18 additions & 0 deletions README.md
Expand Up @@ -141,6 +141,24 @@ An optional *accessor* function may be specified, which is equivalent to calling

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.

<a name="rank" href="#rank">#</a> d3.<b>rank</b>(<i>iterable</i>[, <i>ties</i>][, <i>accessor</i>]) · [Source](https://github.com/d3/d3-array/blob/master/src/rank.js)<!-- , [Examples](https://observablehq.com/@d3/rank) -->

Returns an array with the rank of each value in the *iterable*, *i.e* the index of the value when the iterable is sorted. Nullish values are sorted to the end and ranked NaN. An optional *accessor* function may be specified, which is equivalent to calling *array*.map(*accessor*) before computing the ranks. Ties (equivalent values) all get the same rank, defined by default as the first time the value is found.

The *ties* option sets a different strategy for ties:

* `low` (default) - the first time the value was found
* `mean` - the average time the value was found
* `round` - the rounded average time the value was found
* `high` - the last time the value was found
* `order` - each tie has its own rank

```js
d3.rank([{x: 1}, {x: 2}, {x: 0}, {}], d => d.x); // returns [1, 2, 0, NaN]
d3.rank(["b", "c", "b", "a"]); // returns [1, 3, 1, 0]
d3.rank(["b", "c", "b", "a"], "order"); // returns [1, 3, 2, 0]
```

<a name="variance" href="#variance">#</a> d3.<b>variance</b>(<i>iterable</i>[, <i>accessor</i>]) · [Source](https://github.com/d3/d3-array/blob/master/src/variance.js), [Examples](https://observablehq.com/@d3/d3-mean-d3-median-and-friends)

Returns an [unbiased estimator of the population variance](http://mathworld.wolfram.com/SampleVariance.html) of the given *iterable* of numbers using [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm). If the iterable has fewer than two numbers, returns undefined. An optional *accessor* function may be specified, which is equivalent to calling Array.from before computing the variance. This method ignores undefined and NaN values; this is useful for ignoring missing data.
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Expand Up @@ -28,6 +28,7 @@ export {default as permute} from "./permute.js";
export {default as quantile, 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";
export {default as least} from "./least.js";
export {default as leastIndex} from "./leastIndex.js";
export {default as greatest} from "./greatest.js";
Expand Down
47 changes: 47 additions & 0 deletions src/rank.js
@@ -0,0 +1,47 @@
import range from "./range.js";
import sort from "./sort.js";

export default function rank(values, ties = "low", valueof) {
if (typeof ties === "function") {
valueof = ties; ties = "low";
}
values = Array.from(values, valueof);
const n = values.length;
const r = new Float64Array(n);

let last, l;
const order = sort(range(n), (i) => values[i]);
order.forEach((j, i) => {
const value = values[j];
if (value == null || !(value <= value)) {
r[j] = NaN;
return;
}
if (last === undefined || !(value <= last)) {
last = value;
l = i;
}
r[j] = l;
});

// backtrack to handle ties: low, mean, round, high, order
if (ties === "order") {
order.forEach((i,j) => r[i] = isNaN(r[i]) ? NaN : j);
} else if (ties !== "low") {
let find, replace;
for (let i = n - 1; i >= 0; i--) {
const j = r[order[i]];
if (i !== j && find !== j) {
find = j;
switch (ties) {
case "mean": replace = (i + j) / 2; break;
case "round": replace = (i + j) >> 1; break;
case "high": replace = i; break;
}
}
if (j === find) r[order[i]] = replace;
}
}

return r;
}
56 changes: 56 additions & 0 deletions test/rank-test.js
@@ -0,0 +1,56 @@
import assert from "assert";
import rank from "../src/rank.js";

it("rank(numbers) returns the rank of numbers", () => {
assert.deepStrictEqual(rank([1000, 10, 0]), new Float64Array([2, 1, 0]));
assert.deepStrictEqual(rank([1.2, 1.1, 1.2, 1.0, 1.5, 1.2]), new Float64Array([2, 1, 2, 0, 5, 2]));
});

it("rank(strings) returns the rank of letters", () => {
assert.deepStrictEqual(rank([..."EDGFCBA"]), new Float64Array([4, 3, 6, 5, 2, 1, 0]));
});

it("rank(dates) returns the rank of Dates", () => {
assert.deepStrictEqual(rank([new Date(2000, 0, 1), new Date(2000, 0, 1), new Date(1999, 0, 1), new Date(2001, 0, 1)]), new Float64Array([1, 1, 0, 3]));
});

it("rank(iterator) accepts an iterator", () => {
assert.deepStrictEqual(rank(new Set(["B", "C", "A"])), new Float64Array([1, 2, 0]));
assert.deepStrictEqual(rank(new Set(["B", "C", "A"]), "high"), new Float64Array([1, 2, 0]));
assert.deepStrictEqual(rank({length: 3}, (_, i) => i), new Float64Array([0, 1, 2]));
});

it("rank(undefineds) ranks undefined as NaN", () => {
assert.deepStrictEqual(rank([1.2, 1.1, undefined, 1.0, undefined, 1.5]), new Float64Array([2, 1, NaN, 0, NaN, 3]));
assert.deepStrictEqual(rank([, null, , 1.2, 1.1, undefined, 1.0, NaN, 1.5]), new Float64Array([NaN, NaN, NaN, 2, 1, NaN, 0, NaN, 3]));
});

it("rank(values, valueof) accepts an accessor", () => {
assert.deepStrictEqual(rank([{x: 3}, {x: 1}, {x: 2}, {x: 4}, {}], d => d.x), new Float64Array([2, 0, 1, 3, NaN]));
});

it("rank(values, ties) computes the ties as expected", () => {
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c"], "low"), new Float64Array([0, 1, 1, 1, 4]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c"], "mean"), new Float64Array([0, 2, 2, 2, 4]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c"], "round"), new Float64Array([0, 2, 2, 2, 4]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c"], "high"), new Float64Array([0, 3, 3, 3, 4]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c"], "order"), new Float64Array([0, 1, 2, 3, 4]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c"], "low"), new Float64Array([0, 1, 1, 1, 1, 5]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c"], "mean"), new Float64Array([0, 2.5, 2.5, 2.5, 2.5, 5]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c"], "round"), new Float64Array([0, 2, 2, 2, 2, 5]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c"], "high"), new Float64Array([0, 4, 4, 4, 4, 5]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c"], "order"), new Float64Array([0, 1, 2, 3, 4, 5]));
});

it("rank(values, ties) handles NaNs as expected", () => {
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c", null], "low"), new Float64Array([0, 1, 1, 1, 4, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c", null], "mean"), new Float64Array([0, 2, 2, 2, 4, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c", null], "round"), new Float64Array([0, 2, 2, 2, 4, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c", null], "high"), new Float64Array([0, 3, 3, 3, 4, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c", null], "order"), new Float64Array([0, 1, 2, 3, 4, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c", null], "low"), new Float64Array([0, 1, 1, 1, 1, 5, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c", null], "mean"), new Float64Array([0, 2.5, 2.5, 2.5, 2.5, 5, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c", null], "round"), new Float64Array([0, 2, 2, 2, 2, 5, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c", null], "high"), new Float64Array([0, 4, 4, 4, 4, 5, NaN]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c", null], "order"), new Float64Array([0, 1, 2, 3, 4, 5, NaN]));
});

0 comments on commit a30f720

Please sign in to comment.