Skip to content

Commit

Permalink
Adds support for sample curotis. Fixes #23
Browse files Browse the repository at this point in the history
  • Loading branch information
boristane committed May 6, 2019
1 parent 77163d0 commit ce088d3
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 47 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ npm i stat-methods
- [mode](#mode)
- [rms](#rms)
- [percentile](#percentile)
- [kurtosis](#kurtosis)

2. [Measures of spread](#Measures-of-spread)

Expand Down Expand Up @@ -75,6 +76,7 @@ These methods compute an average or typical value from a population or sample.
| [mode](#mode) | Modes (most common data points) of discrete data |
| [rms](#rms) | Root Mean Square |
| [percentile](#percentile) | Percentile |
| [kurtosis](#kurtosis) | Kurtosis |

Note: The methods do not require the data given to them to be sorted.

Expand Down Expand Up @@ -373,6 +375,22 @@ percentile([13, 20, 8, 8, 7, 10, 3, 15, 16, 6], 0.25); // -> 7

If the data array is empty or contains a non-numeric value, the method returns `undefined`. If the value of `k` is non-numeric and not in the interval `[0, 1]`, the method returns `undefined`.

#### kurtosis

```js
kurtosis(arr);
```

Returns the sample kurtosis of the data array.

The sample kurtosis is a measure of the "tailedness" of a data array.
```js
const arr = [0, 3, 4, 1, 2, 3, 0, 2, 1, 3, 2, 0, 2, 2, 3, 2, 5, 2, 3, 999];
kurtosis(arr).toFixed(2); // -> '15.05';
```

If the data array is empty or contains a non-numeric value, the method returns `undefined`.

### Measures of spread

These methods compute a measure of the variability in a sample or population, how much the sample or population tends to deviate from the typical or average values.
Expand Down
16 changes: 15 additions & 1 deletion src/central.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { product, min, max } from './descriptive';
import { getAllIndexes, nthRoot, kahanSum } from './utils';
import { getAllIndexes, nthRoot, kahanSum, nthMomentAboutMean } from './utils';

/**
* Return the sample arithmetic mean of a numeric data array.
Expand Down Expand Up @@ -252,6 +252,20 @@ export function percentile(arr, k) {
return sorted[l];
}

/**
* Returns the sample kurtosis of a numerical data array.
* The sample kurtosis is a measure of the "tailedness" of the data.
* @param {Number[]} arr the data array
* @returns {Number} the sample kurtosis of the data array
*/
export function kurtosis(arr) {
if (!Array.isArray(arr) || arr.length === 0) return undefined;
const m4 = nthMomentAboutMean(arr, 4);
const m2 = nthMomentAboutMean(arr, 2);
if (m2 === undefined || m4 === undefined) return undefined;
return m4 / m2 ** 2 - 3;
}

export default {
mean,
harmonicMean,
Expand Down
20 changes: 18 additions & 2 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function getAllIndexes(arr, elt) {
export function nthRoot(val, n) {
if (!Number.isFinite(val) || !Number.isFinite(n)) return undefined;
if (val < 0 && n % 2 === 0) return undefined;
return (val < 0 ? -1 : 1) * (Math.abs(val) ** (1 / n));
return (val < 0 ? -1 : 1) * Math.abs(val) ** (1 / n);
}

/**
Expand All @@ -37,14 +37,30 @@ export function kahanSum(arr) {
if (!Number.isFinite(arr[i])) return undefined;
const y = arr[i] - compensation;
const total = result + y;
compensation = (total - result) - y;
compensation = total - result - y;
result = total;
}
return result;
}

/**
* Returns the nth moment about the mean of the data array.
* @param {Number[]} arr the data array
* @param {Number} n the moment order
* @returns {Number} the nth moment about the mean
*/
export function nthMomentAboutMean(arr, n) {
const s = kahanSum(arr);
if (s === undefined) return undefined;
const xBar = s / arr.length;
const a = arr.map((elt) => (elt - xBar) ** n);
const sum = a.reduce((acc, curr) => acc + curr);
return sum / arr.length;
}

export default {
getAllIndexes,
nthRoot,
kahanSum,
nthMomentAboutMean,
};
92 changes: 52 additions & 40 deletions test/central.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { testUndefinedWithNullable } from "./utils";
import { testUndefinedWithNullable } from './utils';
import {
mean,
harmonicMean,
Expand All @@ -11,14 +11,15 @@ import {
mode,
quartiles,
rms,
percentile
} from "../src/central";
percentile,
kurtosis,
} from '../src/central';

describe("Averages and measures of central location", () => {
test("Mean", () => {
describe('Averages and measures of central location', () => {
test('Mean', () => {
expect(mean([1, 2, 3, 4, 4])).toBe(2.8);
expect(mean([-1.0, 2.5, 3.25, 5.75])).toBe(2.625);
expect(mean(["a", 2.5, "b", 5.75])).toBeUndefined();
expect(mean(['a', 2.5, 'b', 5.75])).toBeUndefined();
expect(mean([NaN, 2.5, 3, 5.75])).toBeUndefined();
expect(mean([])).toBeUndefined();
expect(mean(3)).toBeUndefined();
Expand All @@ -27,81 +28,81 @@ describe("Averages and measures of central location", () => {
testUndefinedWithNullable(mean);
});

test("Harmonic mean", () => {
test('Harmonic mean', () => {
expect(harmonicMean([2.5, 3, 10]) * 10).toBe(36);
expect(harmonicMean([1.1, 1.4, 3.5, 9.5])).toBe(1.9857482185273159);
expect(harmonicMean([2.5, 0, 10])).toBeUndefined();
expect(harmonicMean(["a", 2.5, "b", 5.75])).toBeUndefined();
expect(harmonicMean(['a', 2.5, 'b', 5.75])).toBeUndefined();
expect(harmonicMean([NaN, 2.5, 3, 5.75])).toBeUndefined();
expect(harmonicMean([])).toBeUndefined();
expect(harmonicMean(3)).toBeUndefined();
expect(harmonicMean([3])).toBe(3);
testUndefinedWithNullable(harmonicMean);
});

test("Geometric mean", () => {
test('Geometric mean', () => {
expect(geometricMean([1, 2, 4])).toBe(2);
expect(geometricMean([4, 1, 1 / 32])).toBe(0.5);
expect(geometricMean([1.1, 1.4, 3.5, 9.5])).toBe(2.6750265241846307);
expect(geometricMean([2.5, 0, 10])).toBe(0);
expect(geometricMean([-1, 2, 4])).toBe(-2);
expect(geometricMean([-1, 2, 4, 2])).toBeUndefined();
expect(geometricMean(["a", 2.5, "b", 5.75])).toBeUndefined();
expect(geometricMean(['a', 2.5, 'b', 5.75])).toBeUndefined();
expect(geometricMean([NaN, 2.5, 3, 5.75])).toBeUndefined();
expect(geometricMean([])).toBeUndefined();
expect(geometricMean(3)).toBeUndefined();
expect(geometricMean([3])).toBe(3);
testUndefinedWithNullable(geometricMean);
});

test("Median", () => {
test('Median', () => {
expect(median([1, 12, 3, 15, 6, 8, 9])).toBe(8);
expect(median([1, -2, 3, 4, 8, 6, 5, 9])).toBe(4.5);
expect(median([1, 2, 3, 4, 5])).toBe(3);
expect(median([1, 2, 3, 4, 5, 6])).toBe(3.5);
expect(median(["a", 2.5, "b", 5.75])).toBeUndefined();
expect(median(['a', 2.5, 'b', 5.75])).toBeUndefined();
expect(median([NaN, 2.5, 3, 5.75])).toBeUndefined();
expect(median([])).toBeUndefined();
expect(median(3)).toBeUndefined();
expect(median([3])).toBe(3);
testUndefinedWithNullable(median);
});

test("Median Low", () => {
test('Median Low', () => {
expect(medianLow([1, 12, 3, 15, 6, 8, 9])).toBe(8);
expect(medianLow([1, -2, 3, 4, 8, 6, 5, 9])).toBe(4);
expect(medianLow([1, 2, 3, 4, 5])).toBe(3);
expect(medianLow([1, 2, 3, 4, 5, 6])).toBe(3);
expect(
medianLow(
["a", "c", "b", "d"],
(a, b) => a.charCodeAt(0) - b.charCodeAt(0)
)
).toBe("b");
['a', 'c', 'b', 'd'],
(a, b) => a.charCodeAt(0) - b.charCodeAt(0),
),
).toBe('b');
expect(medianLow([])).toBeUndefined();
expect(medianLow(3)).toBeUndefined();
expect(medianLow([3])).toBe(3);
testUndefinedWithNullable(medianLow);
});

test("Median High", () => {
test('Median High', () => {
expect(medianHigh([1, 12, 3, 15, 6, 8, 9])).toBe(8);
expect(medianHigh([1, -2, 3, 4, 8, 6, 5, 9])).toBe(5);
expect(medianHigh([1, 2, 3, 4, 5])).toBe(3);
expect(medianHigh([1, 2, 3, 4, 5, 6])).toBe(4);
expect(
medianHigh(
["a", "c", "b", "d"],
(a, b) => a.charCodeAt(0) - b.charCodeAt(0)
)
).toBe("c");
['a', 'c', 'b', 'd'],
(a, b) => a.charCodeAt(0) - b.charCodeAt(0),
),
).toBe('c');
expect(medianHigh([])).toBeUndefined();
expect(medianHigh(3)).toBeUndefined();
expect(medianHigh([3])).toBe(3);
testUndefinedWithNullable(medianHigh);
});

test("Median Grouped", () => {
test('Median Grouped', () => {
expect(medianGrouped([52, 52, 53, 54])).toBe(52.5);
expect(medianGrouped([1, 2, 2, 3, 4, 4, 4, 4, 4, 5])).toBe(3.7);
expect(
Expand All @@ -127,56 +128,56 @@ describe("Averages and measures of central location", () => {
59,
68,
61,
67
67,
],
5
)
5,
),
).toBe(61.4375);
expect(medianGrouped([1, 3, 3, 5, 7])).toBe(3.25);
expect(medianGrouped([1, 3, 3, 5, 7], 2)).toBe(3.5);
expect(medianGrouped(["a", 2.5, "b", 5.75])).toBeUndefined();
expect(medianGrouped(['a', 2.5, 'b', 5.75])).toBeUndefined();
expect(medianGrouped([NaN, 2.5, 3, 5.75])).toBeUndefined();
expect(medianGrouped([])).toBeUndefined();
expect(medianGrouped(3)).toBeUndefined();
expect(medianGrouped([3])).toEqual(3);
testUndefinedWithNullable(medianGrouped);
});

test("Mid-range", () => {
test('Mid-range', () => {
expect(midRange([1, 12, 3, 15, 6, 8, 9])).toBe(8);
expect(midRange([1, -2, 3, 4, 8, 6, 5, 9])).toBe(3.5);
expect(midRange([1, 2, 3, 4, 5, 6, -1])).toBe(2.5);
expect(midRange([1, 2, 3, 4, 5, 6, "xyz"])).toBeUndefined();
expect(midRange([1, 2, 3, 4, 5, 6, 'xyz'])).toBeUndefined();
expect(midRange([1, 2, NaN, 4, 5, 6])).toBeUndefined();
expect(midRange([1, 2, 3, 4, 5, 6, 7, 7, 7.12, 7, 0.12])).toBe(3.62);
expect(midRange(["a", "c", "b", "d"])).toBeUndefined();
expect(midRange(['a', 'c', 'b', 'd'])).toBeUndefined();
expect(midRange([])).toBeUndefined();
expect(midRange(3)).toBeUndefined();
expect(midRange([30])).toBe(30);
testUndefinedWithNullable(midRange);
});

test("Mode", () => {
test('Mode', () => {
expect(mode([1, 2, 3, 3, 4, 4])).toEqual([3, 4]);
expect(mode([1, 1, 2])).toEqual([1]);
expect(mode(["a", "c", "b", "d", "c"])).toEqual(["c"]);
expect(mode(["a", 2.5, "b", 5.75])).toEqual(["a", 2.5, "b", 5.75]);
expect(mode(['a', 'c', 'b', 'd', 'c'])).toEqual(['c']);
expect(mode(['a', 2.5, 'b', 5.75])).toEqual(['a', 2.5, 'b', 5.75]);
expect(mode([NaN, 2.5, 2.5, 5.75])).toEqual([2.5]);
expect(mode([])).toBeUndefined();
expect(mode(3)).toBeUndefined();
expect(mode([3])).toEqual([3]);
testUndefinedWithNullable(mode);
});

test("Quartiles", () => {
test('Quartiles', () => {
expect(quartiles([6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49])).toEqual([
15,
40,
43
43,
]);
expect(quartiles([7, 15, 36, 39, 40, 41])).toEqual([15, 37.5, 40]);
expect(quartiles([2, 2, 3, 4])).toEqual([2, 2.5, 3.5]);
expect(quartiles(["a", 2.5, "b", 5.75, 5])).toBeUndefined();
expect(quartiles(['a', 2.5, 'b', 5.75, 5])).toBeUndefined();
expect(quartiles([NaN, 2.5, 3, 5.75, 12])).toBeUndefined();
expect(quartiles([])).toBeUndefined();
expect(quartiles(3)).toBeUndefined();
Expand All @@ -186,18 +187,18 @@ describe("Averages and measures of central location", () => {
testUndefinedWithNullable(quartiles);
});

test("Root Mean Square (rms)", () => {
test('Root Mean Square (rms)', () => {
expect(rms([1, 2, 1, 3])).toBe(1.9364916731037085);
expect(rms([4, 1, 1, 3])).toBe(2.598076211353316);
expect(rms(["a", 2.5, "b", 5.75, 5])).toBeUndefined();
expect(rms(['a', 2.5, 'b', 5.75, 5])).toBeUndefined();
expect(rms([NaN, 2.5, 3, 5.75, 12])).toBeUndefined();
expect(rms([])).toBeUndefined();
expect(rms(3)).toBeUndefined();
expect(rms([3])).toBe(3);
testUndefinedWithNullable(rms);
});

test("Percentile", () => {
test('Percentile', () => {
let arr = [13, 20, 8, 8, 7, 10, 3, 15, 16, 6];
expect(percentile(arr, 0.25)).toBe(7);
expect(percentile(arr, 0.5)).toBe(8);
Expand All @@ -220,11 +221,22 @@ describe("Averages and measures of central location", () => {
expect(percentile(arr, 0.75)).toBe(15);
expect(percentile(arr, 1.0)).toBe(20);

expect(percentile(["a", 2.5, "b", 5.75, 5], 0.25)).toBeUndefined();
expect(percentile(['a', 2.5, 'b', 5.75, 5], 0.25)).toBeUndefined();
expect(percentile([NaN, 2.5, 3, 5.75, 12], 0.5)).toBeUndefined();
expect(percentile([], 0.2)).toBeUndefined();
expect(percentile(3, 0.5)).toBeUndefined();
expect(percentile([3], 0.5)).toBe(3);
testUndefinedWithNullable(percentile);
});

test('Kurtosis', () => {
const arr = [0, 3, 4, 1, 2, 3, 0, 2, 1, 3, 2, 0, 2, 2, 3, 2, 5, 2, 3, 999];
expect(kurtosis(arr).toFixed(2)).toEqual('15.05');
expect(kurtosis(['a', 2.5, 'b', 5.75, 5])).toBeUndefined();
expect(kurtosis([NaN, 2.5, 3, 5.75, 12])).toBeUndefined();
expect(kurtosis([])).toBeUndefined();
expect(kurtosis(3)).toBeUndefined();
expect(Number.isNaN(kurtosis([3]))).toBeTruthy();
testUndefinedWithNullable(kurtosis);
});
});
Loading

0 comments on commit ce088d3

Please sign in to comment.