Fix of an array processing error in the Theil-Sen regressor (#40)

Update to fix an error in both Theil-Sen regressors, as the arrays used were not processed completely. Thanks to @Abasz for reporting.
This commit is contained in:
Jaap van Ekris 2024-02-15 11:15:07 +01:00 committed by GitHub
parent f1b621fde5
commit 414372ef23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 201 additions and 105 deletions

View File

@ -1,6 +1,6 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/jaapvanekris/openrowingmonitor
Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor
*/
import { test } from 'uvu'
import * as assert from 'uvu/assert'
@ -90,9 +90,9 @@ test('Correct Flywheel behaviour for a noisefree stroke', () => {
testDeltaTime(flywheel, 0.011062297)
testSpinningTime(flywheel, 0.077918634)
testAngularPosition(flywheel, 8.377580409572781)
testAngularVelocity(flywheel, 94.76231358849583)
testAngularAcceleration(flywheel, 28.980404808837132)
testTorque(flywheel, 3.975668304221995)
testAngularVelocity(flywheel, 94.77498684553687)
testAngularAcceleration(flywheel, 28.980405331480235)
testTorque(flywheel, 3.975932584148498)
testDragFactor(flywheel, 0.00011)
testIsDwelling(flywheel, false)
testIsUnpowered(flywheel, false)
@ -115,9 +115,9 @@ test('Correct Flywheel behaviour for a noisefree stroke', () => {
testDeltaTime(flywheel, 0.010722165)
testSpinningTime(flywheel, 0.23894732900000007)
testAngularPosition(flywheel, 24.085543677521745)
testAngularVelocity(flywheel, 97.13471664858164)
testAngularAcceleration(flywheel, -29.657593800236377)
testTorque(flywheel, -2.0198310711803433)
testAngularVelocity(flywheel, 97.12541571421204)
testAngularAcceleration(flywheel, -29.657604177526746)
testTorque(flywheel, -2.0200308891605716)
testDragFactor(flywheel, 0.00011)
testIsDwelling(flywheel, false)
testIsUnpowered(flywheel, true)
@ -140,9 +140,9 @@ test('Correct Flywheel behaviour for a noisefree stroke', () => {
testDeltaTime(flywheel, 0.020722165)
testSpinningTime(flywheel, 0.43343548300000007)
testAngularPosition(flywheel, 39.79350694547071)
testAngularVelocity(flywheel, 50.71501160141977)
testAngularAcceleration(flywheel, -159.90034506799844)
testTorque(flywheel, -16.202804212320103)
testAngularVelocity(flywheel, 50.85265548983507)
testAngularAcceleration(flywheel, -159.89027501034317)
testTorque(flywheel, -16.20022817082592)
testDragFactor(flywheel, 0.00011)
testIsDwelling(flywheel, true)
testIsUnpowered(flywheel, true)

View File

@ -1,5 +1,6 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor
This test is a test of the Rower object, that tests wether this object fills all fields correctly, given one validated rower, (the
Concept2 RowErg) using a validated cycle of strokes. This thoroughly tests the raw physics of the translation of Angular physics
@ -110,11 +111,11 @@ test('Test behaviour for three perfect identical strokes, including settingling
testDriveDuration(rower, 0) // Shouldn't this one be filled after the first drive?
testDriveLinearDistance(rower, 0.2491943602992768)
testDriveLength(rower, 0) // Shouldn't this one be filled after the first drive?
testDriveAverageHandleForce(rower, 1691.793078056684)
testDrivePeakHandleForce(rower, 10246.062011594136)
testDriveAverageHandleForce(rower, 249.91096328436572)
testDrivePeakHandleForce(rower, 280.43473478416803)
testRecoveryDuration(rower, 0)
testRecoveryDragFactor(rower, 110)
testInstantHandlePower(rower, 372.0199762100516)
testInstantHandlePower(rower, 372.09477620281604)
// Recovery initial stroke starts here
rower.handleRotationImpulse(0.010769)
rower.handleRotationImpulse(0.010707554)
@ -142,8 +143,8 @@ test('Test behaviour for three perfect identical strokes, including settingling
testDriveDuration(rower, 0.19636192600000005)
testDriveLinearDistance(rower, 0.6407854979124261)
testDriveLength(rower, 0.2638937829015426)
testDriveAverageHandleForce(rower, 851.8820525641245) // This is the first stroke, which always leads to insane data like this
testDrivePeakHandleForce(rower, 10246.062011594136)
testDriveAverageHandleForce(rower, 247.35502383653122)
testDrivePeakHandleForce(rower, 325.1619554833936)
testRecoveryDuration(rower, 0)
testRecoveryDragFactor(rower, 110)
testInstantHandlePower(rower, 0)
@ -178,11 +179,11 @@ test('Test behaviour for three perfect identical strokes, including settingling
testDriveDuration(rower, 0.19636192600000005)
testDriveLinearDistance(rower, 0.4520822644211139)
testDriveLength(rower, 0.2638937829015426)
testDriveAverageHandleForce(rower, 251.04336322997108)
testDrivePeakHandleForce(rower, 396.7011215867992)
testDriveAverageHandleForce(rower, 251.12896067596512)
testDrivePeakHandleForce(rower, 396.7733761783577)
testRecoveryDuration(rower, 0.152533057)
testRecoveryDragFactor(rower, 309.02744980039836)
testInstantHandlePower(rower, 526.5255378434941)
testInstantHandlePower(rower, 526.7173432408988)
// Recovery second stroke starts here
rower.handleRotationImpulse(0.010769)
rower.handleRotationImpulse(0.010707554)
@ -210,8 +211,8 @@ test('Test behaviour for three perfect identical strokes, including settingling
testDriveDuration(rower, 0.25056694500000004)
testDriveLinearDistance(rower, 1.1553213424095137)
testDriveLength(rower, 0.3371976114853044)
testDriveAverageHandleForce(rower, 290.98159585708896)
testDrivePeakHandleForce(rower, 456.9929898648157)
testDriveAverageHandleForce(rower, 290.9778542238004)
testDrivePeakHandleForce(rower, 456.96817421319486)
testRecoveryDuration(rower, 0.152533057)
testRecoveryDragFactor(rower, 309.02744980039836) // As we decelerate the flywheel quite fast, this is expected
testInstantHandlePower(rower, 0)
@ -246,11 +247,11 @@ test('Test behaviour for three perfect identical strokes, including settingling
testDriveDuration(rower, 0.25056694500000004)
testDriveLinearDistance(rower, 0.552544989848028)
testDriveLength(rower, 0.3371976114853044)
testDriveAverageHandleForce(rower, 223.750606354492)
testDrivePeakHandleForce(rower, 396.7011215854034)
testDriveAverageHandleForce(rower, 223.8446015637509)
testDrivePeakHandleForce(rower, 396.7733761769528)
testRecoveryDuration(rower, 0.09847952300000018)
testRecoveryDragFactor(rower, 309.02744980039836)
testInstantHandlePower(rower, 526.5255378417136)
testInstantHandlePower(rower, 526.7173432390936)
// Recovery third stroke starts here
rower.handleRotationImpulse(0.010769)
rower.handleRotationImpulse(0.010707554)
@ -278,8 +279,8 @@ test('Test behaviour for three perfect identical strokes, including settingling
testDriveDuration(rower, 0.2727572410000002)
testDriveLinearDistance(rower, 1.2557840678364274)
testDriveLength(rower, 0.36651914291880905)
testDriveAverageHandleForce(rower, 272.7765993429924)
testDrivePeakHandleForce(rower, 456.99298986363897)
testDriveAverageHandleForce(rower, 272.78784054454604)
testDrivePeakHandleForce(rower, 456.96817421200865)
testRecoveryDuration(rower, 0.09847952300000018)
testRecoveryDragFactor(rower, 309.02744980039836)
testInstantHandlePower(rower, 0)
@ -310,8 +311,8 @@ test('Test behaviour for three perfect identical strokes, including settingling
testDriveDuration(rower, 0.2727572410000002)
testDriveLinearDistance(rower, 1.2557840678364274)
testDriveLength(rower, 0.36651914291880905)
testDriveAverageHandleForce(rower, 272.7765993429924)
testDrivePeakHandleForce(rower, 456.99298986363897)
testDriveAverageHandleForce(rower, 272.78784054454604)
testDrivePeakHandleForce(rower, 456.96817421200865)
testRecoveryDuration(rower, 0.1430115999999999)
testRecoveryDragFactor(rower, 309.02744980039836)
testInstantHandlePower(rower, 0)
@ -379,10 +380,10 @@ test('sample data for NordicTrack RX800 should produce plausible results', async
await replayRowingSession(rower.handleRotationImpulse, { filename: 'recordings/RX800.csv', realtime: false, loop: false })
testTotalMovingTimeSinceStart(rower, 17.389910236000024)
testTotalLinearDistanceSinceStart(rower, 62.49982252262572)
testTotalLinearDistanceSinceStart(rower, 62.499750609934196)
testTotalNumberOfStrokes(rower, 8)
// As dragFactor is dynamic, it should have changed
testRecoveryDragFactor(rower, 493.1277530352103)
testRecoveryDragFactor(rower, 493.127960064474)
})
test('A full session for SportsTech WRX700 should produce plausible results', async () => {
@ -411,10 +412,10 @@ test('A full session for a Concept2 RowErg should produce plausible results', as
await replayRowingSession(rower.handleRotationImpulse, { filename: 'recordings/Concept2_RowErg_Session_2000meters.csv', realtime: false, loop: false })
testTotalMovingTimeSinceStart(rower, 590.111937)
testTotalLinearDistanceSinceStart(rower, 2029.6932502534587)
testTotalLinearDistanceSinceStart(rower, 2029.6932305734617)
testTotalNumberOfStrokes(rower, 206)
// As dragFactor isn't static, it should have changed
testRecoveryDragFactor(rower, 80.79039510767821)
testRecoveryDragFactor(rower, 80.7904433692072)
})
function testStrokeState (rower, expectedValue) {

View File

@ -1,6 +1,6 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/jaapvanekris/openrowingmonitor
Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor
This creates an ordered series with labels
It allows for efficient determining the Median, Number of Above and Below

View File

@ -1,6 +1,6 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor
As this object is fundamental for most other utility objects, we must test its behaviour quite thoroughly
*/

View File

@ -1,6 +1,6 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor
The TSLinearSeries is a datatype that represents a Linear Series. It allows
values to be retrieved (like a FiFo buffer, or Queue) but it also includes
@ -33,11 +33,11 @@ function createTSLinearSeries (maxSeriesLength = 0) {
let _B = 0
function push (x, y) {
// Invariant: A contains all a's (as in the general formula y = a * x^2 + b * x + c)
// Invariant: A contains all a's (as in the general formula y = a * x + b)
// Where the a's are labeled in the Binary Search Tree with their xi when they BEGIN in the point (xi, yi)
if (maxSeriesLength > 0 && X.length() >= maxSeriesLength) {
// The maximum of the array has been reached, so when pushing the x,y the array gets shifted,
// thus we have to remove the a's belonging to the current position X0 as well before this value is trashed
// The maximum of the array has been reached, so when pushing the x,y into the arrays, they get shifted automatically,
// So, we have to remove the a's belonging to the current position X0 as well before this value is trashed
A.remove(X.get(0))
}
@ -49,6 +49,8 @@ function createTSLinearSeries (maxSeriesLength = 0) {
// There are at least two points in the X and Y arrays, so let's add the new datapoint
let i = 0
while (i < X.length() - 1) {
// Calculate the slope with all preceeding datapoints with the X.length() - 1'th datapoint (as the array starts at zero)
// And store it at its beginpoint (i.e. X.get(i)) to allow remove when that point gets removed from the flank
A.push(X.get(i), calculateSlope(i, X.length() - 1))
i++
}
@ -66,7 +68,7 @@ function createTSLinearSeries (maxSeriesLength = 0) {
if (X.length() > 1) {
// There are at least two points in the X and Y arrays, so let's calculate the intercept
let i = 0
while (i < X.length() - 1) {
while (i < X.length()) {
// Please note , as we need to recreate the B-tree for each newly added datapoint anyway, the label i isn't relevant
B.push(i, (Y.get(i) - (_A * X.get(i))))
i++
@ -101,23 +103,34 @@ function createTSLinearSeries (maxSeriesLength = 0) {
function goodnessOfFit () {
// This function returns the R^2 as a goodness of fit indicator
let i = 0
let ssr = 0
let sse = 0
let sst = 0
let _goodnessOfFit = 0
if (X.length() >= 2) {
while (i < X.length() - 1) {
ssr += Math.pow((Y.get(i) - projectX(X.get(i))), 2)
while (i < X.length()) {
sse += Math.pow((Y.get(i) - projectX(X.get(i))), 2)
sst += Math.pow((Y.get(i) - Y.average()), 2)
i++
}
if (sst !== 0) {
const _goodnessOfFit = 1 - (ssr / sst)
return _goodnessOfFit
} else {
return 0
switch (true) {
case (sse === 0):
_goodnessOfFit = 1
break
case (sse > sst):
// This is a pretty bad fit as the error is bigger than just using the line for the average y as intercept
_goodnessOfFit = 0
break
case (sst !== 0):
_goodnessOfFit = 1 - (sse / sst)
break
default:
// When SST = 0, R2 isn't defined
_goodnessOfFit = 0
}
} else {
return 0
_goodnessOfFit = 0
}
return _goodnessOfFit
}
function projectX (x) {

View File

@ -1,6 +1,6 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor
*/
import { test } from 'uvu'
import * as assert from 'uvu/assert'

View File

@ -1,6 +1,6 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor
The FullTSQuadraticSeries is a datatype that represents a Quadratic Series. It allows
values to be retrieved (like a FiFo buffer, or Queue) but it also includes
@ -69,21 +69,13 @@ function createTSQuadraticSeries (maxSeriesLength = 0) {
// Next, we calculate the B and C via Linear regression over the residu
i = 0
while (i < X.length() - 1) {
while (i < X.length()) {
linearResidu.push(X.get(i), Y.get(i) - (_A * Math.pow(X.get(i), 2)))
i++
}
_B = linearResidu.coefficientA()
_C = linearResidu.coefficientB()
break
case (X.length() === 2 && X.get(1) - X.get(0) !== 0):
// There are only two datapoints, so we need to be creative to get to a quadratic solution
// As we know this is part of a 'linear' acceleration, we know that the second derivative should obey 2 * _A = angular acceleration = 2 * angular distance / (delta t)^2
_A = (Y.get(1) - Y.get(0)) / Math.pow(X.get(1) - X.get(0), 2)
// As the first derivative should match angular velocity (= angular acceleration * (delta t))
_B = -2 * _A * X.get(0)
_C = 0
break
default:
_A = 0
_B = 0
@ -141,23 +133,34 @@ function createTSQuadraticSeries (maxSeriesLength = 0) {
function goodnessOfFit () {
// This function returns the R^2 as a goodness of fit indicator
let i = 0
let ssr = 0
let sse = 0
let sst = 0
if (X.length() >= 2) {
while (i < X.length() - 1) {
ssr += Math.pow((Y.get(i) - projectX(X.get(i))), 2)
let _goodnessOfFit = 0
if (X.length() > 2) {
while (i < X.length()) {
sse += Math.pow((Y.get(i) - projectX(X.get(i))), 2)
sst += Math.pow((Y.get(i) - Y.average()), 2)
i++
}
if (sst !== 0) {
const _goodnessOfFit = 1 - (ssr / sst)
return _goodnessOfFit
} else {
return 0
switch (true) {
case (sse === 0):
_goodnessOfFit = 1
break
case (sse > sst):
// This is a pretty bad fit as the error is bigger than just using the line for the average y as intercept
_goodnessOfFit = 0
break
case (sst !== 0):
_goodnessOfFit = 1 - (sse / sst)
break
default:
// When SST = 0, R2 isn't defined
_goodnessOfFit = 0
}
} else {
return 0
_goodnessOfFit = 0
}
return _goodnessOfFit
}
function projectX (x) {

View File

@ -1,6 +1,6 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor
This tests the Quadratic Theil-Senn Regression algorithm. As regression is an estimation and methods have biasses,
we need to accept some slack with respect to real-life examples
@ -56,6 +56,7 @@ test('Quadratic Approximation on a perfect noisefree function y = 2 * Math.pow(x
testCoefficientA(dataSeries, 2)
testCoefficientB(dataSeries, 2)
testCoefficientC(dataSeries, 2)
testGoodnessOfFitEquals(dataSeries, 1)
})
test('Quadratic Approximation on a perfect noisefree function y = 2 * Math.pow(x, 2) + 2 * x + 2, with 10 datapoints and some shifting in the series', () => {
@ -75,6 +76,7 @@ test('Quadratic Approximation on a perfect noisefree function y = 2 * Math.pow(x
testCoefficientA(dataSeries, 2)
testCoefficientB(dataSeries, 2)
testCoefficientC(dataSeries, 2)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(1, 6)
dataSeries.push(2, 14)
dataSeries.push(3, 26)
@ -88,6 +90,7 @@ test('Quadratic Approximation on a perfect noisefree function y = 2 * Math.pow(x
testCoefficientA(dataSeries, 2)
testCoefficientB(dataSeries, 2)
testCoefficientC(dataSeries, 2)
testGoodnessOfFitEquals(dataSeries, 1)
})
test('Quadratic Approximation on function y = 4 * Math.pow(x, 2) + 4 * x + 4, noisefree', () => {
@ -99,82 +102,102 @@ test('Quadratic Approximation on function y = 4 * Math.pow(x, 2) + 4 * x + 4, no
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(-8, 228)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(-7, 172)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(-6, 124)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(-5, 84)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(-4, 52)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(-3, 28)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(-2, 12)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(-1, 4)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(0, 4)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(1, 12)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(2, 28)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(3, 52)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(4, 84)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(5, 124)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(6, 172)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(7, 228)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(8, 292)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(9, 364)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(10, 444)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 1)
})
test('Quadratic Approximation on function y = 4 * Math.pow(x, 2) + 4 * x + 4, with some noise (+/- 1)', () => {
@ -186,82 +209,102 @@ test('Quadratic Approximation on function y = 4 * Math.pow(x, 2) + 4 * x + 4, wi
testCoefficientA(dataSeries, 2)
testCoefficientB(dataSeries, -36)
testCoefficientC(dataSeries, -195)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(-8, 229)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testCoefficientB(dataSeries, 4.333333333333334)
testCoefficientC(dataSeries, 7.166666666666671)
testGoodnessOfFitEquals(dataSeries, 0.9998746217034155)
dataSeries.push(-7, 171)
testCoefficientA(dataSeries, 3.3333333333333335)
testCoefficientB(dataSeries, -7.999999999999995)
testCoefficientC(dataSeries, -48.333333333333314)
testCoefficientB(dataSeries, -7.999999999999991)
testCoefficientC(dataSeries, -48.33333333333328)
testGoodnessOfFitEquals(dataSeries, 0.9998468647471163)
dataSeries.push(-6, 125)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 0.9999165499911914)
dataSeries.push(-5, 83)
testCoefficientA(dataSeries, 3.8666666666666667)
testCoefficientB(dataSeries, 1.8666666666666742)
testCoefficientC(dataSeries, -4.3333333333332575) // This is quite acceptable as ORM ignores the C
testCoefficientB(dataSeries, 1.8666666666666671)
testCoefficientC(dataSeries, -4.333333333333336) // This is quite acceptable as ORM ignores the C
testGoodnessOfFitEquals(dataSeries, 0.9999366117119067)
dataSeries.push(-4, 53)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 0.9999402806808002)
dataSeries.push(-3, 27)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 3)
testGoodnessOfFitEquals(dataSeries, 0.9999042318865254)
dataSeries.push(-2, 13)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 0.9999495097395712)
dataSeries.push(-1, 3)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 3)
testGoodnessOfFitEquals(dataSeries, 0.9999117149452151)
dataSeries.push(0, 5)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 5)
testGoodnessOfFitEquals(dataSeries, 0.9998721709098177)
dataSeries.push(1, 11)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 3)
testGoodnessOfFitEquals(dataSeries, 0.9997996371611135)
dataSeries.push(2, 29)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 5)
testGoodnessOfFitEquals(dataSeries, 0.9996545703483187)
dataSeries.push(3, 51)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 3)
testGoodnessOfFitEquals(dataSeries, 0.9993201651380683)
dataSeries.push(4, 85)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 5)
testGoodnessOfFitEquals(dataSeries, 0.9987227718173796)
dataSeries.push(5, 123)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 3)
testGoodnessOfFitEquals(dataSeries, 0.9986961263098004)
dataSeries.push(6, 173)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 5)
testGoodnessOfFitEquals(dataSeries, 0.9993274803746546)
dataSeries.push(7, 227)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 3)
testGoodnessOfFitEquals(dataSeries, 0.9996526505917571)
dataSeries.push(8, 293)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 5)
testGoodnessOfFitEquals(dataSeries, 0.9998002774328024)
dataSeries.push(9, 363)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 3)
testCoefficientC(dataSeries, 3) // We get a 3 instead of 4, which is quite acceptable (especially since ORM ignores the C)
testGoodnessOfFitEquals(dataSeries, 0.9998719089295779)
dataSeries.push(10, 444)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 5)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 0.9999558104799866)
})
test('Quadratic Approximation on function y = 4 * Math.pow(x, 2) + 4 * x + 4, with some noise (+/- 1) and spikes (+/- 9)', () => {
@ -278,62 +321,77 @@ test('Quadratic Approximation on function y = 4 * Math.pow(x, 2) + 4 * x + 4, wi
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 0.9999402806808002)
dataSeries.push(-3, 37) // FIRST SPIKE +9
testCoefficientA(dataSeries, 4.215277777777778)
testCoefficientB(dataSeries, 7.321527777777776)
testCoefficientC(dataSeries, 15.70208333333332)
testCoefficientB(dataSeries, 7.694940476190471)
testCoefficientC(dataSeries, 18.816964285714235)
testGoodnessOfFitEquals(dataSeries, 0.9997971509015441)
dataSeries.push(-2, 3) // SECOND SPIKE -9
testCoefficientA(dataSeries, 3.9714285714285715)
testCoefficientB(dataSeries, 3.78571428571429) // Coefficient B seems to take a hit anyway
testCoefficientC(dataSeries, 4.35000000000003) // We get a 4.35000000000003 instead of 4, which is quite acceptable (especially since ORM ignores the C)
testCoefficientB(dataSeries, 3.6000000000000036) // Coefficient B seems to take a hit anyway
testCoefficientC(dataSeries, 2.842857142857163) // We get a 2.8 instead of 4, which is quite acceptable (especially since ORM ignores the C)
testGoodnessOfFitEquals(dataSeries, 0.9991656951087963)
dataSeries.push(-1, 3)
testCoefficientA(dataSeries, 3.9555555555555557)
testCoefficientB(dataSeries, 3.37777777777778)
testCoefficientC(dataSeries, 2.8666666666666742)
testCoefficientC(dataSeries, 2.4222222222222243)
testGoodnessOfFitEquals(dataSeries, 0.9992769580376006)
dataSeries.push(0, 5)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 5)
testGoodnessOfFitEquals(dataSeries, 0.9988530568930122)
dataSeries.push(1, 11)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 3)
testGoodnessOfFitEquals(dataSeries, 0.9982053643291688)
dataSeries.push(2, 29)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 5)
testGoodnessOfFitEquals(dataSeries, 0.9969166946967148)
dataSeries.push(3, 51)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 3)
testGoodnessOfFitEquals(dataSeries, 0.9939797134586851)
dataSeries.push(4, 85)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 5)
testGoodnessOfFitEquals(dataSeries, 0.9888468297958631)
dataSeries.push(5, 123)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 3)
testGoodnessOfFitEquals(dataSeries, 0.9886212128178015)
dataSeries.push(6, 173)
testCoefficientA(dataSeries, 4.044444444444444)
testCoefficientB(dataSeries, 3.8222222222222215)
testCoefficientC(dataSeries, 3.5777777777777775)
testCoefficientB(dataSeries, 3.822222222222223)
testCoefficientC(dataSeries, 3.577777777777783)
testGoodnessOfFitEquals(dataSeries, 0.9945681627011398)
dataSeries.push(7, 227)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 3)
testGoodnessOfFitEquals(dataSeries, 0.9968997006175546)
dataSeries.push(8, 293)
testCoefficientA(dataSeries, 3.9047619047619047)
testCoefficientB(dataSeries, 4.761904761904762)
testCoefficientC(dataSeries, 3.476190476190478) // This is quite acceptable as ORM ignores the C
testCoefficientB(dataSeries, 4.888888888888889)
testCoefficientC(dataSeries, 2.9682539682539684) // This is quite acceptable as ORM ignores the C
testGoodnessOfFitEquals(dataSeries, 0.9995034675221599)
dataSeries.push(9, 363)
testCoefficientA(dataSeries, 4)
testCoefficientA(dataSeries, 4) // These results match up 100% with the previous test, showing that a spike has no carry over effects
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 3) // We get a 3 instead of 4, which is quite acceptable (especially since ORM ignores the C)
testCoefficientC(dataSeries, 3)
testGoodnessOfFitEquals(dataSeries, 0.9998719089295779)
dataSeries.push(10, 444)
testCoefficientA(dataSeries, 4)
testCoefficientB(dataSeries, 4)
testCoefficientC(dataSeries, 5)
testCoefficientC(dataSeries, 4)
testGoodnessOfFitEquals(dataSeries, 0.9999558104799866)
})
test('Quadratic TS Estimation should be decent for standard real-life example from MathBits with some noise', () => {
@ -353,8 +411,9 @@ test('Quadratic TS Estimation should be decent for standard real-life example fr
dataSeries.push(60, 231.4)
dataSeries.push(64, 180.4)
testCoefficientA(dataSeries, -0.17702838827838824) // In the example, the TI084 results in -0.1737141137, which we consider acceptably close
testCoefficientB(dataSeries, 15.059093406593405) // In the example, the TI084 results in 14.52117133, which we consider acceptably close
testCoefficientC(dataSeries, -37.563076923077006) // In the example, the TI084 results in -21.89774466, which we consider acceptably close
testCoefficientB(dataSeries, 14.929144536019532) // In the example, the TI084 results in 14.52117133, which we consider acceptably close
testCoefficientC(dataSeries, -31.325531135531037) // In the example, the TI084 results in -21.89774466, which we consider acceptably close
testGoodnessOfFitEquals(dataSeries, 0.9781087883163964)
})
test('Quadratic TS Estimation should be decent for standard real-life example from VarsityTutors with some noise', () => {
@ -368,8 +427,9 @@ test('Quadratic TS Estimation should be decent for standard real-life example fr
dataSeries.push(2, 6)
dataSeries.push(3, 14)
testCoefficientA(dataSeries, 1.0833333333333333) // The example results in 1.1071 for OLS, which we consider acceptably close
testCoefficientB(dataSeries, 0.9166666666666667) // The example results in 1 for OLS, which we consider acceptably close
testCoefficientC(dataSeries, 0.5000000000000004) // The example results in 0.5714 for OLS, which we consider acceptably close
testCoefficientB(dataSeries, 1.0833333333333333) // The example results in 1 for OLS, which we consider acceptably close
testCoefficientC(dataSeries, 0.8333333333333335) // The example results in 0.5714 for OLS, which we consider acceptably close
testGoodnessOfFitEquals(dataSeries, 0.9851153039832286)
})
test('Quadratic TS Estimation should be decent for standard example from VTUPulse with some noise, without the vertex being part of the dataset', () => {
@ -381,8 +441,9 @@ test('Quadratic TS Estimation should be decent for standard example from VTUPuls
dataSeries.push(6, 6.5)
dataSeries.push(7, 11.5)
testCoefficientA(dataSeries, 0.8583333333333334) // The example results in 0.7642857 for OLS, which we consider acceptably close given the small sample size
testCoefficientB(dataSeries, -6.566666666666666) // The example results in -5.5128571 for OLS, which we consider acceptably close given the small sample size
testCoefficientC(dataSeries, 15.174999999999994) // The example results in 12.4285714 for OLS, which we consider acceptably close given the small sample size
testCoefficientB(dataSeries, -6.420833333333334) // The example results in -5.5128571 for OLS, which we consider acceptably close given the small sample size
testCoefficientC(dataSeries, 14.387500000000003) // The example results in 12.4285714 for OLS, which we consider acceptably close given the small sample size
testGoodnessOfFitEquals(dataSeries, 0.9825283785404673)
})
test('Quadratic TS Estimation should be decent for standard real-life example from Uni Berlin with some noise without the vertex being part of the dataset', () => {
@ -414,8 +475,9 @@ test('Quadratic TS Estimation should be decent for standard real-life example fr
dataSeries.push(0.715372314, -1.20379729)
dataSeries.push(0.681745393, -0.83059624)
testCoefficientA(dataSeries, -2.030477132951317)
testCoefficientB(dataSeries, 0.6253742507247935)
testCoefficientC(dataSeries, 0.2334077291108024)
testCoefficientB(dataSeries, 0.5976858995201227)
testCoefficientC(dataSeries, 0.17630021024409503)
testGoodnessOfFitEquals(dataSeries, 0.23921110548689295)
})
test('Quadratic TS Estimation should be decent for standard real-life example from Statology.org with some noise and chaotic X values', () => {
@ -433,8 +495,9 @@ test('Quadratic TS Estimation should be decent for standard real-life example fr
dataSeries.push(55, 44)
dataSeries.push(60, 27)
testCoefficientA(dataSeries, -0.10119047619047619) // The example results in -0.1012 for R after two rounds, which we consider acceptably close
testCoefficientB(dataSeries, 6.767857142857142) // The example results in 6.7444 for R after two rounds, which we consider acceptably close
testCoefficientC(dataSeries, -19.55952380952374) // The example results in 18.2536 for R after two rounds, but for ORM, this factor is irrelevant
testCoefficientB(dataSeries, 6.801190476190477) // The example results in 6.7444 for R after two rounds, which we consider acceptably close
testCoefficientC(dataSeries, -21.126190476190516) // The example results in 18.2536 for R after two rounds, but for ORM, this factor is irrelevant
testGoodnessOfFitEquals(dataSeries, 0.9571127392718894)
})
test('Quadratic TS Estimation should be decent for standard real-life example from StatsDirect.com with some noise and chaotic X values', () => {
@ -451,8 +514,9 @@ test('Quadratic TS Estimation should be decent for standard real-life example fr
dataSeries.push(2400, 1956)
dataSeries.push(2930, 1954)
testCoefficientA(dataSeries, -0.00046251263566907585) // The example results in -0.00045 through QR decomposition by Givens rotations, which we consider acceptably close
testCoefficientB(dataSeries, 2.429942262608943) // The example results in 2.39893 for QR decomposition by Givens rotations, which we consider acceptably close
testCoefficientC(dataSeries, -1221.3216719814116) // The example results in -1216.143887 for QR decomposition by Givens rotations, but for ORM, this factor is irrelevant
testCoefficientB(dataSeries, 2.441798780934297) // The example results in 2.39893 for QR decomposition by Givens rotations, which we consider acceptably close
testCoefficientC(dataSeries, -1235.044997485239) // The example results in -1216.143887 for QR decomposition by Givens rotations, but for ORM, this factor is irrelevant
testGoodnessOfFitEquals(dataSeries, 0.9790379024208455)
})
test('Quadratic Approximation with a clean function and a reset', () => {
@ -467,6 +531,7 @@ test('Quadratic Approximation with a clean function and a reset', () => {
testCoefficientA(dataSeries, 2)
testCoefficientB(dataSeries, 2)
testCoefficientC(dataSeries, 2)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(-4, 26)
dataSeries.push(-3, 14) // Pi ;)
dataSeries.push(-2, 6)
@ -477,6 +542,7 @@ test('Quadratic Approximation with a clean function and a reset', () => {
testCoefficientA(dataSeries, 2)
testCoefficientB(dataSeries, 2)
testCoefficientC(dataSeries, 2)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.push(3, 26)
dataSeries.push(4, 42)
dataSeries.push(5, 62)
@ -488,22 +554,27 @@ test('Quadratic Approximation with a clean function and a reset', () => {
testCoefficientA(dataSeries, 2)
testCoefficientB(dataSeries, 2)
testCoefficientC(dataSeries, 2)
testGoodnessOfFitEquals(dataSeries, 1)
dataSeries.reset()
testCoefficientA(dataSeries, 0)
testCoefficientB(dataSeries, 0)
testCoefficientC(dataSeries, 0)
testGoodnessOfFitEquals(dataSeries, 0)
dataSeries.push(-1, 2)
testCoefficientA(dataSeries, 0)
testCoefficientB(dataSeries, 0)
testCoefficientC(dataSeries, 0)
testGoodnessOfFitEquals(dataSeries, 0)
dataSeries.push(0, 2)
testCoefficientA(dataSeries, 0)
testCoefficientB(dataSeries, 0)
testCoefficientC(dataSeries, 0)
testGoodnessOfFitEquals(dataSeries, 0)
dataSeries.push(1, 6)
testCoefficientA(dataSeries, 2)
testCoefficientB(dataSeries, 2)
testCoefficientC(dataSeries, 2)
testGoodnessOfFitEquals(dataSeries, 1)
})
test('Quadratic TS Estimation should result in a straight line for function y = x', () => {
@ -519,6 +590,7 @@ test('Quadratic TS Estimation should result in a straight line for function y =
testCoefficientA(dataSeries, 0)
testCoefficientB(dataSeries, 1)
testCoefficientC(dataSeries, 0)
testGoodnessOfFitEquals(dataSeries, 1)
})
function testCoefficientA (series, expectedValue) {
@ -533,14 +605,21 @@ function testCoefficientC (series, expectedValue) {
assert.ok(series.coefficientC() === expectedValue, `Expected value for coefficientC at X-position ${series.xAtSeriesEnd()} is ${expectedValue}, encountered a ${series.coefficientC()}`)
}
/*
function testSlope (series, position, expectedValue) {
function testGoodnessOfFitEquals (series, expectedValue) {
assert.ok(series.goodnessOfFit() === expectedValue, `Expected goodnessOfFit at X-position ${series.xAtSeriesEnd()} is ${expectedValue}, encountered ${series.goodnessOfFit()}`)
}
function testGoodnessOfFitBetween (series, expectedValueAbove, expectedValueBelow) { // eslint-disable-line no-unused-vars
assert.ok(series.goodnessOfFit() > expectedValueAbove, `Expected goodnessOfFit at X-position ${series.xAtSeriesEnd()} above ${expectedValueAbove}, encountered ${series.goodnessOfFit()}`)
assert.ok(series.goodnessOfFit() < expectedValueBelow, `Expected goodnessOfFit at X-position ${series.xAtSeriesEnd()} below ${expectedValueBelow}, encountered ${series.goodnessOfFit()}`)
}
function testSlope (series, position, expectedValue) { // eslint-disable-line no-unused-vars
assert.ok(series.slope(position) === expectedValue, `Expected value for Slope-${position} at X-position ${series.xAtSeriesEnd()} (slope at X-position ${series.xAtPosition(position)}) is ${expectedValue}, encountered a ${series.slope(position)}`)
}
function reportAll (series) {
function reportAll (series) { // eslint-disable-line no-unused-vars
assert.ok(series.coefficientA() === 99, `time: ${series.xAtSeriesEnd()}, coefficientA: ${series.coefficientA()}, coefficientB: ${series.coefficientB()}, coefficientC: ${series.coefficientC()}, Slope-10: ${series.slope(10)}, Slope-9: ${series.slope(9)}, Slope-8: ${series.slope(8)}, Slope-7: ${series.slope(7)}, Slope-6: ${series.slope(6)}, Slope-5: ${series.slope(5)}, Slope-4: ${series.slope(4)}, Slope-3: ${series.slope(3)}, Slope-2: ${series.slope(2)}, Slope-1: ${series.slope(1)}, Slope-0: ${series.slope(0)}`)
}
*/
test.run()