Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add performLinearRegression to stats module in @observerly/fits #52

Merged
merged 1 commit into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 }
}

/*****************************************************************************************************************/
Loading