lib/maths/time_series/CTrendComponent.cc (683 lines of code) (raw):

/* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0 and the following additional limitation. Functionality enabled by the * files subject to the Elastic License 2.0 may only be used in production when * invoked by an Elasticsearch process with a license key installed that permits * use of machine learning features. You may not use this file except in * compliance with the Elastic License 2.0 and the foregoing additional * limitation. */ #include <maths/time_series/CTrendComponent.h> #include <core/CLogger.h> #include <core/CStatePersistInserter.h> #include <core/CStateRestoreTraverser.h> #include <core/Constants.h> #include <core/RestoreMacros.h> #include <maths/common/CBasicStatistics.h> #include <maths/common/CChecksum.h> #include <maths/common/CIntegerTools.h> #include <maths/common/CLeastSquaresOnlineRegression.h> #include <maths/common/CLeastSquaresOnlineRegressionDetail.h> #include <maths/common/CLinearAlgebra.h> #include <maths/common/CNaiveBayes.h> #include <maths/common/COrderings.h> #include <maths/common/COrderingsSimultaneousSort.h> #include <maths/common/CSampling.h> #include <maths/common/CStatisticalTests.h> #include <maths/common/CTools.h> #include <boost/math/distributions/chi_squared.hpp> #include <boost/math/distributions/normal.hpp> #include <algorithm> #include <cmath> #include <exception> namespace ml { namespace maths { namespace time_series { namespace { using TVector2x1 = CTrendComponent::TVector2x1; const double TIME_SCALES[]{144.0, 72.0, 36.0, 12.0, 4.0, 1.0, 0.25, 0.05}; const std::size_t NUMBER_MODELS{std::size(TIME_SCALES)}; const double MINIMUM_WEIGHT_TO_USE_MODEL_FOR_PREDICTION{0.01}; const double MODEL_MSE_DECREASE_SIGNFICANT{0.05}; const double MAX_CONDITION{1e12}; const core_t::TTime UNSET_TIME{0}; const std::size_t NO_CHANGE_LABEL{0}; const std::size_t LEVEL_CHANGE_LABEL{1}; class CChangeForecastFeatureWeight : public common::CNaiveBayesFeatureWeight { public: void add(std::size_t class_, double logLikelihood) override { if (class_ == NO_CHANGE_LABEL) { m_LogLikelihood = logLikelihood; } } double calculate() const override { // Downweight features for which we don't have sufficient examples // of the time series not changing. // Note that m_LogLikelihood = 0.5 * (x - m)^2 / sigma^2 so 4.5 // corresponds to the case the feature value is at the 3 sigma // point of the conditional distribution. return common::CTools::logisticFunction((4.5 + m_LogLikelihood) / 4.5, 0.1); } private: double m_LogLikelihood{0.0}; }; //! Get the desired weight for the regression model. double modelWeight(double targetDecayRate, double modelDecayRate) { return targetDecayRate == modelDecayRate ? 1.0 : std::min(targetDecayRate, modelDecayRate) / std::max(targetDecayRate, modelDecayRate); } //! We scale the time used for the regression model to improve the condition //! of the design matrix. double scaleTime(core_t::TTime time, core_t::TTime origin) { return static_cast<double>(time - origin) / static_cast<double>(core::constants::WEEK); } //! Get the \p confidence interval for \p prediction and \p variance. TVector2x1 confidenceInterval(double prediction, double variance, double confidence) { if (variance > 0.0) { try { boost::math::normal normal{prediction, std::sqrt(variance)}; double ql{boost::math::quantile(normal, (100.0 - confidence) / 200.0)}; double qu{boost::math::quantile(normal, (100.0 + confidence) / 200.0)}; return TVector2x1{{ql, qu}}; } catch (const std::exception& e) { LOG_ERROR(<< "Failed calculating confidence interval: " << e.what() << ", prediction = " << prediction << ", variance = " << variance << ", confidence = " << confidence); } } return TVector2x1{prediction}; } common::CNaiveBayesFeatureDensityFromPrior naiveBayesExemplar(double decayRate) { return common::CNaiveBayesFeatureDensityFromPrior{common::CNormalMeanPrecConjugate::nonInformativePrior( maths_t::E_ContinuousData, TIME_SCALES[NUMBER_MODELS - 1] * decayRate)}; } common::CNaiveBayes initialProbabilityOfChangeModel(double decayRate) { return common::CNaiveBayes{naiveBayesExemplar(decayRate), TIME_SCALES[NUMBER_MODELS - 1] * decayRate}; } common::CNormalMeanPrecConjugate initialMagnitudeOfChangeModel(double decayRate) { return common::CNormalMeanPrecConjugate::nonInformativePrior( maths_t::E_ContinuousData, decayRate); } const std::string VERSION_7_1_TAG("7.1"); const core::TPersistenceTag TARGET_DECAY_RATE_TAG{"a", "target_decay_rate"}; const core::TPersistenceTag FIRST_UPDATE_TAG{"b", "first_update"}; const core::TPersistenceTag LAST_UPDATE_TAG{"c", "last_update"}; const core::TPersistenceTag REGRESSION_ORIGIN_TAG{"d", "regression_origin"}; const core::TPersistenceTag MODEL_TAG{"e", "model"}; const core::TPersistenceTag PREDICTION_ERROR_VARIANCE_TAG{"f", "prediction_error_variance"}; const core::TPersistenceTag VALUE_MOMENTS_TAG{"g", "value_moments"}; const core::TPersistenceTag TIME_OF_LAST_LEVEL_CHANGE_TAG{"h", "time_of_last_level_change"}; const core::TPersistenceTag PROBABILITY_OF_LEVEL_CHANGE_MODEL_TAG{ "i", "probability_of_level_change_model"}; const core::TPersistenceTag MAGNITUDE_OF_LEVEL_CHANGE_MODEL_TAG{"j", "magnitude_of_level_change_model"}; // Version 7.1 const core::TPersistenceTag WEIGHT_7_1_TAG{"a", "weight"}; const core::TPersistenceTag REGRESSION_7_1_TAG{"b", "regression"}; const core::TPersistenceTag MSE_7_1_TAG{"c", "mse"}; } CTrendComponent::CTrendComponent(double decayRate) : m_DefaultDecayRate(decayRate), m_TargetDecayRate(decayRate), m_FirstUpdate(UNSET_TIME), m_LastUpdate(UNSET_TIME), m_RegressionOrigin(UNSET_TIME), m_PredictionErrorVariance(0.0), m_TimeOfLastLevelChange(UNSET_TIME), m_ProbabilityOfLevelChangeModel(initialProbabilityOfChangeModel(decayRate)), m_MagnitudeOfLevelChangeModel(initialMagnitudeOfChangeModel(decayRate)) { for (std::size_t i = 0; i < NUMBER_MODELS; ++i) { m_TrendModels.emplace_back(modelWeight(1.0, TIME_SCALES[i])); } } void CTrendComponent::swap(CTrendComponent& other) { std::swap(m_DefaultDecayRate, other.m_DefaultDecayRate); std::swap(m_TargetDecayRate, other.m_TargetDecayRate); std::swap(m_FirstUpdate, other.m_FirstUpdate); std::swap(m_LastUpdate, other.m_LastUpdate); std::swap(m_RegressionOrigin, other.m_RegressionOrigin); m_TrendModels.swap(other.m_TrendModels); std::swap(m_PredictionErrorVariance, other.m_PredictionErrorVariance); std::swap(m_ValueMoments, other.m_ValueMoments); std::swap(m_TimeOfLastLevelChange, other.m_TimeOfLastLevelChange); m_ProbabilityOfLevelChangeModel.swap(other.m_ProbabilityOfLevelChangeModel); m_MagnitudeOfLevelChangeModel.swap(other.m_MagnitudeOfLevelChangeModel); } void CTrendComponent::acceptPersistInserter(core::CStatePersistInserter& inserter) const { inserter.insertValue(TARGET_DECAY_RATE_TAG, m_TargetDecayRate); inserter.insertValue(FIRST_UPDATE_TAG, m_FirstUpdate); inserter.insertValue(LAST_UPDATE_TAG, m_LastUpdate); inserter.insertValue(REGRESSION_ORIGIN_TAG, m_RegressionOrigin); for (const auto& model : m_TrendModels) { inserter.insertLevel(MODEL_TAG, [&model](auto& inserter_) { model.acceptPersistInserter(inserter_); }); } inserter.insertValue(PREDICTION_ERROR_VARIANCE_TAG, m_PredictionErrorVariance, core::CIEEE754::E_DoublePrecision); inserter.insertValue(VALUE_MOMENTS_TAG, m_ValueMoments.toDelimited()); inserter.insertValue(TIME_OF_LAST_LEVEL_CHANGE_TAG, m_TimeOfLastLevelChange); inserter.insertLevel(PROBABILITY_OF_LEVEL_CHANGE_MODEL_TAG, [this](auto& inserter_) { m_ProbabilityOfLevelChangeModel.acceptPersistInserter(inserter_); }); inserter.insertLevel(MAGNITUDE_OF_LEVEL_CHANGE_MODEL_TAG, [this](auto& inserter_) { m_MagnitudeOfLevelChangeModel.acceptPersistInserter(inserter_); }); } bool CTrendComponent::acceptRestoreTraverser(const common::SDistributionRestoreParams& params, core::CStateRestoreTraverser& traverser) { std::size_t i{0}; do { const std::string& name{traverser.name()}; RESTORE_BUILT_IN(TARGET_DECAY_RATE_TAG, m_TargetDecayRate) RESTORE_BUILT_IN(FIRST_UPDATE_TAG, m_FirstUpdate) RESTORE_BUILT_IN(LAST_UPDATE_TAG, m_LastUpdate) RESTORE_BUILT_IN(REGRESSION_ORIGIN_TAG, m_RegressionOrigin) RESTORE(MODEL_TAG, traverser.traverseSubLevel([&](auto& traverser_) { return m_TrendModels[i++].acceptRestoreTraverser(traverser_); })) RESTORE_BUILT_IN(PREDICTION_ERROR_VARIANCE_TAG, m_PredictionErrorVariance) RESTORE(VALUE_MOMENTS_TAG, m_ValueMoments.fromDelimited(traverser.value())) RESTORE_BUILT_IN(TIME_OF_LAST_LEVEL_CHANGE_TAG, m_TimeOfLastLevelChange) RESTORE_NO_ERROR(PROBABILITY_OF_LEVEL_CHANGE_MODEL_TAG, m_ProbabilityOfLevelChangeModel = common::CNaiveBayes( naiveBayesExemplar(m_DefaultDecayRate), params, traverser)) RESTORE_NO_ERROR(MAGNITUDE_OF_LEVEL_CHANGE_MODEL_TAG, m_MagnitudeOfLevelChangeModel = common::CNormalMeanPrecConjugate(params, traverser)) } while (traverser.next()); this->checkRestoredInvariants(); return true; } void CTrendComponent::checkRestoredInvariants() const { VIOLATES_INVARIANT(m_FirstUpdate, >, m_LastUpdate); } bool CTrendComponent::initialized() const { return m_LastUpdate != UNSET_TIME; } void CTrendComponent::clear() { m_FirstUpdate = UNSET_TIME; m_LastUpdate = UNSET_TIME; m_RegressionOrigin = UNSET_TIME; for (std::size_t i = 0; i < NUMBER_MODELS; ++i) { m_TrendModels[i] = SModel(modelWeight(1.0, TIME_SCALES[i])); } m_PredictionErrorVariance = 0.0; m_ValueMoments = TMeanVarAccumulator(); m_TimeOfLastLevelChange = UNSET_TIME; m_ProbabilityOfLevelChangeModel = initialProbabilityOfChangeModel(m_DefaultDecayRate); m_MagnitudeOfLevelChangeModel = initialMagnitudeOfChangeModel(m_DefaultDecayRate); } void CTrendComponent::shiftOrigin(core_t::TTime time) { time = common::CIntegerTools::floor(time, core::constants::WEEK); double scaledShift{scaleTime(time, m_RegressionOrigin)}; if (scaledShift > 0.0) { for (auto& model : m_TrendModels) { model.s_Regression.shiftAbscissa(-scaledShift); } m_RegressionOrigin = time; } } void CTrendComponent::shiftSlope(core_t::TTime time, double shift) { // This shifts all models gradients by a fixed amount whilst keeping // the prediction at time fixed. This avoids a jump in the current // predicted value as a result of adjusting the gradient. Otherwise, // this changes by "shift" x "scaled time since the regression origin". if (std::fabs(shift) > 0.0) { for (std::size_t i = 0; i < NUMBER_MODELS; ++i) { double modelDecayRate{m_DefaultDecayRate * TIME_SCALES[i]}; double shift_{std::min(modelDecayRate / m_TargetDecayRate, 1.0) * shift}; m_TrendModels[i].s_Regression.shiftGradient(shift_); m_TrendModels[i].s_Regression.shiftOrdinate( -shift_ * scaleTime(time, m_RegressionOrigin)); } } } void CTrendComponent::shiftLevel(double shift) { for (auto& model : m_TrendModels) { model.s_Regression.shiftOrdinate(shift); } } void CTrendComponent::shiftLevel(double shift, core_t::TTime valuesStartTime, core_t::TTime bucketLength, const TFloatMeanAccumulatorVec& values, const TSizeVec& segments, const TDoubleVec& shifts) { this->shiftLevel(shift); if (segments.size() <= 2) { m_MagnitudeOfLevelChangeModel.addSamples({shift}, maths_t::CUnitWeights::SINGLE_UNIT); return; } auto indexTime = [&](std::size_t i) { return valuesStartTime + bucketLength * static_cast<core_t::TTime>(i); }; // If the last change is in the values' time window the next change // is the first one after the closest. Otherwise, it is the first // change. auto next = std::min( static_cast<std::size_t>( m_TimeOfLastLevelChange < valuesStartTime ? 1 : std::min_element(segments.begin() + 1, segments.end() - 1, [&](auto lhs, auto rhs) { return std::abs(indexTime(lhs) - m_TimeOfLastLevelChange) < std::abs(indexTime(rhs) - m_TimeOfLastLevelChange); }) - segments.begin() + 1), segments.size() - 2); std::size_t last{shifts.size() - 1}; core_t::TTime time{valuesStartTime + static_cast<core_t::TTime>(segments[last]) * bucketLength}; double magnitude{shifts[last] - shifts[next - 1]}; if (m_TimeOfLastLevelChange != UNSET_TIME) { double dt{static_cast<double>(time - m_TimeOfLastLevelChange)}; if (values.size() > segments[next] - 1) { double value{static_cast<double>( common::CBasicStatistics::mean(values[segments[next] - 1]))}; m_ProbabilityOfLevelChangeModel.addTrainingDataPoint(LEVEL_CHANGE_LABEL, {{dt}, {value}}); } else { LOG_DEBUG(<< "Size mis-match reading from values. Length = " << values.size() << ", requested index = " << segments[next] - 1); } } m_TimeOfLastLevelChange = time; for (std::size_t i = segments[last]; i < values.size(); ++i, time += bucketLength) { this->dontShiftLevel(time, common::CBasicStatistics::mean(values[i])); } m_MagnitudeOfLevelChangeModel.addSamples({magnitude}, maths_t::CUnitWeights::SINGLE_UNIT); } void CTrendComponent::dontShiftLevel(core_t::TTime time, double value) { if (m_TimeOfLastLevelChange != UNSET_TIME) { double dt{static_cast<double>(time - m_TimeOfLastLevelChange)}; m_ProbabilityOfLevelChangeModel.addTrainingDataPoint(NO_CHANGE_LABEL, {{dt}, {value}}); } } void CTrendComponent::linearScale(core_t::TTime time, double scale) { double shift{(scale - 1.0) * this->value(time, 0.0).mean()}; this->shiftLevel(shift); } void CTrendComponent::add(core_t::TTime time, double value, double weight) { // Update the model weights: we weight the components based on the // relative difference in the component scale and the target scale. for (std::size_t i = 0; i < NUMBER_MODELS; ++i) { m_TrendModels[i].s_Weight.add( modelWeight(m_TargetDecayRate, m_DefaultDecayRate * TIME_SCALES[i])); } if (m_FirstUpdate == UNSET_TIME) { m_RegressionOrigin = common::CIntegerTools::floor(time, core::constants::WEEK); } // Update the models. double prediction{this->value(time, 0.0).mean()}; double count{this->count()}; if (count > 0.0) { TMeanVarAccumulator moments{common::CBasicStatistics::momentsAccumulator( count, prediction, m_PredictionErrorVariance)}; moments.add(value, weight); m_PredictionErrorVariance = common::CBasicStatistics::maximumLikelihoodVariance(moments); } double scaledTime{scaleTime(time, m_RegressionOrigin)}; for (auto& model : m_TrendModels) { TVector3x1 mse; for (std::size_t order = 1; order <= TRegression::N; ++order) { mse(order - 1) = value - model.s_Regression.predict(order, scaledTime, MAX_CONDITION); } model.s_Mse.add(mse * mse, weight); model.s_Regression.add(scaledTime, value, weight); } m_ValueMoments.add(value, weight); m_FirstUpdate = m_FirstUpdate == UNSET_TIME ? time : std::min(m_FirstUpdate, time); m_LastUpdate = std::max(m_LastUpdate, time); } void CTrendComponent::dataType(maths_t::EDataType dataType) { m_ProbabilityOfLevelChangeModel.dataType(dataType); m_MagnitudeOfLevelChangeModel.dataType(dataType); } double CTrendComponent::defaultDecayRate() const { return m_DefaultDecayRate; } void CTrendComponent::decayRate(double decayRate) { m_TargetDecayRate = decayRate; } void CTrendComponent::propagateForwardsByTime(core_t::TTime interval) { TDoubleVec factors; this->smoothingFactors(interval, factors); double median{common::CBasicStatistics::median(factors)}; for (std::size_t i = 0; i < NUMBER_MODELS; ++i) { m_TrendModels[i].s_Weight.age(median); m_TrendModels[i].s_Regression.age(factors[i]); m_TrendModels[i].s_Mse.age(std::sqrt(factors[i])); } double interval_{static_cast<double>(interval) / static_cast<double>(core::constants::DAY)}; m_ProbabilityOfLevelChangeModel.propagateForwardsByTime(interval_); m_MagnitudeOfLevelChangeModel.propagateForwardsByTime(interval_); } CTrendComponent::TVector2x1 CTrendComponent::value(core_t::TTime time, double confidence) const { if (this->initialized() == false) { return TVector2x1{0.0}; } double a{this->weightOfPrediction(time)}; double b{1.0 - a}; double scaledTime{scaleTime(time, m_RegressionOrigin)}; TMeanAccumulator prediction_; TDoubleVec weights; this->smoothingFactors(std::abs(time - m_LastUpdate), weights); double Z{0.0}; for (std::size_t i = 0; i < NUMBER_MODELS; ++i) { weights[i] *= common::CBasicStatistics::mean(m_TrendModels[i].s_Weight); Z += weights[i]; } for (std::size_t i = 0; i < NUMBER_MODELS; ++i) { if (weights[i] > MINIMUM_WEIGHT_TO_USE_MODEL_FOR_PREDICTION * Z) { prediction_.add(m_TrendModels[i].s_Regression.predict(scaledTime, MAX_CONDITION), weights[i]); } } double prediction{a * common::CBasicStatistics::mean(prediction_) + b * common::CBasicStatistics::mean(m_ValueMoments)}; if (confidence > 0.0 && m_PredictionErrorVariance > 0.0) { double variance{a * m_PredictionErrorVariance / std::max(this->count(), 1.0) + b * common::CBasicStatistics::variance(m_ValueMoments) / std::max(common::CBasicStatistics::count(m_ValueMoments), 1.0)}; return confidenceInterval(prediction, variance, confidence); } return TVector2x1{prediction}; } CTrendComponent::TPredictor CTrendComponent::predictor() const { if (this->initialized() == false) { return [](core_t::TTime) { return 0.0; }; } TDoubleVec weights; TRegressionArrayVec parameters(NUMBER_MODELS); for (std::size_t i = 0; i < NUMBER_MODELS; ++i) { m_TrendModels[i].s_Regression.parameters(parameters[i], MAX_CONDITION); } return [weights, parameters, this](core_t::TTime time) mutable { TMeanAccumulator prediction; double a{this->weightOfPrediction(time)}; double b{1.0 - a}; double scaledTime{scaleTime(time, m_RegressionOrigin)}; this->smoothingFactors(std::abs(time - m_LastUpdate), weights); double Z{0.0}; for (std::size_t i = 0; i < NUMBER_MODELS; ++i) { weights[i] *= common::CBasicStatistics::mean(m_TrendModels[i].s_Weight); Z += weights[i]; } for (std::size_t i = 0; i < NUMBER_MODELS; ++i) { if (weights[i] > MINIMUM_WEIGHT_TO_USE_MODEL_FOR_PREDICTION * Z) { prediction.add(m_TrendModels[i].s_Regression.predict(parameters[i], scaledTime), weights[i]); } } return a * common::CBasicStatistics::mean(prediction) + b * common::CBasicStatistics::mean(m_ValueMoments); }; } core_t::TTime CTrendComponent::maximumForecastInterval() const { double timescale{static_cast<double>(core::constants::DAY)}; double interval{std::min( 1.0 / (1.0 - std::exp(-TIME_SCALES[NUMBER_MODELS - 1] * m_DefaultDecayRate)), std::floor(static_cast<double>(std::numeric_limits<core_t::TTime>::max()) / timescale))}; return static_cast<core_t::TTime>(interval * timescale); } CTrendComponent::TVector2x1 CTrendComponent::variance(double confidence) const { if (this->initialized() == false) { return TVector2x1{0.0}; } double variance{m_PredictionErrorVariance}; if (confidence > 0.0 && m_PredictionErrorVariance > 0.0) { double df{std::max(this->count(), 2.0) - 1.0}; try { boost::math::chi_squared chi{df}; double ql{boost::math::quantile(chi, (100.0 - confidence) / 200.0)}; double qu{boost::math::quantile(chi, (100.0 + confidence) / 200.0)}; return TVector2x1{{ql * variance / df, qu * variance / df}}; } catch (const std::exception& e) { LOG_ERROR(<< "Failed calculating confidence interval: " << e.what() << ", df = " << df << ", confidence = " << confidence); } } return TVector2x1{variance}; } void CTrendComponent::forecast(core_t::TTime startTime, core_t::TTime endTime, core_t::TTime step, double confidence, bool isNonNegative, const TSeasonalForecast& seasonal, const TWriteForecastResult& writer) const { if (endTime < startTime) { LOG_ERROR(<< "Bad forecast range: [" << startTime << "," << endTime << "]"); return; } if (confidence < 0.0 || confidence >= 100.0) { LOG_ERROR(<< "Bad confidence interval: " << confidence << "%"); return; } endTime = startTime + common::CIntegerTools::ceil(endTime - startTime, step); LOG_TRACE(<< "forecasting = " << this->print()); TSizeVec selectedModelOrders(this->selectModelOrdersForForecasting()); LOG_TRACE(<< "Selected model orders = " << selectedModelOrders); TDoubleVec factors; this->smoothingFactors(step, factors); TDoubleVec modelWeights(this->initialForecastModelWeights(NUMBER_MODELS)); TDoubleVec errorWeights(this->initialForecastModelWeights(NUMBER_MODELS + 1)); TRegressionArrayVec models(NUMBER_MODELS); TMatrix3x3Vec modelCovariances(NUMBER_MODELS); TDoubleVec mse(NUMBER_MODELS); for (std::size_t i = 0; i < NUMBER_MODELS; ++i) { // Note in the following we multiply the bias by the sample count // when estimating the regression coefficients' covariance matrix. // This is because, unlike the noise like component of the residual, // it is overly optimistic to assume these will cancel in the // estimates of the regression coefficients when adding multiple // samples. Instead, we make the most pessimistic assumption that // this term is from a fixed random variable and so multiply its // variance by the number of samples to undo the scaling applied // in the regression model covariance method. const SModel& model{m_TrendModels[i]}; std::size_t order{selectedModelOrders[i]}; double n{model.s_Regression.count()}; mse[i] = common::CBasicStatistics::mean(model.s_Mse)(order - 1); model.s_Regression.parameters(order, models[i], MAX_CONDITION); model.s_Regression.covariances(order, n * mse[i], modelCovariances[i], MAX_CONDITION); LOG_TRACE(<< "params = " << models[i]); LOG_TRACE(<< "covariances = " << modelCovariances[i].toDelimited()); LOG_TRACE(<< "mse = " << mse[i]); } LOG_TRACE(<< "long time variance = " << common::CBasicStatistics::variance(m_ValueMoments)); CForecastLevel level{m_ProbabilityOfLevelChangeModel, m_MagnitudeOfLevelChangeModel, m_TimeOfLastLevelChange}; TDoubleVec variances(NUMBER_MODELS + 1); for (core_t::TTime time = startTime; time < endTime; time += step) { double scaledDt{scaleTime(time, startTime)}; TVector3x1 times({0.0, scaledDt, scaledDt * scaledDt}); double a{this->weightOfPrediction(time)}; double b{1.0 - a}; for (std::size_t j = 0; j < NUMBER_MODELS; ++j) { modelWeights[j] *= factors[j]; errorWeights[j] *= common::CTools::pow2(factors[j]); } for (std::size_t j = 0; j < NUMBER_MODELS; ++j) { variances[j] = times.inner(modelCovariances[j] * times) + mse[j]; } variances[NUMBER_MODELS] = common::CBasicStatistics::variance(m_ValueMoments); for (auto v = variances.rbegin(); v != variances.rend(); ++v) { *v = *std::min_element(variances.rbegin(), v + 1); } TMeanAccumulator variance_; for (std::size_t j = 0; j < NUMBER_MODELS; ++j) { variance_.add(variances[j], errorWeights[j]); } double variance{a * common::CBasicStatistics::mean(variance_) + b * common::CBasicStatistics::variance(m_ValueMoments)}; TVector2x1 trend{confidenceInterval( this->value(modelWeights, models, scaleTime(time, m_RegressionOrigin)), variance, confidence)}; TDouble3Vec seasonal_(seasonal(time)); TDouble3Vec level_(level.forecast(time, seasonal_[1] + trend.mean(), confidence)); TDouble3Vec forecast{level_[0] + trend(0) + seasonal_[0], level_[1] + trend.mean() + seasonal_[1], level_[2] + trend(1) + seasonal_[2]}; forecast[0] = isNonNegative ? std::max(forecast[0], 0.0) : forecast[0]; forecast[1] = isNonNegative ? std::max(forecast[1], 0.0) : forecast[1]; forecast[2] = isNonNegative ? std::max(forecast[2], 0.0) : forecast[2]; writer(time, forecast); } } core_t::TTime CTrendComponent::observedInterval() const { return m_LastUpdate - m_FirstUpdate; } double CTrendComponent::parameters() const { return static_cast<double>(TRegression::N); } std::uint64_t CTrendComponent::checksum(std::uint64_t seed) const { seed = common::CChecksum::calculate(seed, m_TargetDecayRate); seed = common::CChecksum::calculate(seed, m_FirstUpdate); seed = common::CChecksum::calculate(seed, m_LastUpdate); seed = common::CChecksum::calculate(seed, m_TrendModels); seed = common::CChecksum::calculate(seed, m_PredictionErrorVariance); seed = common::CChecksum::calculate(seed, m_ValueMoments); seed = common::CChecksum::calculate(seed, m_TimeOfLastLevelChange); seed = common::CChecksum::calculate(seed, m_ProbabilityOfLevelChangeModel); return common::CChecksum::calculate(seed, m_MagnitudeOfLevelChangeModel); } std::string CTrendComponent::print() const { std::ostringstream result; result << "\n===\n"; result << "Trend Models:"; for (const auto& model : m_TrendModels) { result << "\n" << model.s_Regression.print(); } result << "\n===\n"; result << "Probability of Change Model:"; result << m_ProbabilityOfLevelChangeModel.print(); result << "===\n"; result << "Magnitude of Change Model:"; result << m_MagnitudeOfLevelChangeModel.print(); return result.str(); } void CTrendComponent::smoothingFactors(core_t::TTime interval, TDoubleVec& result) const { result.assign(NUMBER_MODELS, 0.0); double factor{m_DefaultDecayRate * static_cast<double>(interval) / static_cast<double>(core::constants::DAY)}; for (std::size_t i = 0; i < NUMBER_MODELS; ++i) { result[i] = std::exp(-TIME_SCALES[i] * factor); } } CTrendComponent::TSizeVec CTrendComponent::selectModelOrdersForForecasting() const { // We test the models in order of increasing complexity and require // reasonable evidence to select a more complex model. TSizeVec result(NUMBER_MODELS, 1); for (std::size_t i = 0; i < NUMBER_MODELS; ++i) { const SModel& model{m_TrendModels[i]}; double n{common::CBasicStatistics::count(model.s_Mse)}; double mse0{common::CBasicStatistics::mean(model.s_Mse)(0)}; double df0{n - 1.0}; for (std::size_t order = 2; mse0 > 0.0 && order <= TRegression::N; ++order) { double mse1{common::CBasicStatistics::mean(model.s_Mse)(order - 1)}; double df1{n - static_cast<double>(order)}; if (df1 < 0.0) { break; } double p{common::CStatisticalTests::leftTailFTest(mse1, mse0, df1, df0)}; if (p < MODEL_MSE_DECREASE_SIGNFICANT) { result[i] = order; mse0 = mse1; df0 = df1; } } } return result; } CTrendComponent::TDoubleVec CTrendComponent::initialForecastModelWeights(std::size_t n) const { TDoubleVec result(n); for (std::size_t i = 0; i < n; ++i) { result[i] = std::exp(static_cast<double>(NUMBER_MODELS / 2) - static_cast<double>(i)); } return result; } double CTrendComponent::count() const { TMeanAccumulator result; for (const auto& model : m_TrendModels) { result.add(common::CTools::fastLog(model.s_Regression.count()), common::CBasicStatistics::mean(model.s_Weight)); } return std::exp(common::CBasicStatistics::mean(result)); } double CTrendComponent::value(const TDoubleVec& weights, const TRegressionArrayVec& models, double time) const { TMeanAccumulator prediction; for (std::size_t i = 0; i < models.size(); ++i) { prediction.add(TRegression::predict(models[i], time), weights[i]); } return common::CBasicStatistics::mean(prediction); } double CTrendComponent::weightOfPrediction(core_t::TTime time) const { double interval{static_cast<double>(m_LastUpdate - m_FirstUpdate)}; if (interval == 0.0) { return 0.0; } double extrapolateInterval{static_cast<double>(common::CBasicStatistics::max( time - m_LastUpdate, m_FirstUpdate - time, core_t::TTime(0)))}; if (extrapolateInterval == 0.0) { return 1.0; } return common::CTools::logisticFunction(extrapolateInterval / interval, 0.1, 1.0, -1.0); } CTrendComponent::SModel::SModel(double weight) { s_Weight.add(weight, 0.01); } void CTrendComponent::SModel::acceptPersistInserter(core::CStatePersistInserter& inserter) const { inserter.insertValue(VERSION_7_1_TAG, ""); inserter.insertValue(WEIGHT_7_1_TAG, s_Weight.toDelimited()); inserter.insertLevel(REGRESSION_7_1_TAG, [this](auto& inserter_) { s_Regression.acceptPersistInserter(inserter_); }); inserter.insertValue(MSE_7_1_TAG, s_Mse.toDelimited()); } bool CTrendComponent::SModel::acceptRestoreTraverser(core::CStateRestoreTraverser& traverser) { if (traverser.name() == VERSION_7_1_TAG) { while (traverser.next()) { const std::string& name{traverser.name()}; RESTORE(WEIGHT_7_1_TAG, s_Weight.fromDelimited(traverser.value())) RESTORE(REGRESSION_7_1_TAG, traverser.traverseSubLevel([this](auto& traverser_) { return s_Regression.acceptRestoreTraverser(traverser_); })) RESTORE(MSE_7_1_TAG, s_Mse.fromDelimited(traverser.value())) } } else { LOG_ERROR(<< "Input error: unsupported state serialization version '" << traverser.name() << "'. Currently supported minimum version: " << VERSION_7_1_TAG); return false; } return true; } std::uint64_t CTrendComponent::SModel::checksum(std::uint64_t seed) const { seed = common::CChecksum::calculate(seed, s_Weight); seed = common::CChecksum::calculate(seed, s_Regression); return common::CChecksum::calculate(seed, s_Mse); } CTrendComponent::CForecastLevel::CForecastLevel(const common::CNaiveBayes& probability, const common::CNormalMeanPrecConjugate& magnitude, core_t::TTime timeOfLastChange, std::size_t numberPaths) : m_Probability{probability}, m_Magnitude{magnitude}, m_Levels(numberPaths, 0.0), m_TimesOfLastChange(numberPaths, timeOfLastChange), m_ProbabilitiesOfChange(numberPaths, 0.0) { m_Uniform01.reserve(numberPaths); } CTrendComponent::TDouble3Vec CTrendComponent::CForecastLevel::forecast(core_t::TTime time, double prediction, double confidence) { TDouble3Vec result{0.0, 0.0, 0.0}; if (m_Probability.initialized() && m_Probability.numberClasses() > 1) { common::CSampling::uniformSample(0.0, 1.0, m_Levels.size(), m_Uniform01); bool reorder{false}; auto weightProvider = [weight = CChangeForecastFeatureWeight{}]() mutable->common::CNaiveBayesFeatureWeight& { weight = CChangeForecastFeatureWeight{}; return weight; }; for (std::size_t i = 0; i < m_Levels.size(); ++i) { double dt{static_cast<double>(time - m_TimesOfLastChange[i])}; double x{m_Levels[i] + prediction}; auto[p, pConfidence] = m_Probability.classProbability( LEVEL_CHANGE_LABEL, {{dt}, {x}}, weightProvider); // Here we decide whether to increase the probability we should have // seen a step change for this rollout. If we are no longer confident // in our predicted probability we do not predict changes based on // the principle of least surprise. if (pConfidence > 0.5) { m_ProbabilitiesOfChange[i] = std::max(m_ProbabilitiesOfChange[i], p); } if (m_Uniform01[i] < m_ProbabilitiesOfChange[i]) { double stepMean{m_Magnitude.marginalLikelihoodMean()}; double stepVariance{m_Magnitude.marginalLikelihoodVariance()}; m_Levels[i] += common::CSampling::normalSample(m_Rng, stepMean, stepVariance); m_TimesOfLastChange[i] = time; m_ProbabilitiesOfChange[i] = 0.0; reorder = true; } } if (reorder) { common::COrderings::simultaneousSort(m_Levels, m_TimesOfLastChange, m_ProbabilitiesOfChange); } double rollouts{static_cast<double>(m_Levels.size())}; std::size_t lower{std::min( static_cast<std::size_t>((100.0 - confidence) / 200.0 * rollouts + 0.5), m_Levels.size())}; std::size_t upper{std::min( static_cast<std::size_t>((100.0 + confidence) / 200.0 * rollouts + 0.5), m_Levels.size() - 1)}; result[0] = m_Levels[lower]; result[1] = common::CBasicStatistics::median(m_Levels); result[2] = m_Levels[upper]; } return result; } } } }