Skip to content

Commit 747c6a9

Browse files
committed
add more quantiles computation methods
1 parent df6889e commit 747c6a9

File tree

3 files changed

+99
-9
lines changed

3 files changed

+99
-9
lines changed

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,18 @@ interface IArrayLinearScale {
170170
coef: number;
171171

172172
/**
173-
* the method to compute the quantiles. 7 and 'quantiles' refers to the type-7 method as used by R 'quantiles' method. 'hinges' and 'fivenum' refers to the method used by R 'boxplot.stats' method.
173+
* the method to compute the quantiles.
174+
*
175+
* 7, 'quantiles': the type-7 method as used by R 'quantiles' method.
176+
* 'hinges' and 'fivenum': the method used by R 'boxplot.stats' method.
177+
* 'linear': the interpolation method 'linear' as used by 'numpy.percentile' function
178+
* 'lower': the interpolation method 'lower' as used by 'numpy.percentile' function
179+
* 'higher': the interpolation method 'higher' as used by 'numpy.percentile' function
180+
* 'nearest': the interpolation method 'nearest' as used by 'numpy.percentile' function
181+
* 'midpoint': the interpolation method 'midpoint' as used by 'numpy.percentile' function
174182
* @default 7
175183
*/
176-
quantiles: 7 | 'quantiles' | 'hinges' | 'fivenum' | ((sortedArr: number[]) => {min: number, q1: number, median: number, q3: number, max: number});
184+
quantiles: 7 | 'quantiles' | 'hinges' | 'fivenum' | 'linear' | 'lower' | 'higher' | 'nearest' | 'midpoint' | ((sortedArr: number[]) => {min: number, q1: number, median: number, q3: number, max: number});
177185
};
178186
}
179187
```

src/data.js

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22

33
import kde from '@sgratzl/science/src/stats/kde';
44

5-
// Uses R's quantile algorithm type=7.
6-
// https://en.wikipedia.org/wiki/Quantile#Quantiles_of_a_population
7-
export function quantilesType7(arr) {
5+
/**
6+
* computes the boxplot stats using the given interpolation function if needed
7+
* @param {number[]} arr sorted array of number
8+
* @param {(i: number, j: number, fraction: number)} interpolate interpolation function
9+
*/
10+
function quantilesInterpolate(arr, interpolate) {
811
const n1 = arr.length - 1;
912
const compute = (q) => {
1013
const index = 1 + q * n1;
1114
const lo = Math.floor(index);
1215
const h = index - lo;
1316
const a = arr[lo - 1];
1417

15-
return h === 0 ? a : a + h * (arr[lo] - a);
18+
return h === 0 ? a : interpolate(a, arr[lo], h);
1619
};
1720

1821
return {
@@ -24,6 +27,50 @@ export function quantilesType7(arr) {
2427
};
2528
}
2629

30+
/**
31+
* Uses R's quantile algorithm type=7.
32+
* https://en.wikipedia.org/wiki/Quantile#Quantiles_of_a_population
33+
*/
34+
export function quantilesType7(arr) {
35+
return quantilesInterpolate(arr, (a, b, alpha) => a + alpha * (b - a));
36+
}
37+
38+
/**
39+
* ‘linear’: i + (j - i) * fraction, where fraction is the fractional part of the index surrounded by i and j.
40+
* (same as type 7)
41+
*/
42+
export function quantilesLinear(arr) {
43+
return quantilesInterpolate(arr, (i, j, fraction) => i + (j - i) * fraction);
44+
}
45+
46+
/**
47+
* ‘lower’: i.
48+
*/
49+
export function quantilesLower(arr) {
50+
return quantilesInterpolate(arr, (i) => i);
51+
}
52+
53+
/**
54+
* 'higher': j.
55+
*/
56+
export function quantilesHigher(arr) {
57+
return quantilesInterpolate(arr, (_, j) => j);
58+
}
59+
60+
/**
61+
* ‘nearest’: i or j, whichever is nearest
62+
*/
63+
export function quantilesNearest(arr) {
64+
return quantilesInterpolate(arr, (i, j, fraction) => (fraction <= 0.5 ? i : j));
65+
}
66+
67+
/**
68+
* ‘midpoint’: (i + j) / 2
69+
*/
70+
export function quantilesMidpoint(arr) {
71+
return quantilesInterpolate(arr, (i, j) => (i + j) * 0.5);
72+
}
73+
2774
/**
2875
* The hinges equal the quartiles for odd n (where n <- length(x))
2976
* and differ for even n. Whereas the quartiles only equal observations
@@ -92,10 +139,28 @@ const defaultStatsOptions = {
92139
quantiles: 7
93140
};
94141

142+
function determineQuantiles(q) {
143+
if (typeof q === 'function') {
144+
return q;
145+
}
146+
const lookup = {
147+
hinges: fivenum,
148+
fivenum: fivenum,
149+
7: quantilesType7,
150+
quantiles: quantilesType7,
151+
linear: quantilesLinear,
152+
lower: quantilesLower,
153+
higher: quantilesHigher,
154+
nearest: quantilesNearest,
155+
midpoint: quantilesMidpoint
156+
}
157+
return lookup[q] || quantilesType7;
158+
}
159+
95160
function determineStatsOptions(options) {
96161
const coef = options == null || typeof options.coef !== 'number' ? defaultStatsOptions.coef : options.coef;
97162
const q = options == null ? null : options.quantiles;
98-
const quantiles = typeof q === 'function' ? q : (q === 'hinges' || q === 'fivenum' ? fivenum : quantilesType7);
163+
const quantiles = determineQuantiles(q);
99164
return {
100165
coef,
101166
quantiles

src/data.spec.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {quantilesType7, fivenum} from './data';
1+
import {quantilesType7, fivenum, quantilesNearest, quantilesHigher, quantilesLinear, quantilesLower, quantilesMidpoint} from './data';
22

33
function asc(a, b) {
44
return a - b;
@@ -72,6 +72,23 @@ describe('quantiles and fivenum', () => {
7272
expect(fivenum(arr)).toEqual(asB(5830.748, 6518.398999999999, 7459.0635, 13297.2845, 18882.492));
7373
});
7474
});
75+
});
7576

76-
77+
describe('numpy interpolation', () => {
78+
const arr = [3.375, 3.75, 3.875, 3, 3, 3.5, 3.125, 3, 2.625, 3.375, 3].sort(asc);
79+
it('linear', () => {
80+
expect(quantilesLinear(arr).q3).toBe(3.475);
81+
});
82+
it('higher', () => {
83+
expect(quantilesHigher(arr).q3).toBe(3.5);
84+
});
85+
it('lower', () => {
86+
expect(quantilesLower(arr).q3).toBe(3.375);
87+
});
88+
it('nearest', () => {
89+
expect(quantilesNearest(arr).q3).toBe(3.5);
90+
});
91+
it('midpoint', () => {
92+
expect(quantilesMidpoint(arr).q3).toBe(3.4375);
93+
});
7794
});

0 commit comments

Comments
 (0)