Skip to content

Commit

Permalink
only implement “low” ties strategy (#236)
Browse files Browse the repository at this point in the history
* only implement “low” ties strategy
* only test “low” ties strategy
* only allow iterables
* adopt Float64Array.of
* documentation: remove ties; observablehq/@d3/rank
Co-authored-by: Philippe Rivière <fil@rezo.net>
  • Loading branch information
mbostock committed Oct 1, 2021
1 parent 2841584 commit f090568
Show file tree
Hide file tree
Showing 3 changed files with 21 additions and 70 deletions.
18 changes: 5 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,22 +141,14 @@ 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) -->
<a name="rank" href="#rank">#</a> d3.<b>rank</b>(<i>iterable</i>[, <i>accessor</i>]) · [Source](https://github.com/d3/d3-array/blob/master/src/rank.js), [Examp
les](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
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 as the first time the value is found.

```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]
d3.rank([{x: 1}, {}, {x: 2}, {x: 0}], d => d.x); // [1, NaN, 2, 0]
d3.rank(["b", "c", "b", "a"]); // [1, 3, 1, 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)
Expand Down
30 changes: 3 additions & 27 deletions src/rank.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
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";
}
export default function rank(values, valueof) {
if (typeof values[Symbol.iterator] !== "function") throw new TypeError("values is not iterable");
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) => {
sort(range(n), (i) => values[i]).forEach((j, i) => {
const value = values[j];
if (value == null || !(value <= value)) {
r[j] = NaN;
Expand All @@ -23,25 +19,5 @@ export default function rank(values, ties = "low", valueof) {
}
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;
}
43 changes: 13 additions & 30 deletions test/rank-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,38 @@ 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]));
assert.deepStrictEqual(rank([1000, 10, 0]), Float64Array.of(2, 1, 0));
assert.deepStrictEqual(rank([1.2, 1.1, 1.2, 1.0, 1.5, 1.2]), Float64Array.of(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]));
assert.deepStrictEqual(rank([..."EDGFCBA"]), Float64Array.of(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]));
assert.deepStrictEqual(rank([new Date(2000, 0, 1), new Date(2000, 0, 1), new Date(1999, 0, 1), new Date(2001, 0, 1)]), Float64Array.of(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]));
assert.deepStrictEqual(rank(new Set(["B", "C", "A"])), Float64Array.of(1, 2, 0));
assert.deepStrictEqual(rank({length: 3}, (_, i) => i), Float64Array.of(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]));
assert.deepStrictEqual(rank([1.2, 1.1, undefined, 1.0, undefined, 1.5]), Float64Array.of(2, 1, NaN, 0, NaN, 3));
assert.deepStrictEqual(rank([, null, , 1.2, 1.1, undefined, 1.0, NaN, 1.5]), Float64Array.of(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]));
assert.deepStrictEqual(rank([{x: 3}, {x: 1}, {x: 2}, {x: 4}, {}], d => d.x), Float64Array.of(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]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c"]), Float64Array.of(0, 1, 1, 1, 4));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c"]), Float64Array.of(0, 1, 1, 1, 1, 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]));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "c", null]), Float64Array.of(0, 1, 1, 1, 4, NaN));
assert.deepStrictEqual(rank(["a", "b", "b", "b", "b", "c", null]), Float64Array.of(0, 1, 1, 1, 1, 5, NaN));
});

0 comments on commit f090568

Please sign in to comment.