From 414372ef235af075f1fa4992bd20a8e329bd63c2 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:15:07 +0100 Subject: [PATCH] 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. --- app/engine/Flywheel.test.js | 20 +-- app/engine/Rower.test.js | 43 ++--- app/engine/utils/BinarySearchTree.js | 2 +- app/engine/utils/BinarySearchTree.test.js | 2 +- app/engine/utils/FullTSLinearSeries.js | 41 +++-- app/engine/utils/FullTSLinearSeries.test.js | 2 +- app/engine/utils/FullTSQuadraticSeries.js | 43 ++--- .../utils/FullTSQuadraticSeries.test.js | 153 +++++++++++++----- 8 files changed, 201 insertions(+), 105 deletions(-) diff --git a/app/engine/Flywheel.test.js b/app/engine/Flywheel.test.js index 331b064..b64943b 100644 --- a/app/engine/Flywheel.test.js +++ b/app/engine/Flywheel.test.js @@ -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) diff --git a/app/engine/Rower.test.js b/app/engine/Rower.test.js index 357911a..8510fde 100644 --- a/app/engine/Rower.test.js +++ b/app/engine/Rower.test.js @@ -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) { diff --git a/app/engine/utils/BinarySearchTree.js b/app/engine/utils/BinarySearchTree.js index 7a181df..c2ae5ec 100644 --- a/app/engine/utils/BinarySearchTree.js +++ b/app/engine/utils/BinarySearchTree.js @@ -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 diff --git a/app/engine/utils/BinarySearchTree.test.js b/app/engine/utils/BinarySearchTree.test.js index c56feb5..e7b8e54 100644 --- a/app/engine/utils/BinarySearchTree.test.js +++ b/app/engine/utils/BinarySearchTree.test.js @@ -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 */ diff --git a/app/engine/utils/FullTSLinearSeries.js b/app/engine/utils/FullTSLinearSeries.js index ec2f2c1..f531b2e 100644 --- a/app/engine/utils/FullTSLinearSeries.js +++ b/app/engine/utils/FullTSLinearSeries.js @@ -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) { diff --git a/app/engine/utils/FullTSLinearSeries.test.js b/app/engine/utils/FullTSLinearSeries.test.js index 8b0153e..238b809 100644 --- a/app/engine/utils/FullTSLinearSeries.test.js +++ b/app/engine/utils/FullTSLinearSeries.test.js @@ -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' diff --git a/app/engine/utils/FullTSQuadraticSeries.js b/app/engine/utils/FullTSQuadraticSeries.js index 0329af2..67e4896 100644 --- a/app/engine/utils/FullTSQuadraticSeries.js +++ b/app/engine/utils/FullTSQuadraticSeries.js @@ -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) { diff --git a/app/engine/utils/FullTSQuadraticSeries.test.js b/app/engine/utils/FullTSQuadraticSeries.test.js index effb7aa..0a3e987 100644 --- a/app/engine/utils/FullTSQuadraticSeries.test.js +++ b/app/engine/utils/FullTSQuadraticSeries.test.js @@ -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()