From 4884b8d37eebb90a39a8295f1b61cf822dcf0869 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Mon, 29 May 2023 15:08:56 +0200 Subject: [PATCH 1/1] refactor(simulator): factor out statistic helpers MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- src/performance/PerformanceStatistics.ts | 9 ++-- src/utils/StatisticUtils.ts | 55 ++++++++++++++++++++++++ src/utils/Utils.ts | 55 ------------------------ src/utils/index.ts | 1 + test/utils/CircularArrayTest.ts | 2 +- test/utils/StatisticUtilsTest.ts | 29 +++++++++++++ test/utils/UtilsTest.ts | 24 ----------- 7 files changed, 92 insertions(+), 83 deletions(-) create mode 100644 src/utils/StatisticUtils.ts create mode 100644 test/utils/StatisticUtilsTest.ts diff --git a/src/performance/PerformanceStatistics.ts b/src/performance/PerformanceStatistics.ts index cd0ff3df..565ef6e7 100644 --- a/src/performance/PerformanceStatistics.ts +++ b/src/performance/PerformanceStatistics.ts @@ -18,6 +18,9 @@ import { Utils, buildPerformanceStatisticsMessage, logger, + median, + nthPercentile, + stdDeviation, } from '../utils'; export class PerformanceStatistics { @@ -231,19 +234,19 @@ export class PerformanceStatistics { timestamp: entry.startTime, value: entry.duration, })); - this.statistics.statisticsData.get(entryName).medTimeMeasurement = Utils.median( + this.statistics.statisticsData.get(entryName).medTimeMeasurement = median( this.extractTimeSeriesValues( this.statistics.statisticsData.get(entryName).timeMeasurementSeries ) ); this.statistics.statisticsData.get(entryName).ninetyFiveThPercentileTimeMeasurement = - Utils.percentile( + nthPercentile( this.extractTimeSeriesValues( this.statistics.statisticsData.get(entryName).timeMeasurementSeries ), 95 ); - this.statistics.statisticsData.get(entryName).stdDevTimeMeasurement = Utils.stdDeviation( + this.statistics.statisticsData.get(entryName).stdDevTimeMeasurement = stdDeviation( this.extractTimeSeriesValues( this.statistics.statisticsData.get(entryName).timeMeasurementSeries ) diff --git a/src/utils/StatisticUtils.ts b/src/utils/StatisticUtils.ts new file mode 100644 index 00000000..bb9a8470 --- /dev/null +++ b/src/utils/StatisticUtils.ts @@ -0,0 +1,55 @@ +import { Utils } from './Utils'; + +export const median = (dataSet: number[]): number => { + if (Utils.isEmptyArray(dataSet)) { + return 0; + } + if (Array.isArray(dataSet) === true && dataSet.length === 1) { + return dataSet[0]; + } + const sortedDataSet = dataSet.slice().sort((a, b) => a - b); + return ( + (sortedDataSet[(sortedDataSet.length - 1) >> 1] + sortedDataSet[sortedDataSet.length >> 1]) / 2 + ); +}; + +// TODO: use order statistics tree https://en.wikipedia.org/wiki/Order_statistic_tree +export const nthPercentile = (dataSet: number[], percentile: number): number => { + if (percentile < 0 && percentile > 100) { + throw new RangeError('Percentile is not between 0 and 100'); + } + if (Utils.isEmptyArray(dataSet)) { + return 0; + } + const sortedDataSet = dataSet.slice().sort((a, b) => a - b); + if (percentile === 0 || sortedDataSet.length === 1) { + return sortedDataSet[0]; + } + if (percentile === 100) { + return sortedDataSet[sortedDataSet.length - 1]; + } + const percentileIndexBase = (percentile / 100) * (sortedDataSet.length - 1); + const percentileIndexInteger = Math.floor(percentileIndexBase); + if (!Utils.isNullOrUndefined(sortedDataSet[percentileIndexInteger + 1])) { + return ( + sortedDataSet[percentileIndexInteger] + + (percentileIndexBase - percentileIndexInteger) * + (sortedDataSet[percentileIndexInteger + 1] - sortedDataSet[percentileIndexInteger]) + ); + } + return sortedDataSet[percentileIndexInteger]; +}; + +export const stdDeviation = (dataSet: number[]): number => { + let totalDataSet = 0; + for (const data of dataSet) { + totalDataSet += data; + } + const dataSetMean = totalDataSet / dataSet.length; + let totalGeometricDeviation = 0; + for (const data of dataSet) { + const deviation = data - dataSetMean; + totalGeometricDeviation += deviation * deviation; + } + return Math.sqrt(totalGeometricDeviation / dataSet.length); +}; diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 3d8c73bd..c86afeca 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -334,59 +334,4 @@ export class Utils { } return '(Unknown)'; } - - public static median(dataSet: number[]): number { - if (Utils.isEmptyArray(dataSet)) { - return 0; - } - if (Array.isArray(dataSet) === true && dataSet.length === 1) { - return dataSet[0]; - } - const sortedDataSet = dataSet.slice().sort((a, b) => a - b); - return ( - (sortedDataSet[(sortedDataSet.length - 1) >> 1] + sortedDataSet[sortedDataSet.length >> 1]) / - 2 - ); - } - - // TODO: use order statistics tree https://en.wikipedia.org/wiki/Order_statistic_tree - public static percentile(dataSet: number[], percentile: number): number { - if (percentile < 0 && percentile > 100) { - throw new RangeError('Percentile is not between 0 and 100'); - } - if (Utils.isEmptyArray(dataSet)) { - return 0; - } - const sortedDataSet = dataSet.slice().sort((a, b) => a - b); - if (percentile === 0 || sortedDataSet.length === 1) { - return sortedDataSet[0]; - } - if (percentile === 100) { - return sortedDataSet[sortedDataSet.length - 1]; - } - const percentileIndexBase = (percentile / 100) * (sortedDataSet.length - 1); - const percentileIndexInteger = Math.floor(percentileIndexBase); - if (!Utils.isNullOrUndefined(sortedDataSet[percentileIndexInteger + 1])) { - return ( - sortedDataSet[percentileIndexInteger] + - (percentileIndexBase - percentileIndexInteger) * - (sortedDataSet[percentileIndexInteger + 1] - sortedDataSet[percentileIndexInteger]) - ); - } - return sortedDataSet[percentileIndexInteger]; - } - - public static stdDeviation(dataSet: number[]): number { - let totalDataSet = 0; - for (const data of dataSet) { - totalDataSet += data; - } - const dataSetMean = totalDataSet / dataSet.length; - let totalGeometricDeviation = 0; - for (const data of dataSet) { - const deviation = data - dataSetMean; - totalGeometricDeviation += deviation * deviation; - } - return Math.sqrt(totalGeometricDeviation / dataSet.length); - } } diff --git a/src/utils/index.ts b/src/utils/index.ts index 2506da85..b2374d73 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -24,4 +24,5 @@ export { buildStoppedMessage, } from './MessageChannelUtils'; export { Utils } from './Utils'; +export { median, nthPercentile, stdDeviation } from './StatisticUtils'; export { logger } from './Logger'; diff --git a/test/utils/CircularArrayTest.ts b/test/utils/CircularArrayTest.ts index 0a2609c0..720ca68d 100644 --- a/test/utils/CircularArrayTest.ts +++ b/test/utils/CircularArrayTest.ts @@ -2,7 +2,7 @@ import { expect } from 'expect'; import { CircularArray } from '../../src/utils/CircularArray'; -describe('Circular array test suite', () => { +describe('CircularArray test suite', () => { it('Verify that circular array can be instantiated', () => { const circularArray = new CircularArray(); expect(circularArray).toBeInstanceOf(CircularArray); diff --git a/test/utils/StatisticUtilsTest.ts b/test/utils/StatisticUtilsTest.ts new file mode 100644 index 00000000..2910c32d --- /dev/null +++ b/test/utils/StatisticUtilsTest.ts @@ -0,0 +1,29 @@ +import { expect } from 'expect'; + +import { median, nthPercentile, stdDeviation } from '../../src/utils/StatisticUtils'; + +describe('StatisticUtils test suite', () => { + it('Verify median()', () => { + expect(median([])).toBe(0); + expect(median([0.08])).toBe(0.08); + expect(median([0.25, 4.75, 3.05, 6.04, 1.01, 2.02, 5.03])).toBe(3.05); + expect(median([0.25, 4.75, 3.05, 6.04, 1.01, 2.02])).toBe(2.535); + }); + + it('Verify nthPercentile()', () => { + expect(nthPercentile([], 25)).toBe(0); + expect(nthPercentile([0.08], 50)).toBe(0.08); + const array0 = [0.25, 4.75, 3.05, 6.04, 1.01, 2.02, 5.03]; + expect(nthPercentile(array0, 0)).toBe(0.25); + expect(nthPercentile(array0, 50)).toBe(3.05); + expect(nthPercentile(array0, 80)).toBe(4.974); + expect(nthPercentile(array0, 85)).toBe(5.131); + expect(nthPercentile(array0, 90)).toBe(5.434); + expect(nthPercentile(array0, 95)).toBe(5.736999999999999); + expect(nthPercentile(array0, 100)).toBe(6.04); + }); + + it('Verify stdDeviation()', () => { + expect(stdDeviation([0.25, 4.75, 3.05, 6.04, 1.01, 2.02, 5.03])).toBe(2.0256064851429216); + }); +}); diff --git a/test/utils/UtilsTest.ts b/test/utils/UtilsTest.ts index f5e1259d..5732e57e 100644 --- a/test/utils/UtilsTest.ts +++ b/test/utils/UtilsTest.ts @@ -329,28 +329,4 @@ describe('Utils test suite', () => { expect(Utils.isEmptyObject(new WeakMap())).toBe(false); expect(Utils.isEmptyObject(new WeakSet())).toBe(false); }); - - it('Verify median()', () => { - expect(Utils.median([])).toBe(0); - expect(Utils.median([0.08])).toBe(0.08); - expect(Utils.median([0.25, 4.75, 3.05, 6.04, 1.01, 2.02, 5.03])).toBe(3.05); - expect(Utils.median([0.25, 4.75, 3.05, 6.04, 1.01, 2.02])).toBe(2.535); - }); - - it('Verify percentile()', () => { - expect(Utils.percentile([], 25)).toBe(0); - expect(Utils.percentile([0.08], 50)).toBe(0.08); - const array0 = [0.25, 4.75, 3.05, 6.04, 1.01, 2.02, 5.03]; - expect(Utils.percentile(array0, 0)).toBe(0.25); - expect(Utils.percentile(array0, 50)).toBe(3.05); - expect(Utils.percentile(array0, 80)).toBe(4.974); - expect(Utils.percentile(array0, 85)).toBe(5.131); - expect(Utils.percentile(array0, 90)).toBe(5.434); - expect(Utils.percentile(array0, 95)).toBe(5.736999999999999); - expect(Utils.percentile(array0, 100)).toBe(6.04); - }); - - it('Verify stdDeviation()', () => { - expect(Utils.stdDeviation([0.25, 4.75, 3.05, 6.04, 1.01, 2.02, 5.03])).toBe(2.0256064851429216); - }); }); -- 2.34.1