Skip to content

Commit e046c56

Browse files
Merge pull request #52 from observerly/feature/stats/performLinearRegression
2 parents 161db47 + 60d42f6 commit e046c56

File tree

3 files changed

+180
-0
lines changed

3 files changed

+180
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*****************************************************************************************************************/
2+
3+
// @author Michael Roberts <michael@observerly.com>
4+
// @package @observerly/fits
5+
// @license Copyright © 2021-2025 observerly
6+
7+
/*****************************************************************************************************************/
8+
9+
import { describe, expect, it } from 'vitest'
10+
11+
import { type Point, performLinearRegression } from '../regression'
12+
13+
/*****************************************************************************************************************/
14+
15+
describe('performLinearRegression', () => {
16+
it('should correctly compute slope and intercept for a simple linear relationship', () => {
17+
const points: Point[] = [
18+
{ x: 0, y: 1 },
19+
{ x: 1, y: 3 },
20+
{ x: 2, y: 5 },
21+
{ x: 3, y: 7 }
22+
]
23+
24+
const { m, c } = performLinearRegression(points)
25+
26+
expect(m).toBeCloseTo(2)
27+
expect(c).toBeCloseTo(1)
28+
})
29+
30+
it('should handle floating point values accurately', () => {
31+
const points: Point[] = [
32+
{ x: 0.5, y: 2.1 },
33+
{ x: 1.5, y: 3.9 },
34+
{ x: 2.5, y: 5.8 },
35+
{ x: 3.5, y: 7.7 }
36+
]
37+
38+
const { m, c } = performLinearRegression(points)
39+
40+
expect(m).toBeCloseTo(1.867, 2)
41+
expect(c).toBeCloseTo(1.135, 2)
42+
})
43+
44+
it('should correctly compute slope and intercept for a vertical line-like data', () => {
45+
const points: Point[] = [
46+
{ x: 1, y: 2 },
47+
{ x: 2, y: 4 },
48+
{ x: 3, y: 6 },
49+
{ x: 4, y: 8 }
50+
]
51+
52+
const { m, c } = performLinearRegression(points)
53+
expect(m).toBeCloseTo(2)
54+
expect(c).toBeCloseTo(0)
55+
})
56+
57+
it('should handle negative slopes correctly', () => {
58+
const points: Point[] = [
59+
{ x: 0, y: 10 },
60+
{ x: 1, y: 8 },
61+
{ x: 2, y: 6 },
62+
{ x: 3, y: 4 }
63+
]
64+
65+
const { m, c } = performLinearRegression(points)
66+
expect(m).toBeCloseTo(-2)
67+
expect(c).toBeCloseTo(10)
68+
})
69+
70+
it('should handle points with zero variance in y', () => {
71+
const points: Point[] = [
72+
{ x: 0, y: 5 },
73+
{ x: 1, y: 5 },
74+
{ x: 2, y: 5 },
75+
{ x: 3, y: 5 }
76+
]
77+
78+
const { m, c } = performLinearRegression(points)
79+
expect(m).toBeCloseTo(0)
80+
expect(c).toBeCloseTo(5)
81+
})
82+
83+
it('should compute correct values for a random set of points', () => {
84+
const points: Point[] = [
85+
{ x: 1, y: 2 },
86+
{ x: 2, y: 3 },
87+
{ x: 3, y: 5 },
88+
{ x: 4, y: 4 },
89+
{ x: 5, y: 6 }
90+
]
91+
92+
const { m, c } = performLinearRegression(points)
93+
expect(m).toBeCloseTo(0.9)
94+
expect(c).toBeCloseTo(1.3)
95+
})
96+
97+
it('should throw an error when no points are provided', () => {
98+
const points: Point[] = []
99+
100+
expect(() => performLinearRegression(points)).toThrow(
101+
'No valid points provided for linear regression.'
102+
)
103+
})
104+
105+
it('should throw an error when all x values are the same', () => {
106+
const points: Point[] = [
107+
{ x: 2, y: 3 },
108+
{ x: 2, y: 4 },
109+
{ x: 2, y: 5 }
110+
]
111+
112+
expect(() => performLinearRegression(points)).toThrow(
113+
'Denominator is zero. Cannot compute linear regression.'
114+
)
115+
})
116+
})
117+
118+
/*****************************************************************************************************************/

src/stats/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
export { mean } from './mean'
1010
export { median } from './median'
11+
export { performLinearRegression, type Point } from './regression'
1112
export { variance } from './variance'
1213

1314
/*****************************************************************************************************************/

src/stats/regression.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*****************************************************************************************************************/
2+
3+
// @author Michael Roberts <michael@observerly>
4+
// @package @observerly/fits
5+
// @license Copyright © 2021-2025 observerly
6+
7+
/*****************************************************************************************************************/
8+
9+
export type Point = {
10+
x: number
11+
y: number
12+
}
13+
14+
/*****************************************************************************************************************/
15+
16+
/**
17+
*
18+
* performLinearRegression
19+
*
20+
* Calculates the linear regression (slope and intercept) for a set of points.
21+
* This is crucial for identifying the trend within the pixel data, enabling effective contrast adjustment.
22+
*
23+
* @param points: Array of points containing x and y coordinates.
24+
* @returns: An object containing the y-intercept (`c`) and the slope (`m`) of the fitted line.
25+
* @throws Will throw an error if there is insufficient variation in the x-values.
26+
*/
27+
export function performLinearRegression(points: Point[]): { m: number; c: number } {
28+
const n = points.length
29+
30+
if (n === 0) {
31+
throw new Error('No valid points provided for linear regression.')
32+
}
33+
34+
// Aggregate sums required for calculating slope and intercept using reduce for immutability and clarity:
35+
const { sumX, sumY, sumXY, sumX2 } = points.reduce(
36+
(acc, { x, y }) => ({
37+
sumX: acc.sumX + x,
38+
sumY: acc.sumY + y,
39+
sumXY: acc.sumXY + x * y,
40+
sumX2: acc.sumX2 + x ** 2
41+
}),
42+
{ sumX: 0, sumY: 0, sumXY: 0, sumX2: 0 }
43+
)
44+
45+
// Calculate the denominator to ensure there is enough variation in x-values for a valid regression:
46+
const denominator = n * sumX2 - sumX ** 2
47+
48+
if (denominator === 0) {
49+
throw new Error('Denominator is zero. Cannot compute linear regression.')
50+
}
51+
52+
// Compute the slope (m) of the best-fit line:
53+
const m = (n * sumXY - sumX * sumY) / denominator
54+
55+
// Compute the y-intercept (c) of the best-fit line:
56+
const c = (sumY - m * sumX) / n
57+
58+
return { m, c }
59+
}
60+
61+
/*****************************************************************************************************************/

0 commit comments

Comments
 (0)