-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #52 from observerly/feature/stats/performLinearReg…
…ression
- Loading branch information
Showing
3 changed files
with
180 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.' | ||
) | ||
}) | ||
}) | ||
|
||
/*****************************************************************************************************************/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
} | ||
|
||
/*****************************************************************************************************************/ |