Skip to content

Commit 80a7b01

Browse files
authored
percentile reducers (#776)
1 parent df073d8 commit 80a7b01

File tree

8 files changed

+34
-29
lines changed

8 files changed

+34
-29
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,9 +1354,10 @@ The following aggregation methods are supported:
13541354
* *max-index* - the zero-based index of the maximum value
13551355
* *mean* - the mean value (average)
13561356
* *median* - the median value
1357+
* *mode* - the value with the most occurrences
1358+
* *pXX* - the percentile value, where XX is a number in [00,99]
13571359
* *deviation* - the standard deviation
13581360
* *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm)
1359-
* *mode* - the value with the most occurrences
13601361
* *x* - the middle the bin’s *x*-extent (when binning on *x*)
13611362
* *x1* - the lower bound of the bin’s *x*-extent (when binning on *x*)
13621363
* *x2* - the upper bound of the bin’s *x*-extent (when binning on *x*)
@@ -1492,6 +1493,7 @@ The following aggregation methods are supported:
14921493
* *max-index* - the zero-based index of the maximum value
14931494
* *mean* - the mean value (average)
14941495
* *median* - the median value
1496+
* *pXX* - the percentile value, where XX is a number in [00,99]
14951497
* *deviation* - the standard deviation
14961498
* *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm)
14971499
* a function - passed the array of values for each group
@@ -1579,10 +1581,11 @@ The Plot.normalizeX and Plot.normalizeY transforms normalize series values relat
15791581

15801582
* *first* - the first value, as in an index chart; the default
15811583
* *last* - the last value
1584+
* *min* - the minimum value
15821585
* *max* - the maximum value
15831586
* *mean* - the mean value (average)
15841587
* *median* - the median value
1585-
* *min* - the minimum value
1588+
* *pXX* - the percentile value, where XX is a number in [00,99]
15861589
* *sum* - the sum of values
15871590
* *extent* - the minimum is mapped to zero, and the maximum to one
15881591
* *deviation* - each value is transformed by subtracting the mean and then dividing by the standard deviation
@@ -1601,6 +1604,7 @@ The following window reducers are supported:
16011604
* *mean* - the mean (average)
16021605
* *median* - the median
16031606
* *mode* - the mode (most common occurrence)
1607+
* *pXX* - the percentile value, where XX is a number in [00,99]
16041608
* *sum* - the sum of values
16051609
* *deviation* - the standard deviation
16061610
* *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm)

src/options.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {parse as isoParse} from "isoformat";
2-
import {color, descending} from "d3";
2+
import {color, descending, quantile} from "d3";
33
import {symbolAsterisk, symbolDiamond2, symbolPlus, symbolSquare2, symbolTriangle2, symbolX as symbolTimes} from "d3";
44
import {symbolCircle, symbolCross, symbolDiamond, symbolSquare, symbolStar, symbolTriangle, symbolWye} from "d3";
55

@@ -29,6 +29,13 @@ export const first = x => x ? x[0] : undefined;
2929
export const second = x => x ? x[1] : undefined;
3030
export const constant = x => () => x;
3131

32+
// Converts a string like “p25” into a function that takes an index I and an
33+
// accessor function f, returning the corresponding percentile value.
34+
export function percentile(reduce) {
35+
const p = +`${reduce}`.slice(1) / 100;
36+
return (I, f) => quantile(I, p, f);
37+
}
38+
3239
// Some channels may allow a string constant to be specified; to differentiate
3340
// string constants (e.g., "red") from named fields (e.g., "date"), this
3441
// function tests whether the given value is a CSS color string and returns a

src/transforms/group.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {group as grouper, sort, sum, deviation, min, max, mean, median, mode, variance, InternSet, minIndex, maxIndex, rollup} from "d3";
22
import {ascendingDefined, firstof} from "../defined.js";
3-
import {valueof, maybeColorChannel, maybeInput, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take, labelof, range, second} from "../options.js";
3+
import {valueof, maybeColorChannel, maybeInput, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take, labelof, range, second, percentile} from "../options.js";
44
import {basic} from "./basic.js";
55

66
// Group on {z, fill, stroke}.
@@ -198,6 +198,7 @@ export function maybeGroup(I, X) {
198198
export function maybeReduce(reduce, value) {
199199
if (reduce && typeof reduce.reduce === "function") return reduce;
200200
if (typeof reduce === "function") return reduceFunction(reduce);
201+
if (/^p\d{2}$/i.test(reduce)) return reduceAccessor(percentile(reduce));
201202
switch (`${reduce}`.toLowerCase()) {
202203
case "first": return reduceFirst;
203204
case "last": return reduceLast;

src/transforms/normalize.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {extent, deviation, max, mean, median, min, sum} from "d3";
22
import {defined} from "../defined.js";
3-
import {take} from "../options.js";
3+
import {percentile, take} from "../options.js";
44
import {mapX, mapY} from "./map.js";
55

66
export function normalizeX(basis, options) {
@@ -16,6 +16,7 @@ export function normalizeY(basis, options) {
1616
export function normalize(basis) {
1717
if (basis === undefined) return normalizeFirst;
1818
if (typeof basis === "function") return normalizeBasis((I, S) => basis(take(S, I)));
19+
if (/^p\d{2}$/i.test(basis)) return normalizeAccessor(percentile(basis));
1920
switch (`${basis}`.toLowerCase()) {
2021
case "deviation": return normalizeDeviation;
2122
case "first": return normalizeFirst;
@@ -41,6 +42,10 @@ function normalizeBasis(basis) {
4142
};
4243
}
4344

45+
function normalizeAccessor(f) {
46+
return normalizeBasis((I, S) => f(I, i => S[i]));
47+
}
48+
4449
const normalizeExtent = {
4550
map(I, S, T) {
4651
const [s1, s2] = extent(I, i => S[i]), d = s2 - s1;
@@ -74,8 +79,8 @@ const normalizeDeviation = {
7479
}
7580
};
7681

77-
const normalizeMax = normalizeBasis((I, S) => max(I, i => S[i]));
78-
const normalizeMean = normalizeBasis((I, S) => mean(I, i => S[i]));
79-
const normalizeMedian = normalizeBasis((I, S) => median(I, i => S[i]));
80-
const normalizeMin = normalizeBasis((I, S) => min(I, i => S[i]));
81-
const normalizeSum = normalizeBasis((I, S) => sum(I, i => S[i]));
82+
const normalizeMax = normalizeAccessor(max);
83+
const normalizeMean = normalizeAccessor(mean);
84+
const normalizeMedian = normalizeAccessor(median);
85+
const normalizeMin = normalizeAccessor(min);
86+
const normalizeSum = normalizeAccessor(sum);

src/transforms/window.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {mapX, mapY} from "./map.js";
22
import {deviation, max, min, median, mode, variance} from "d3";
33
import {warn} from "../warnings.js";
4+
import {percentile} from "../options.js";
45

56
export function windowX(windowOptions = {}, options) {
67
if (arguments.length === 1) options = windowOptions;
@@ -43,6 +44,7 @@ function maybeShift(shift) {
4344

4445
function maybeReduce(reduce = "mean") {
4546
if (typeof reduce === "string") {
47+
if (/^p\d{2}$/i.test(reduce)) return reduceSubarray(percentile(reduce));
4648
switch (reduce.toLowerCase()) {
4749
case "deviation": return reduceSubarray(deviation);
4850
case "max": return reduceSubarray(max);

test/plots/aapl-monthly.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import * as d3 from "d3";
44
export default async function() {
55
const data = await d3.csv("data/aapl.csv", d3.autoType);
66
const bin = {x: "Date", y: "Volume", thresholds: 40};
7-
const q1 = data => d3.quantile(data, 0.25);
8-
const q3 = data => d3.quantile(data, 0.75);
97
return Plot.plot({
108
y: {
119
transform: d => d / 1e6,
@@ -15,8 +13,8 @@ export default async function() {
1513
marks: [
1614
Plot.ruleY([0]),
1715
Plot.ruleX(data, Plot.binX({y1: "min", y2: "max"}, {...bin, stroke: "#999"})),
18-
Plot.rect(data, Plot.binX({y1: q1, y2: q3}, {...bin, fill: "#bbb"})),
19-
Plot.ruleY(data, Plot.binX({y: "median"}, {...bin, strokeWidth: 2}))
16+
Plot.rect(data, Plot.binX({y1: "p25", y2: "p75"}, {...bin, fill: "#bbb"})),
17+
Plot.ruleY(data, Plot.binX({y: "p50"}, {...bin, strokeWidth: 2}))
2018
]
2119
});
2220
}

test/plots/morley-boxplot.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ function boxX(data, {
1717
} = {}) {
1818
return Plot.marks(
1919
Plot.ruleY(data, Plot.groupY({x1: iqr1, x2: iqr2}, {x, y, stroke, ...options})),
20-
Plot.barX(data, Plot.groupY({x1: quartile1, x2: quartile3}, {x, y, fill, ...options})),
21-
Plot.tickX(data, Plot.groupY({x: median}, {x, y, stroke, strokeWidth: 2, ...options})),
20+
Plot.barX(data, Plot.groupY({x1: "p25", x2: "p75"}, {x, y, fill, ...options})),
21+
Plot.tickX(data, Plot.groupY({x: "p50"}, {x, y, stroke, strokeWidth: 2, ...options})),
2222
Plot.dot(data, Plot.map({x: outliers}, {x, y, z: y, stroke, ...options}))
2323
);
2424
}
@@ -39,10 +39,6 @@ function iqr2(values, value) {
3939
return Math.min(d3.max(values, value), quartile3(values, value) * 2.5 - quartile1(values, value) * 1.5);
4040
}
4141

42-
function median(values, value) {
43-
return d3.median(values, value);
44-
}
45-
4642
function quartile1(values, value) {
4743
return d3.quantile(values, 0.25, value);
4844
}

test/plots/movies-profit-by-genre.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default async function() {
1515
},
1616
marks: [
1717
Plot.ruleX([0]),
18-
Plot.barX(movies, Plot.groupY({x1: quartile1, x2: quartile3}, {
18+
Plot.barX(movies, Plot.groupY({x1: "p25", x2: "p75"}, {
1919
y: Genre,
2020
x: Profit,
2121
fillOpacity: 0.2
@@ -35,11 +35,3 @@ export default async function() {
3535
]
3636
});
3737
}
38-
39-
function quartile1(values, value) {
40-
return d3.quantile(values, 0.25, value);
41-
}
42-
43-
function quartile3(values, value) {
44-
return d3.quantile(values, 0.75, value);
45-
}

0 commit comments

Comments
 (0)