Skip to content

Commit

Permalink
Merge pull request #512 from kitsuyui/mymath-implement-holder-mean
Browse files Browse the repository at this point in the history
Implement: various mean functions in mymath
  • Loading branch information
kitsuyui authored Jan 22, 2025
2 parents 34a0710 + 0f567fd commit 86d36b4
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 5 deletions.
94 changes: 92 additions & 2 deletions packages/mymath/src/array/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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)
})
Expand All @@ -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)
)
})
})
127 changes: 125 additions & 2 deletions packages/mymath/src/array/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
12 changes: 11 additions & 1 deletion packages/mymath/src/index.ts
Original file line number Diff line number Diff line change
@@ -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'

0 comments on commit 86d36b4

Please sign in to comment.