Skip to content

Commit f8c552e

Browse files
committed
[ML] Correct ageing of windowed seasonal components (#88)
In particular, we were ageing them too fast and their decay rate should be prorated by the fraction of values with which they are updated.
1 parent b7773ef commit f8c552e

7 files changed

+72
-75
lines changed

docs/CHANGELOG.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ model state on disk ({pull}89[#89])
2222

2323
=== Bug Fixes
2424

25+
Age seasonal components in proportion to the fraction of values with which they're updated ({pull}88[#88])
26+
2527
=== Regressions
2628

2729
=== Known Issues

lib/maths/CSeasonalComponentAdaptiveBucketing.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,8 @@ void CSeasonalComponentAdaptiveBucketing::propagateForwardsByTime(double time, b
240240
if (time < 0.0) {
241241
LOG_ERROR(<< "Can't propagate bucketing backwards in time");
242242
} else if (this->initialized()) {
243-
double factor{std::exp(-this->CAdaptiveBucketing::decayRate() * time)};
243+
double factor{std::exp(-this->CAdaptiveBucketing::decayRate() *
244+
m_Time->fractionInWindow() * time)};
244245
this->CAdaptiveBucketing::age(factor);
245246
for (auto& bucket : m_Buckets) {
246247
bucket.s_Regression.age(factor, meanRevert);

lib/maths/CTimeSeriesDecompositionDetail.cc

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,9 +1103,8 @@ void CTimeSeriesDecompositionDetail::CComponents::handle(const SAddValue& messag
11031103
for (std::size_t i = 1u; i <= m; ++i) {
11041104
CSeasonalComponent* component{seasonalComponents[i - 1]};
11051105
CComponentErrors* error_{seasonalErrors[i - 1]};
1106-
double wi{weight / component->time().fractionInWindow()};
1107-
component->add(time, values[i], wi);
1108-
error_->add(error, predictions[i - 1], wi);
1106+
component->add(time, values[i], weight);
1107+
error_->add(error, predictions[i - 1], weight);
11091108
}
11101109
for (std::size_t i = m + 1; i <= m + n; ++i) {
11111110
CCalendarComponent* component{calendarComponents[i - m - 1]};

lib/maths/unittest/CForecastTest.cc

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,14 @@ void CForecastTest::testDailyNoLongTermTrend() {
8383

8484
test::CRandomNumbers rng;
8585

86-
auto trend = [&y, bucketLength](core_t::TTime time, double noise) {
87-
core_t::TTime i{(time % 86400) / bucketLength};
86+
TTrend trend = [&y, bucketLength](core_t::TTime time, double noise) {
87+
core_t::TTime i{(time % core::constants::DAY) / bucketLength};
8888
double alpha{static_cast<double>(i % 6) / 6.0};
8989
double beta{1.0 - alpha};
9090
return 40.0 + alpha * y[i / 6] + beta * y[(i / 6 + 1) % y.size()] + noise;
9191
};
9292

93-
this->test(trend, bucketLength, 60, 64.0, 5.0, 0.14);
93+
this->test(trend, bucketLength, 63, 64.0, 5.0, 0.14);
9494
}
9595

9696
void CForecastTest::testDailyConstantLongTermTrend() {
@@ -99,18 +99,18 @@ void CForecastTest::testDailyConstantLongTermTrend() {
9999
80.0, 100.0, 110.0, 120.0, 110.0, 100.0, 90.0, 80.0,
100100
30.0, 15.0, 10.0, 8.0, 5.0, 3.0, 2.0, 0.0};
101101

102-
auto trend = [&y, bucketLength](core_t::TTime time, double noise) {
103-
core_t::TTime i{(time % 86400) / bucketLength};
102+
TTrend trend = [&y, bucketLength](core_t::TTime time, double noise) {
103+
core_t::TTime i{(time % core::constants::DAY) / bucketLength};
104104
return 0.25 * static_cast<double>(time) / static_cast<double>(bucketLength) +
105105
y[i] + noise;
106106
};
107107

108-
this->test(trend, bucketLength, 60, 64.0, 16.0, 0.02);
108+
this->test(trend, bucketLength, 63, 64.0, 14.0, 0.02);
109109
}
110110

111111
void CForecastTest::testDailyVaryingLongTermTrend() {
112112
core_t::TTime bucketLength{3600};
113-
double day{86400.0};
113+
double day{static_cast<double>(core::constants::DAY)};
114114
TDoubleVec times{0.0, 5.0 * day, 10.0 * day, 15.0 * day,
115115
20.0 * day, 25.0 * day, 30.0 * day, 35.0 * day,
116116
40.0 * day, 45.0 * day, 50.0 * day, 55.0 * day,
@@ -124,13 +124,13 @@ void CForecastTest::testDailyVaryingLongTermTrend() {
124124
maths::CSpline<> trend_(maths::CSplineTypes::E_Cubic);
125125
trend_.interpolate(times, values, maths::CSplineTypes::E_Natural);
126126

127-
auto trend = [&trend_](core_t::TTime time, double noise) {
127+
TTrend trend = [&trend_](core_t::TTime time, double noise) {
128128
double time_{static_cast<double>(time)};
129129
return trend_.value(time_) +
130130
8.0 * std::sin(boost::math::double_constants::two_pi * time_ / 43200.0) + noise;
131131
};
132132

133-
this->test(trend, bucketLength, 100, 9.0, 13.0, 0.04);
133+
this->test(trend, bucketLength, 98, 9.0, 14.0, 0.042);
134134
}
135135

136136
void CForecastTest::testComplexNoLongTermTrend() {
@@ -140,13 +140,13 @@ void CForecastTest::testComplexNoLongTermTrend() {
140140
60.0, 40.0, 30.0, 20.0, 10.0, 10.0, 5.0, 0.0};
141141
TDoubleVec scale{1.0, 1.1, 1.05, 0.95, 0.9, 0.3, 0.2};
142142

143-
auto trend = [&y, &scale, bucketLength](core_t::TTime time, double noise) {
144-
core_t::TTime d{(time % 604800) / 86400};
145-
core_t::TTime h{(time % 86400) / bucketLength};
143+
TTrend trend = [&y, &scale, bucketLength](core_t::TTime time, double noise) {
144+
core_t::TTime d{(time % core::constants::WEEK) / core::constants::DAY};
145+
core_t::TTime h{(time % core::constants::DAY) / bucketLength};
146146
return scale[d] * (20.0 + y[h] + noise);
147147
};
148148

149-
this->test(trend, bucketLength, 60, 24.0, 28.0, 0.13);
149+
this->test(trend, bucketLength, 63, 24.0, 8.0, 0.13);
150150
}
151151

152152
void CForecastTest::testComplexConstantLongTermTrend() {
@@ -156,19 +156,19 @@ void CForecastTest::testComplexConstantLongTermTrend() {
156156
60.0, 40.0, 30.0, 20.0, 10.0, 10.0, 5.0, 0.0};
157157
TDoubleVec scale{1.0, 1.1, 1.05, 0.95, 0.9, 0.3, 0.2};
158158

159-
auto trend = [&y, &scale, bucketLength](core_t::TTime time, double noise) {
160-
core_t::TTime d{(time % 604800) / 86400};
161-
core_t::TTime h{(time % 86400) / bucketLength};
159+
TTrend trend = [&y, &scale, bucketLength](core_t::TTime time, double noise) {
160+
core_t::TTime d{(time % core::constants::WEEK) / core::constants::DAY};
161+
core_t::TTime h{(time % core::constants::DAY) / bucketLength};
162162
return 0.25 * static_cast<double>(time) / static_cast<double>(bucketLength) +
163163
scale[d] * (20.0 + y[h] + noise);
164164
};
165165

166-
this->test(trend, bucketLength, 60, 24.0, 14.0, 0.01);
166+
this->test(trend, bucketLength, 63, 24.0, 8.0, 0.01);
167167
}
168168

169169
void CForecastTest::testComplexVaryingLongTermTrend() {
170170
core_t::TTime bucketLength{3600};
171-
double day{86400.0};
171+
double day{static_cast<double>(core::constants::DAY)};
172172
TDoubleVec times{0.0, 5.0 * day, 10.0 * day, 15.0 * day,
173173
20.0 * day, 25.0 * day, 30.0 * day, 35.0 * day,
174174
40.0 * day, 45.0 * day, 50.0 * day, 55.0 * day,
@@ -186,14 +186,14 @@ void CForecastTest::testComplexVaryingLongTermTrend() {
186186
maths::CSpline<> trend_(maths::CSplineTypes::E_Cubic);
187187
trend_.interpolate(times, values, maths::CSplineTypes::E_Natural);
188188

189-
auto trend = [&trend_, &y, &scale, bucketLength](core_t::TTime time, double noise) {
190-
core_t::TTime d{(time % 604800) / 86400};
191-
core_t::TTime h{(time % 86400) / bucketLength};
189+
TTrend trend = [&trend_, &y, &scale, bucketLength](core_t::TTime time, double noise) {
190+
core_t::TTime d{(time % core::constants::WEEK) / core::constants::DAY};
191+
core_t::TTime h{(time % core::constants::DAY) / bucketLength};
192192
double time_{static_cast<double>(time)};
193193
return trend_.value(time_) + scale[d] * (20.0 + y[h] + noise);
194194
};
195195

196-
this->test(trend, bucketLength, 60, 4.0, 28.0, 0.053);
196+
this->test(trend, bucketLength, 63, 4.0, 42.0, 0.06);
197197
}
198198

199199
void CForecastTest::testNonNegative() {
@@ -400,7 +400,6 @@ void CForecastTest::test(TTrend trend,
400400
double noiseVariance,
401401
double maximumPercentageOutOfBounds,
402402
double maximumError) {
403-
404403
//std::ofstream file;
405404
//file.open("results.m");
406405
//TDoubleVec actual;
@@ -423,7 +422,8 @@ void CForecastTest::test(TTrend trend,
423422
TDouble2VecWeightsAryVec weights{maths_t::CUnitWeights::unit<TDouble2Vec>(1)};
424423
for (std::size_t d = 0u; d < daysToLearn; ++d) {
425424
TDoubleVec noise;
426-
rng.generateNormalSamples(0.0, noiseVariance, 86400 / bucketLength, noise);
425+
rng.generateNormalSamples(0.0, noiseVariance,
426+
core::constants::DAY / bucketLength, noise);
427427

428428
for (std::size_t i = 0u; i < noise.size(); ++i, time += bucketLength) {
429429
maths::CModelAddSamplesParams params;
@@ -450,7 +450,8 @@ void CForecastTest::test(TTrend trend,
450450

451451
for (std::size_t i = 0u; i < prediction.size(); /**/) {
452452
TDoubleVec noise;
453-
rng.generateNormalSamples(0.0, noiseVariance, 86400 / bucketLength, noise);
453+
rng.generateNormalSamples(0.0, noiseVariance,
454+
core::constants::DAY / bucketLength, noise);
454455
TDoubleVec day;
455456
for (std::size_t j = 0u; i < prediction.size() && j < noise.size();
456457
++i, ++j, time += bucketLength) {

lib/maths/unittest/CSeasonalComponentAdaptiveBucketingTest.cc

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <core/CRapidXmlParser.h>
1212
#include <core/CRapidXmlStatePersistInserter.h>
1313
#include <core/CRapidXmlStateRestoreTraverser.h>
14+
#include <core/Constants.h>
1415

1516
#include <maths/CSeasonalComponentAdaptiveBucketing.h>
1617
#include <maths/CSeasonalTime.h>
@@ -36,14 +37,14 @@ using TMaxAccumulator = maths::CBasicStatistics::SMax<double>::TAccumulator;
3637
}
3738

3839
void CSeasonalComponentAdaptiveBucketingTest::testInitialize() {
39-
maths::CDiurnalTime time(0, 1, 101, 100);
40+
maths::CGeneralPeriodTime time(100);
4041
maths::CSeasonalComponentAdaptiveBucketing bucketing(time);
4142

4243
CPPUNIT_ASSERT(!bucketing.initialize(0));
4344

4445
const std::string expectedEndpoints("[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]");
45-
const std::string expectedKnots("[0, 4, 14, 24, 34, 44, 54, 64, 74, 84, 94, 100]");
46-
const std::string expectedValues("[41, 5, 15, 25, 35, 45, 55, 65, 75, 85, 95, 41]");
46+
const std::string expectedKnots("[0, 5, 15, 25, 35, 45, 55, 65, 75, 85, 95, 100]");
47+
const std::string expectedValues("[50, 5, 15, 25, 35, 45, 55, 65, 75, 85, 95, 50]");
4748

4849
CPPUNIT_ASSERT(bucketing.initialize(10));
4950
const TFloatVec& endpoints = bucketing.endpoints();
@@ -62,7 +63,7 @@ void CSeasonalComponentAdaptiveBucketingTest::testInitialize() {
6263
}
6364

6465
void CSeasonalComponentAdaptiveBucketingTest::testSwap() {
65-
maths::CDiurnalTime time1(0, 0, 100, 100);
66+
maths::CGeneralPeriodTime time1(100);
6667
maths::CSeasonalComponentAdaptiveBucketing bucketing1(time1, 0.05);
6768

6869
test::CRandomNumbers rng;
@@ -82,7 +83,7 @@ void CSeasonalComponentAdaptiveBucketingTest::testSwap() {
8283
bucketing1.propagateForwardsByTime(1.0);
8384
}
8485

85-
maths::CDiurnalTime time2(10, 10, 120, 110);
86+
maths::CGeneralPeriodTime time2(120);
8687
maths::CSeasonalComponentAdaptiveBucketing bucketing2(time2, 0.1);
8788

8889
uint64_t checksum1 = bucketing1.checksum();
@@ -109,7 +110,7 @@ void CSeasonalComponentAdaptiveBucketingTest::testRefine() {
109110
double function[] = {10, 10, 10, 10, 100, 90, 80,
110111
90, 100, 20, 10, 10, 10, 10};
111112

112-
maths::CDiurnalTime time(0, 0, 86400, 86400);
113+
maths::CDiurnalTime time(0, 0, core::constants::WEEK, core::constants::DAY);
113114
maths::CSeasonalComponentAdaptiveBucketing bucketing1(time);
114115
maths::CSeasonalComponentAdaptiveBucketing bucketing2(time);
115116

@@ -188,7 +189,7 @@ void CSeasonalComponentAdaptiveBucketingTest::testRefine() {
188189
// Test that the variance in each bucket is approximately equal.
189190
// The test function is y = (x - 50)^2 / 50.
190191

191-
maths::CDiurnalTime time(0, 0, 100, 100);
192+
maths::CGeneralPeriodTime time(100);
192193
maths::CSeasonalComponentAdaptiveBucketing bucketing(time, 0.05);
193194

194195
bucketing.initialize(10);
@@ -294,7 +295,7 @@ void CSeasonalComponentAdaptiveBucketingTest::testPropagateForwardsByTime() {
294295
// the bucket values and that the rate at which the total
295296
// count is reduced uniformly.
296297

297-
maths::CDiurnalTime time(0, 0, 100, 100);
298+
maths::CGeneralPeriodTime time(100);
298299
maths::CSeasonalComponentAdaptiveBucketing bucketing(time, 0.2);
299300

300301
bucketing.initialize(10);
@@ -331,7 +332,7 @@ void CSeasonalComponentAdaptiveBucketingTest::testMinimumBucketLength() {
331332

332333
core_t::TTime period = static_cast<core_t::TTime>(n) *
333334
static_cast<core_t::TTime>(bucketLength);
334-
maths::CDiurnalTime time(0, 0, period, period);
335+
maths::CGeneralPeriodTime time(period);
335336
maths::CSeasonalComponentAdaptiveBucketing bucketing1(time, 0.0, 0.0);
336337
maths::CSeasonalComponentAdaptiveBucketing bucketing2(time, 0.0, 3000.0);
337338
bucketing1.initialize(n);
@@ -389,7 +390,7 @@ void CSeasonalComponentAdaptiveBucketingTest::testUnintialized() {
389390
// Check that all the functions work and return the expected
390391
// values on an uninitialized bucketing.
391392

392-
maths::CDiurnalTime time(0, 0, 10, 10);
393+
maths::CGeneralPeriodTime time(10);
393394
maths::CSeasonalComponentAdaptiveBucketing bucketing(time, 0.1);
394395

395396
bucketing.add(0, 1.0, 1.0);
@@ -436,7 +437,7 @@ void CSeasonalComponentAdaptiveBucketingTest::testKnots() {
436437

437438
LOG_DEBUG(<< "*** Values ***");
438439
{
439-
maths::CDiurnalTime time(0, 0, 86400, 86400);
440+
maths::CDiurnalTime time(0, 0, core::constants::WEEK, core::constants::DAY);
440441
maths::CSeasonalComponentAdaptiveBucketing bucketing(time, 0.1, 864.0);
441442

442443
bucketing.initialize(20);
@@ -479,7 +480,7 @@ void CSeasonalComponentAdaptiveBucketingTest::testKnots() {
479480
}
480481
LOG_DEBUG(<< "*** Variances ***");
481482
{
482-
maths::CDiurnalTime time(0, 0, 86400, 86400);
483+
maths::CDiurnalTime time(0, 0, core::constants::WEEK, core::constants::DAY);
483484
maths::CSeasonalComponentAdaptiveBucketing bucketing(time, 0.1, 864.0);
484485

485486
bucketing.initialize(20);
@@ -530,7 +531,7 @@ void CSeasonalComponentAdaptiveBucketingTest::testLongTermTrendKnots() {
530531

531532
test::CRandomNumbers rng;
532533

533-
maths::CDiurnalTime time(0, 0, 86400, 86400);
534+
maths::CDiurnalTime time(0, 0, core::constants::WEEK, core::constants::DAY);
534535
maths::CSeasonalComponentAdaptiveBucketing bucketing(time, 0.1, 864.0);
535536
maths::CSeasonalComponentAdaptiveBucketing::TFloatMeanAccumulatorVec empty;
536537

@@ -588,7 +589,7 @@ void CSeasonalComponentAdaptiveBucketingTest::testShiftValue() {
588589
// Test that applying a shift translates the predicted values
589590
// but doesn't alter the slope or predicted variances.
590591

591-
maths::CDiurnalTime time(0, 0, 86400, 86400);
592+
maths::CDiurnalTime time(0, 0, core::constants::WEEK, core::constants::DAY);
592593
maths::CSeasonalComponentAdaptiveBucketing bucketing(time, 0.1, 600.0);
593594
maths::CSeasonalComponentAdaptiveBucketing::TFloatMeanAccumulatorVec empty;
594595

@@ -632,7 +633,7 @@ void CSeasonalComponentAdaptiveBucketingTest::testShiftValue() {
632633
void CSeasonalComponentAdaptiveBucketingTest::testSlope() {
633634
// Test that the slope increases by the shift.
634635

635-
maths::CDiurnalTime time(0, 0, 86400, 86400);
636+
maths::CDiurnalTime time(0, 0, core::constants::WEEK, core::constants::DAY);
636637
maths::CSeasonalComponentAdaptiveBucketing bucketing(time, 0.1, 600.0);
637638
maths::CSeasonalComponentAdaptiveBucketing::TFloatMeanAccumulatorVec empty;
638639

@@ -669,7 +670,7 @@ void CSeasonalComponentAdaptiveBucketingTest::testPersist() {
669670
double decayRate = 0.1;
670671
double minimumBucketLength = 1.0;
671672

672-
maths::CDiurnalTime time(0, 0, 86400, 86400);
673+
maths::CDiurnalTime time(0, 0, core::constants::WEEK, core::constants::DAY);
673674
maths::CSeasonalComponentAdaptiveBucketing origBucketing(time, decayRate, minimumBucketLength);
674675

675676
origBucketing.initialize(10);
@@ -723,7 +724,7 @@ void CSeasonalComponentAdaptiveBucketingTest::testUpgrade() {
723724
double decayRate = 0.1;
724725
double minimumBucketLength = 1.0;
725726

726-
maths::CDiurnalTime time(0, 0, 86400, 86400);
727+
maths::CDiurnalTime time(0, 0, core::constants::WEEK, core::constants::DAY);
727728
maths::CSeasonalComponentAdaptiveBucketing expectedBucketing(time, decayRate, minimumBucketLength);
728729

729730
expectedBucketing.initialize(10);

0 commit comments

Comments
 (0)