From 69cb82cf039fc56f04f2c17fe91d372c17c5c205 Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Mon, 12 Aug 2024 18:56:09 +0200 Subject: [PATCH] feat: xBoxPlot has an option to calculate outliers (#256) --- src/x/__tests__/xBoxPlot.test.ts | 31 +++++++++++++++++++++++++++++++ src/x/xBoxPlot.ts | 27 ++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/x/__tests__/xBoxPlot.test.ts b/src/x/__tests__/xBoxPlot.test.ts index 0d8edb52..8b0e18ac 100644 --- a/src/x/__tests__/xBoxPlot.test.ts +++ b/src/x/__tests__/xBoxPlot.test.ts @@ -10,6 +10,7 @@ test('test xBoxPlot even', () => { q3: 8.5, min: 0, max: 11, + outliers: [], }); }); @@ -21,6 +22,7 @@ test('test xBoxPlot even small', () => { q3: 4, min: 0, max: 5, + outliers: [], }); }); @@ -32,6 +34,7 @@ test('test xBoxPlot odd', () => { q3: 8, min: 0, max: 10, + outliers: [], }); }); @@ -43,6 +46,7 @@ test('test xBoxPlot odd small', () => { q3: 3.5, min: 0, max: 4, + outliers: [], }); }); @@ -69,6 +73,7 @@ test('test xBoxPlot with one element', () => { q3: 42, min: 42, max: 42, + outliers: [], }); }); @@ -81,6 +86,7 @@ test('test xBoxPlot with 2 elements', () => { q3: 44, min: 40, max: 44, + outliers: [], }); }); @@ -93,5 +99,30 @@ test('test xBoxPlot with 3 elements', () => { q3: 44, min: 40, max: 44, + outliers: [], + }); +}); + +test('outliers', () => { + const array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 100]; + expect(xBoxPlot(array)).toStrictEqual({ + q1: 2, + median: 5, + q3: 8, + min: 0, + max: 100, + outliers: [], + }); +}); + +test('outliers', () => { + const array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 100]; + expect(xBoxPlot(array, { calculateOutliers: true })).toStrictEqual({ + q1: 2, + median: 5, + q3: 8, + min: 0, + max: 9, + outliers: [100], }); }); diff --git a/src/x/xBoxPlot.ts b/src/x/xBoxPlot.ts index 2d0248f7..6eeef07d 100644 --- a/src/x/xBoxPlot.ts +++ b/src/x/xBoxPlot.ts @@ -6,6 +6,12 @@ export interface XBoxPlotOptions { * @default false */ allowSmallArray?: boolean; + + /** + * Calculate outliers (value < min-1.5IQR or value > max+1.5IQR). The min and max are recalculated without the outliers. + * @default false + */ + calculateOutliers?: boolean; } export interface XBoxPlot { @@ -14,6 +20,7 @@ export interface XBoxPlot { q3: number; min: number; max: number; + outliers: number[]; } /** @@ -25,7 +32,7 @@ export function xBoxPlot( array: NumberArray, options: XBoxPlotOptions = {}, ): XBoxPlot { - const { allowSmallArray = false } = options; + const { allowSmallArray = false, calculateOutliers = false } = options; if (array.length < 5) { if (allowSmallArray) { if (array.length === 0) { @@ -46,6 +53,7 @@ export function xBoxPlot( q3: 0, min: array[0], max: array.at(-1) as number, + outliers: [], }; let q1max, q3min; if (array.length % 2 === 1) { @@ -68,5 +76,22 @@ export function xBoxPlot( const middleOver = (array.length + q3min) / 2; info.q3 = (array[middleOver] + array[middleOver - 1]) / 2; } + + if (calculateOutliers) { + const iqr = info.q3 - info.q1; + const min = info.q1 - 1.5 * iqr; + const max = info.q3 + 1.5 * iqr; + // we need to recalculate the min and the max because they could be outliers + info.min = info.median; + info.max = info.median; + for (const value of array) { + if (value < min || value > max) { + info.outliers.push(value); + } else { + if (value < info.min) info.min = value; + if (value > info.max) info.max = value; + } + } + } return info; }