Skip to content

Commit 027afa0

Browse files
authored
Added weights to TSQuadraticSeries
1 parent b42620d commit 027afa0

File tree

1 file changed

+58
-25
lines changed

1 file changed

+58
-25
lines changed

app/engine/utils/TSQuadraticSeries.js

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
'use strict'
2-
/*
3-
Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor
4-
*/
52
/**
6-
* The FullTSQuadraticSeries is a datatype that represents a Quadratic Series. It allows
3+
* @copyright [OpenRowingMonitor]{@link https://github.com/JaapvanEkris/openrowingmonitor}
4+
*
5+
* @file The FullTSQuadraticSeries is a datatype that represents a Quadratic Series. It allows
76
* values to be retrieved (like a FiFo buffer, or Queue) but it also includes
87
* a Theil-Sen Quadratic Regressor to determine the coefficients of this dataseries.
98
*
@@ -33,11 +32,13 @@ import loglevel from 'loglevel'
3332
const log = loglevel.getLogger('RowingEngine')
3433

3534
/**
36-
* @param {integer} the maximum length of the quadratic series, 0 for unlimited
35+
* @param {integer} maxSeriesLength - the maximum length of the quadratic series, 0 for unlimited
3736
*/
3837
export function createTSQuadraticSeries (maxSeriesLength = 0) {
3938
const X = createSeries(maxSeriesLength)
4039
const Y = createSeries(maxSeriesLength)
40+
const weight = createSeries(maxSeriesLength)
41+
const WY = createSeries(maxSeriesLength)
4142
const A = createLabelledBinarySearchTree()
4243
const linearResidu = createTSLinearSeries(maxSeriesLength)
4344
let _A = 0
@@ -47,12 +48,13 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
4748
let _goodnessOfFit = 0
4849

4950
/**
50-
* @param {float} the x value of the datapoint
51-
* @param {float} the y value of the datapoint
51+
* @param {float} x - the x value of the datapoint
52+
* @param {float} y - the y value of the datapoint
53+
* @param {float} w - the weight of the datapoint (defaults to 1)
5254
* Invariant: BinrySearchTree A contains all calculated a's (as in the general formula y = a * x^2 + b * x + c),
5355
* where the a's are labeled in the BinarySearchTree with their Xi when they BEGIN in the point (Xi, Yi)
5456
*/
55-
function push (x, y) {
57+
function push (x, y, w = 1) {
5658
if (x === undefined || isNaN(x) || y === undefined || isNaN(y)) { return }
5759

5860
if (maxSeriesLength > 0 && X.length() >= maxSeriesLength) {
@@ -63,6 +65,8 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
6365

6466
X.push(x)
6567
Y.push(y)
68+
weight.push(w)
69+
WY.push(w * y)
6670

6771
// Calculate the coefficient a for the new interval by adding the newly added datapoint
6872
let i = 0
@@ -72,15 +76,19 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
7276
case (X.length() >= 3):
7377
// There are now at least three datapoints in the X and Y arrays, so let's calculate the A portion belonging for the new datapoint via Quadratic Theil-Sen regression
7478
// First we calculate the A for the formula
79+
let combinedweight = 0
80+
let coeffA = 1
7581
while (i < X.length() - 2) {
7682
j = i + 1
7783
while (j < X.length() - 1) {
78-
A.push(X.get(i), calculateA(i, j, X.length() - 1), 1)
84+
combinedweight = weight.get(i) * weight.get(j) * w
85+
coeffA = calculateA(i, j, X.length() - 1)
86+
A.push(X.get(i), coeffA, combinedweight)
7987
j++
8088
}
8189
i++
8290
}
83-
_A = A.median()
91+
_A = A.weightedMedian()
8492

8593
// We invalidate the linearResidu, B, C, and goodnessOfFit, as this will trigger a recalculate when they are needed
8694
linearResidu.reset()
@@ -99,7 +107,7 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
99107
}
100108

101109
/**
102-
* @param {integer} the position in the flank of the requested value (default = 0)
110+
* @param {integer} position - the position in the flank of the requested value (default = 0)
103111
* @returns {float} the firdt derivative of the quadratic function y = a x^2 + b x + c
104112
*/
105113
function firstDerivativeAtPosition (position = 0) {
@@ -112,7 +120,7 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
112120
}
113121

114122
/**
115-
* @param {integer} the position in the flank of the requested value (default = 0)
123+
* @param {integer} position - the position in the flank of the requested value (default = 0)
116124
* @returns {float} the second derivative of the quadratic function y = a x^2 + b x + c
117125
*/
118126
function secondDerivativeAtPosition (position = 0) {
@@ -124,7 +132,7 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
124132
}
125133

126134
/**
127-
* @param {float} the x value of the requested value
135+
* @param {float} x - the x value of the requested value
128136
* @returns {float} the slope of the linear function
129137
*/
130138
function slope (x) {
@@ -137,22 +145,22 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
137145
}
138146

139147
/**
140-
* @returns {float} the coefficient a of the quadratic function y = a x^2 + b x + c
148+
* @returns {float} the (quadratic) coefficient a of the quadratic function y = a x^2 + b x + c
141149
*/
142150
function coefficientA () {
143151
return _A
144152
}
145153

146154
/**
147-
* @returns {float} the coefficient b of the quadratic function y = a x^2 + b x + c
155+
* @returns {float} the (linear) coefficient b of the quadratic function y = a x^2 + b x + c
148156
*/
149157
function coefficientB () {
150158
calculateB()
151159
return _B
152160
}
153161

154162
/**
155-
* @returns {float} the coefficient c of the quadratic function y = a x^2 + b x + c
163+
* @returns {float} the (intercept) coefficient c of the quadratic function y = a x^2 + b x + c
156164
*/
157165
function coefficientC () {
158166
calculateB()
@@ -177,33 +185,38 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
177185
}
178186

179187
/**
180-
* @returns {float} the R^2 as a goodness of fit indicator
188+
* @returns {float} the R^2 as a global goodness of fit indicator
181189
*/
182190
function goodnessOfFit () {
183191
let i = 0
184192
let sse = 0
185193
if (_goodnessOfFit === null) {
194+
calculateB()
195+
calculateC()
186196
if (X.length() >= 3) {
187197
_sst = 0
198+
const weightedAverageY = WY.sum() / weight.sum()
199+
188200
while (i < X.length()) {
189-
sse += Math.pow((Y.get(i) - projectX(X.get(i))), 2)
190-
_sst += Math.pow((Y.get(i) - Y.average()), 2)
201+
sse += weight.get(i) * Math.pow(Y.get(i) - projectX(X.get(i)), 2)
202+
_sst += weight.get(i) * Math.pow(Y.get(i) - weightedAverageY, 2)
191203
i++
192204
}
205+
193206
switch (true) {
194207
case (sse === 0):
195208
_goodnessOfFit = 1
196209
break
197210
case (sse > _sst):
198211
// This is a pretty bad fit as the error is bigger than just using the line for the average y as intercept
199-
_goodnessOfFit = 0
212+
_goodnessOfFit = 0.01
200213
break
201214
case (_sst !== 0):
202215
_goodnessOfFit = 1 - (sse / _sst)
203216
break
204217
default:
205218
// When _SST = 0, R2 isn't defined
206-
_goodnessOfFit = 0
219+
_goodnessOfFit = 0.01
207220
}
208221
} else {
209222
_goodnessOfFit = 0
@@ -229,14 +242,14 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
229242
break
230243
case (squaredError > _sst):
231244
// This is a pretty bad fit as the error is bigger than just using the line for the average y as intercept
232-
return 0
245+
return 0.01
233246
break
234247
case (_sst !== 0):
235248
return Math.min(Math.max(1 - ((squaredError * X.length()) / _sst), 0), 1)
236249
break
237250
default:
238251
// When _SST = 0, localGoodnessOfFit isn't defined
239-
return 0
252+
return 0.01
240253
}
241254
/* eslint-enable no-unreachable */
242255
} else {
@@ -245,7 +258,7 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
245258
}
246259

247260
/**
248-
* @param {float} the x value to be projected
261+
* @param {float} x - the x value to be projected
249262
* @returns {float} the resulting y value when projected via the linear function
250263
*/
251264
function projectX (x) {
@@ -258,6 +271,12 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
258271
}
259272
}
260273

274+
/**
275+
* @param {integer} pointOne - The position in the series of the first datapoint used for the quadratic coefficient calculation
276+
* @param {integer} pointTwo - The position in the series of the second datapoint used for the quadratic coefficient calculation
277+
* @param {integer} pointThree - The position in the series of the third datapoint used for the quadratic coefficient calculation
278+
* @returns {float} the coefficient A of the linear function
279+
*/
261280
function calculateA (pointOne, pointTwo, pointThree) {
262281
let result = 0
263282
if (X.get(pointOne) !== X.get(pointTwo) && X.get(pointOne) !== X.get(pointThree) && X.get(pointTwo) !== X.get(pointThree)) {
@@ -270,6 +289,9 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
270289
}
271290
}
272291

292+
/**
293+
* @description This helper function calculates the slope of the linear residu and stores it in _B
294+
*/
273295
function calculateB () {
274296
// Calculate all the linear slope for the newly added point and the newly calculated A
275297
// This function is only called when a linear slope is really needed, as this saves a lot of CPU cycles when only a slope suffices
@@ -283,6 +305,9 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
283305
}
284306
}
285307

308+
/**
309+
* @description This helper function calculates the intercept of the linear residu and stores it in _C
310+
*/
286311
function calculateC () {
287312
// Calculate all the intercept for the newly added point and the newly calculated A
288313
// This function is only called when a linear intercept is really needed, as this saves a lot of CPU cycles when only a slope suffices
@@ -296,12 +321,15 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
296321
}
297322
}
298323

324+
/**
325+
* @description This helper function fills the linear residu
326+
*/
299327
function fillLinearResidu () {
300328
// To calculate the B and C via Linear regression over the residu, we need to fill it if empty
301329
if (linearResidu.length() === 0) {
302330
let i = 0
303331
while (i < X.length()) {
304-
linearResidu.push(X.get(i), Y.get(i) - (_A * Math.pow(X.get(i), 2)))
332+
linearResidu.push(X.get(i), Y.get(i) - (_A * Math.pow(X.get(i), 2)), weight.get(i))
305333
i++
306334
}
307335
}
@@ -314,11 +342,16 @@ export function createTSQuadraticSeries (maxSeriesLength = 0) {
314342
return (X.length() >= 3)
315343
}
316344

345+
/**
346+
* @description This function is used for clearing data and state
347+
*/
317348
function reset () {
318349
if (X.length() > 0) {
319350
// There is something to reset
320351
X.reset()
321352
Y.reset()
353+
weight.reset()
354+
WY.reset()
322355
A.reset()
323356
linearResidu.reset()
324357
_A = 0

0 commit comments

Comments
 (0)