From ce088d37918dd85fe9fdfb642d18b34fcb01c990 Mon Sep 17 00:00:00 2001 From: Boris Tane Date: Mon, 6 May 2019 12:53:27 +0100 Subject: [PATCH] Adds support for sample curotis. Fixes #23 --- README.md | 18 +++++++++ src/central.js | 16 +++++++- src/utils.js | 20 +++++++++- test/central.test.js | 92 +++++++++++++++++++++++++------------------- test/utils.test.js | 42 ++++++++++++++++++-- 5 files changed, 141 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 2dcb5c5..89c75ea 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ npm i stat-methods - [mode](#mode) - [rms](#rms) - [percentile](#percentile) + - [kurtosis](#kurtosis) 2. [Measures of spread](#Measures-of-spread) @@ -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. @@ -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. diff --git a/src/central.js b/src/central.js index b84eedd..e08589c 100644 --- a/src/central.js +++ b/src/central.js @@ -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. @@ -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, diff --git a/src/utils.js b/src/utils.js index 7f2fde8..4d182db 100644 --- a/src/utils.js +++ b/src/utils.js @@ -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); } /** @@ -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, }; diff --git a/test/central.test.js b/test/central.test.js index 006e54f..9b115d6 100644 --- a/test/central.test.js +++ b/test/central.test.js @@ -1,4 +1,4 @@ -import { testUndefinedWithNullable } from "./utils"; +import { testUndefinedWithNullable } from './utils'; import { mean, harmonicMean, @@ -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(); @@ -27,11 +28,11 @@ 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(); @@ -39,14 +40,14 @@ describe("Averages and measures of central location", () => { 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(); @@ -54,12 +55,12 @@ describe("Averages and measures of central location", () => { 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(); @@ -67,41 +68,41 @@ describe("Averages and measures of central location", () => { 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( @@ -127,14 +128,14 @@ 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(); @@ -142,25 +143,25 @@ describe("Averages and measures of central location", () => { 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(); @@ -168,15 +169,15 @@ describe("Averages and measures of central location", () => { 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(); @@ -186,10 +187,10 @@ 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(); @@ -197,7 +198,7 @@ describe("Averages and measures of central location", () => { 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); @@ -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); + }); }); diff --git a/test/utils.test.js b/test/utils.test.js index 7a1e3b3..f7c275c 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -3,6 +3,7 @@ import { getAllIndexes, nthRoot, kahanSum, + nthMomentAboutMean, } from '../src/utils'; describe('Descriptive Statistics', () => { @@ -33,14 +34,47 @@ describe('Descriptive Statistics', () => { testUndefinedWithNullable(nthRoot); }); - test("Kahan Sum", () => { + test('Kahan Sum', () => { expect(kahanSum([1, 2, 3, 4])).toBe(10); - expect(kahanSum([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7])).toBe(15.3); + expect( + kahanSum([ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1.0, + 1.1, + 1.2, + 1.3, + 1.4, + 1.5, + 1.6, + 1.7, + ]), + ).toBe(15.3); expect(kahanSum([NaN, 2, 3, 4])).toBeUndefined(); expect(kahanSum(['a', 2, 3, 4])).toBeUndefined(); - expect(kahanSum(["hello", 3, 4, 5])).toBeUndefined(); + expect(kahanSum(['hello', 3, 4, 5])).toBeUndefined(); expect(kahanSum([3])).toBe(3); expect(kahanSum([5, 8, 1.2, null])).toBeUndefined(); - }) + }); + test('nth moment about the mean', () => { + expect( + nthMomentAboutMean( + [0, 3, 4, 1, 2, 3, 0, 2, 1, 3, 2, 0, 2, 2, 3, 2, 5, 2, 3, 999], + 2, + ), + ).toBe(47207.0475); + expect(nthMomentAboutMean([NaN, 2, 3, 4])).toBeUndefined(); + expect(nthMomentAboutMean(['a', 2, 3, 4])).toBeUndefined(); + expect(nthMomentAboutMean(['hello', 3, 4, 5])).toBeUndefined(); + expect(nthMomentAboutMean([3], 2)).toBe(0); + expect(nthMomentAboutMean([5, 8, 1.2, null])).toBeUndefined(); + }); });