diff --git a/packages/mymath/src/array/index.spec.ts b/packages/mymath/src/array/index.spec.ts index 874d9f6d..7dc15f97 100644 --- a/packages/mymath/src/array/index.spec.ts +++ b/packages/mymath/src/array/index.spec.ts @@ -1,6 +1,17 @@ import { describe, expect, it, jest } from '@jest/globals' -import { average, product, sum } from './index' +import { + arithmeticMean, + average, + cubicMean, + generalizedMean, + geometricMean, + harmonicMean, + naiveGeneralizedMean, + product, + rootMeanSquare, + sum, +} from './index' describe('sum', () => { it('should return sum of all values', () => { @@ -22,7 +33,7 @@ describe('product', () => { }) }) -describe('average', () => { +describe('average (alias of arithmeticMean)', () => { it('should return average of all values', () => { expect(average([1, 2, 3, 4])).toBe(2.5) }) @@ -34,4 +45,83 @@ describe('average', () => { it('should return 5 for [] and 5', () => { expect(average([], 5)).toBe(5) }) + + it('should return same value as arithmeticMean', () => { + expect(average([1, 2, 3, 4])).toBe(arithmeticMean([1, 2, 3, 4])) + }) +}) + +describe('rootMeanSquare', () => { + it('should return root mean square of all values', () => { + expect(rootMeanSquare([1, 2, 3, 4])).toBeCloseTo(2.7386) + }) + + it('should return 0 for []', () => { + expect(rootMeanSquare([])).toBe(0) + }) +}) + +describe('cubicMean', () => { + it('should return cubic mean of all values', () => { + expect(cubicMean([1, 2, 3, 4])).toBeCloseTo(2.924) + }) + + it('should return 0 for []', () => { + expect(cubicMean([])).toBe(0) + }) +}) + +describe('geometricMean', () => { + it('should return geometric mean of all values', () => { + expect(geometricMean([1, 2, 3, 4])).toBeCloseTo(2.2134) + }) + + it('should return 1 for []', () => { + expect(geometricMean([])).toBe(1) + }) +}) + +describe('harmonicMean', () => { + it('should return harmonic mean of all values', () => { + expect(harmonicMean([1, 2, 3, 4])).toBeCloseTo(1.92) + }) + + it('should return 0 for []', () => { + expect(() => harmonicMean([])).toThrowError( + 'Cannot calculate harmonic mean of empty set' + ) + }) +}) + +describe('generalizedMean', () => { + it('should return generalized mean of all values', () => { + expect(generalizedMean([1, 2, 3, 4], Number.NEGATIVE_INFINITY)).toBe( + Math.min(...[1, 2, 3, 4]) + ) + expect(generalizedMean([1, 2, 3, 4], 0)).toBe(geometricMean([1, 2, 3, 4])) + expect(generalizedMean([1, 2, 3, 4], 1)).toBe(arithmeticMean([1, 2, 3, 4])) + expect(generalizedMean([1, 2, 3, 4], 2)).toBe(rootMeanSquare([1, 2, 3, 4])) + expect(generalizedMean([1, 2, 3, 4], 3)).toBe(cubicMean([1, 2, 3, 4])) + expect(generalizedMean([1, 2, 3, 4], Number.POSITIVE_INFINITY)).toBe( + Math.max(...[1, 2, 3, 4]) + ) + + expect(generalizedMean([], Number.NEGATIVE_INFINITY)).toBe( + Number.POSITIVE_INFINITY + ) + expect(generalizedMean([], 0)).toBe(1) + expect(generalizedMean([], 1)).toBe(0) + expect(generalizedMean([], 2)).toBe(0) + expect(generalizedMean([], 3)).toBe(0) + expect(generalizedMean([], Number.POSITIVE_INFINITY)).toBe( + Number.NEGATIVE_INFINITY + ) + }) + + it('should return same value as naiveGeneralizedMean', () => { + const values = [1, 2, 3, 4] + expect(naiveGeneralizedMean(values, 0.0001)).toBeCloseTo( + geometricMean(values) + ) + }) }) diff --git a/packages/mymath/src/array/index.ts b/packages/mymath/src/array/index.ts index b3a25ef5..ec2dc204 100644 --- a/packages/mymath/src/array/index.ts +++ b/packages/mymath/src/array/index.ts @@ -37,7 +37,7 @@ export const product = (values: number[]): number => { } /** - * Average of all values in the array + * Arithmetic mean (average) of all values in the array * If the array is empty, it returns 0 or the defaultValue * @example * ```ts @@ -48,9 +48,132 @@ export const product = (values: number[]): number => { * @param defaultValue * @returns average of all values */ -export const average = (values: number[], defaultValue?: number): number => { +export const arithmeticMean = ( + values: number[], + defaultValue?: number +): number => { if (values.length === 0) { return defaultValue || 0 } return sum(values) / values.length } + +/** + * Average of all values in the array (alias of arithmeticMean) + * If the array is empty, it returns 0 or the defaultValue + * @example + * ```ts + * import { average } from '@kitsuyui/mymath' + * average([1, 2, 3, 4]) // => 2.5 + * ``` + * @param values + * @param defaultValue + * @returns average of all values + */ +export const average = arithmeticMean + +/** + * Geometric mean of all values in the array + * If the array is empty, it returns 1 (1 is identity element for multiplication) + * + * @example + * ```ts + * import { geometricMean } from '@kitsuyui/mymath' + * geometricMean([1, 2, 3, 4]) // => 2.213363839400643 + * ``` + * @param values + * @returns + */ +export const geometricMean = (values: number[]): number => { + const n = values.length + if (n === 0) return 1 // geometric mean of empty set is 1 + return product(values) ** (1 / n) +} + +/** + * Harmonic mean of all values in the array + * If the array is empty, it throws an error + * + * @example + * ```ts + * import { harmonicMean } from '@kitsuyui/mymath' + * harmonicMean([1, 2, 3, 4]) // => 1.9200000000000004 + * ``` + * @param values + * @returns + */ +export const harmonicMean = (values: number[]): number => { + const n = values.length + if (n === 0) throw new Error('Cannot calculate harmonic mean of empty set') + return n / sum(values.map((value) => 1 / value)) +} + +/** + * root mean square of all values in the array + * If the array is empty, it returns 0 + * @example + * ```ts + * import { rootMeanSquare } from '@kitsuyui/mymath' + * rootMeanSquare([1, 2, 3, 4]) // => 2.7386127875258306 + * ``` + * @param values + * @returns + */ +export const rootMeanSquare = (values: number[]): number => { + return Math.sqrt(arithmeticMean(values.map((value) => value ** 2))) +} + +/** + * Quadratic mean (root mean square) of all values in the array + * If the array is empty, it returns 0 + * @example + * ```ts + * import { quadraticMean } from '@kitsuyui/mymath' + * quadraticMean([1, 2, 3, 4]) // => 2.7386127875258306 + * ``` + * @param values + * @returns + */ +export const cubicMean = (values: number[]): number => { + return Math.cbrt(arithmeticMean(values.map((value) => value ** 3))) +} + +/** + * Aware implementation of generalized mean (Hölder mean) of all values in the array + * @param values + * @param p + * @returns + */ +const awareGeneralizedMean = (values: number[], p: number): number => { + if (p === Number.NEGATIVE_INFINITY) return Math.min(...values) // p -> -∞ means min + if (p === 0) return geometricMean(values) + if (p === 1) return arithmeticMean(values) + if (p === Number.POSITIVE_INFINITY) return Math.max(...values) // p -> +∞ means max + const n = values.length + if (p > 1 && n === 0) return 0 + if (n === 0) throw new Error('Cannot calculate generalized mean of empty set') + return naiveGeneralizedMean(values, p) +} + +/** + * Naive implementation of generalized mean (Hölder mean) of all values in the array + * This function may cause overflow or underflow (zero division) + * @param values + * @param p + * @returns + */ +export const naiveGeneralizedMean = (values: number[], p: number): number => { + return arithmeticMean(values.map((value) => value ** p)) ** (1 / p) +} + +/** + * Generalized mean (Hölder mean) of all values in the array + * If the array is empty, it returns 0 or the defaultValue + * @example + * ```ts + * import { generalizedMean } from '@kitsuyui/mymath' + * generalizedMean([1, 2, 3, 4], 1) // => 2.5 + * generalizedMean([1, 2, 3, 4], 0) // => 2.213363839400643 + * ``` + */ +export const generalizedMean = awareGeneralizedMean diff --git a/packages/mymath/src/index.ts b/packages/mymath/src/index.ts index d1622328..79d53101 100644 --- a/packages/mymath/src/index.ts +++ b/packages/mymath/src/index.ts @@ -1,5 +1,15 @@ export { sigmoid } from './sigmoid' export { logOfBase, logFnOfBase } from './log' -export { sum, product, average } from './array' +export { + sum, + product, + average, + arithmeticMean, + cubicMean, + generalizedMean, + geometricMean, + harmonicMean, + rootMeanSquare, +} from './array' export { clamp } from './clamp' export { softmax, softmaxWithTemperature } from './softmax'