Skip to content

Commit

Permalink
Merge pull request #52 from observerly/feature/stats/performLinearReg…
Browse files Browse the repository at this point in the history
…ression
  • Loading branch information
michealroberts authored Jan 8, 2025
2 parents 161db47 + 60d42f6 commit e046c56
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 0 deletions.
118 changes: 118 additions & 0 deletions src/stats/__tests__/regression.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*****************************************************************************************************************/

// @author Michael Roberts <[email protected]>
// @package @observerly/fits
// @license Copyright © 2021-2025 observerly

/*****************************************************************************************************************/

import { describe, expect, it } from 'vitest'

import { type Point, performLinearRegression } from '../regression'

/*****************************************************************************************************************/

describe('performLinearRegression', () => {
it('should correctly compute slope and intercept for a simple linear relationship', () => {
const points: Point[] = [
{ x: 0, y: 1 },
{ x: 1, y: 3 },
{ x: 2, y: 5 },
{ x: 3, y: 7 }
]

const { m, c } = performLinearRegression(points)

expect(m).toBeCloseTo(2)
expect(c).toBeCloseTo(1)
})

it('should handle floating point values accurately', () => {
const points: Point[] = [
{ x: 0.5, y: 2.1 },
{ x: 1.5, y: 3.9 },
{ x: 2.5, y: 5.8 },
{ x: 3.5, y: 7.7 }
]

const { m, c } = performLinearRegression(points)

expect(m).toBeCloseTo(1.867, 2)
expect(c).toBeCloseTo(1.135, 2)
})

it('should correctly compute slope and intercept for a vertical line-like data', () => {
const points: Point[] = [
{ x: 1, y: 2 },
{ x: 2, y: 4 },
{ x: 3, y: 6 },
{ x: 4, y: 8 }
]

const { m, c } = performLinearRegression(points)
expect(m).toBeCloseTo(2)
expect(c).toBeCloseTo(0)
})

it('should handle negative slopes correctly', () => {
const points: Point[] = [
{ x: 0, y: 10 },
{ x: 1, y: 8 },
{ x: 2, y: 6 },
{ x: 3, y: 4 }
]

const { m, c } = performLinearRegression(points)
expect(m).toBeCloseTo(-2)
expect(c).toBeCloseTo(10)
})

it('should handle points with zero variance in y', () => {
const points: Point[] = [
{ x: 0, y: 5 },
{ x: 1, y: 5 },
{ x: 2, y: 5 },
{ x: 3, y: 5 }
]

const { m, c } = performLinearRegression(points)
expect(m).toBeCloseTo(0)
expect(c).toBeCloseTo(5)
})

it('should compute correct values for a random set of points', () => {
const points: Point[] = [
{ x: 1, y: 2 },
{ x: 2, y: 3 },
{ x: 3, y: 5 },
{ x: 4, y: 4 },
{ x: 5, y: 6 }
]

const { m, c } = performLinearRegression(points)
expect(m).toBeCloseTo(0.9)
expect(c).toBeCloseTo(1.3)
})

it('should throw an error when no points are provided', () => {
const points: Point[] = []

expect(() => performLinearRegression(points)).toThrow(
'No valid points provided for linear regression.'
)
})

it('should throw an error when all x values are the same', () => {
const points: Point[] = [
{ x: 2, y: 3 },
{ x: 2, y: 4 },
{ x: 2, y: 5 }
]

expect(() => performLinearRegression(points)).toThrow(
'Denominator is zero. Cannot compute linear regression.'
)
})
})

/*****************************************************************************************************************/
1 change: 1 addition & 0 deletions src/stats/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

export { mean } from './mean'
export { median } from './median'
export { performLinearRegression, type Point } from './regression'
export { variance } from './variance'

/*****************************************************************************************************************/
61 changes: 61 additions & 0 deletions src/stats/regression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*****************************************************************************************************************/

// @author Michael Roberts <michael@observerly>
// @package @observerly/fits
// @license Copyright © 2021-2025 observerly

/*****************************************************************************************************************/

export type Point = {
x: number
y: number
}

/*****************************************************************************************************************/

/**
*
* performLinearRegression
*
* Calculates the linear regression (slope and intercept) for a set of points.
* This is crucial for identifying the trend within the pixel data, enabling effective contrast adjustment.
*
* @param points: Array of points containing x and y coordinates.
* @returns: An object containing the y-intercept (`c`) and the slope (`m`) of the fitted line.
* @throws Will throw an error if there is insufficient variation in the x-values.
*/
export function performLinearRegression(points: Point[]): { m: number; c: number } {
const n = points.length

if (n === 0) {
throw new Error('No valid points provided for linear regression.')
}

// Aggregate sums required for calculating slope and intercept using reduce for immutability and clarity:
const { sumX, sumY, sumXY, sumX2 } = points.reduce(
(acc, { x, y }) => ({
sumX: acc.sumX + x,
sumY: acc.sumY + y,
sumXY: acc.sumXY + x * y,
sumX2: acc.sumX2 + x ** 2
}),
{ sumX: 0, sumY: 0, sumXY: 0, sumX2: 0 }
)

// Calculate the denominator to ensure there is enough variation in x-values for a valid regression:
const denominator = n * sumX2 - sumX ** 2

if (denominator === 0) {
throw new Error('Denominator is zero. Cannot compute linear regression.')
}

// Compute the slope (m) of the best-fit line:
const m = (n * sumXY - sumX * sumY) / denominator

// Compute the y-intercept (c) of the best-fit line:
const c = (sumY - m * sumX) / n

return { m, c }
}

/*****************************************************************************************************************/

0 comments on commit e046c56

Please sign in to comment.