Documentation
¶
Overview ¶
Package foresight forecasts time series and picks its model by what would have worked.
It fits several models to a series, replays the past to see how each would have done (a rolling-origin backtest), chooses by out-of-sample error and reports intervals taken from the errors actually observed, including intervals for the total of the next k periods. It has no dependencies outside the standard library and every result is deterministic.
y := foresight.Monthly(values, 2) // first observation in March
report, err := foresight.DefaultBacktest().Run(y, foresight.Defaults())
if err != nil {
log.Fatal(err)
}
best := report.Best()
for _, p := range best.Forecast {
i, _ := p.Interval(0.80)
fmt.Println(p.Horizon, p.Mean, i.Lower, i.Upper)
}
total, _ := best.Cumulative(6) // the next six periods, with its own interval
A single model is used through the Model interface:
fit, err := foresight.Theta{}.Fit(foresight.NonSeasonal(values))
next := fit.Forecast(3)
This is the Go edition of the Rust crate of the same name (https://crates.io/crates/foresight). The two are checked against each other and against the R packages forecast and prophet on public data.
Example ¶
Replay the past with every built-in model, take the best and forecast.
package main
import (
"fmt"
foresight "github.com/milkway/foresight-go"
)
// Eight years of monthly data with growth, a yearly pattern and some noise.
func sales() []float64 {
pattern := []float64{1.1, 0.8, 0.9, 1.0, 1.0, 0.9, 1.0, 1.0, 0.9, 1.0, 1.1, 1.3}
out := make([]float64, 96)
level := 100.0
for t := range out {
noise := float64((t*37)%11-5) * 1.5
out[t] = level*pattern[t%12] + noise
level += 0.5
}
return out
}
func main() {
y := foresight.Monthly(sales(), 0) // the first observation is in January
report, err := foresight.DefaultBacktest().Run(y, foresight.Defaults())
if err != nil {
fmt.Println(err)
return
}
best := report.Best()
fmt.Printf("%s, MAPE %.1f%%\n", best.Name, best.Score)
for _, p := range best.Forecast[:3] {
i, _ := p.Interval(0.80)
fmt.Printf("%d: %.1f [%.1f, %.1f]\n", p.Horizon, p.Mean, i.Lower, i.Upper)
}
total, _ := best.Cumulative(6)
fmt.Printf("next six months: %.0f\n", total.Mean)
}
Output: log_linear, MAPE 3.1% 1: 164.2 [154.7, 172.1] 2: 120.2 [113.5, 125.3] 3: 136.7 [128.8, 142.6] next six months: 862
Index ¶
- Variables
- func ACF(y []float64, maxLag int) []float64
- func Bias(actual, forecast []float64) float64
- func Forecast(m Model, y Series, h int) ([]float64, error)
- func MAE(actual, forecast []float64) float64
- func MAPE(actual, forecast []float64) float64
- func MASE(actual, forecast []float64, scale float64) float64
- func MASEScale(train []float64, period int) (float64, bool)
- func Quantile(v []float64, p float64) (float64, bool)
- func RMSE(actual, forecast []float64) float64
- type Backtest
- type Band
- type Candidate
- type CandidateReport
- type Drift
- type Fitted
- type HoltWinters
- type HorizonStats
- type Interval
- type LogLinear
- type Mean
- type Metric
- type Model
- type Naive
- type Param
- type Point
- type Report
- type SeasonalNaive
- type Series
- func (s Series) Head(n int) Series
- func (s Series) Index(i int) int
- func (s Series) IsFinite() bool
- func (s Series) IsPositive() bool
- func (s Series) Len() int
- func (s Series) Period() int
- func (s Series) Season(i int) int
- func (s Series) Slice(from, to int) Series
- func (s Series) Start() int
- func (s Series) Tail(n int) Series
- func (s Series) Values() []float64
- func (s Series) WithPhase(phase int) Series
- func (s Series) WithValues(values []float64) (Series, error)
- type Theta
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrFewOrigins: the series is too short for at least one pair at the // longest horizon. ErrFewOrigins = errors.New("foresight: series too short for the backtest") // ErrNoCandidate: no candidate could be fitted at every origin. ErrNoCandidate = errors.New("foresight: no candidate could be fitted at every origin") )
Errors of a backtest.
var ( // ErrTooShort: the series has too few observations for the model. ErrTooShort = errors.New("foresight: series too short for the model") // ErrNotFinite: the series has values that are not finite numbers. ErrNotFinite = errors.New("foresight: series has values that are not finite") // ErrNotPositive: the model needs values greater than zero. ErrNotPositive = errors.New("foresight: model needs positive values") // ErrNotSeasonal: the model needs a seasonal period of at least 2. ErrNotSeasonal = errors.New("foresight: model needs a seasonal series") // ErrNoFit: the estimation did not reach a usable result. ErrNoFit = errors.New("foresight: model could not be fitted") )
Errors returned by the models when a series does not suit them. A model never makes up a fit.
var ErrLength = errors.New("foresight: lengths differ")
ErrLength is returned when two things that must have the same number of observations do not.
Functions ¶
func Bias ¶
Bias is the mean of (forecast − actual) / actual, in percent: positive when the forecasts run above what happened. Pairs whose actual value is zero are left out.
func MAPE ¶
MAPE is the mean absolute percentage error, in percent. Pairs whose actual value is zero are left out. NaN when there is no pair.
func MASEScale ¶
MASEScale is the scale of the mean absolute scaled error: the in-sample mean absolute error of the seasonal naive forecast (naive when period is 1). It reports false when the training data is no longer than one period or the scale is zero.
Types ¶
type Backtest ¶
type Backtest struct {
// Origins is how many of the last periods are forecast origins
// (default 36).
Origins int
// Horizon is the longest horizon forecast and evaluated (default 12).
Horizon int
// MinTrain is the number of training observations at the first origin
// (default 48). Shorter series get fewer origins.
MinTrain int
// Window trains on the last Window observations only; zero uses
// everything before the origin.
Window int
// Combine also evaluates the simple average of the best Combine models
// (default 2; 1 disables it).
Combine int
// Levels is the coverage of the intervals, such as 0.8 for the 10%–90%
// quantiles (default 0.80 and 0.95).
Levels []float64
// Metric ranks the candidates (default RankByMAPE).
Metric Metric
// Sequential fits the origins one at a time instead of on all cores. The
// result is the same either way.
Sequential bool
}
Backtest is the configuration of a rolling-origin evaluation.
- Rolling origin: for each of the last Origins periods, every candidate is fitted ONLY on the data before the origin and forecasts 1 to Horizon periods ahead; errors are measured against what happened. Horizon h has Origins − h + 1 pairs.
- Selection: the candidate with the lowest error averaged over the horizons, the simple average of the best few models included.
- Empirical intervals: no normality assumed. For each horizon, the quantiles of the relative error r = actual/forecast − 1 seen in the backtest; the interval is forecast × (1 + q).
- Cumulative intervals: the same for the ratio of sums over the first k periods of each origin, because adding up the limits of k intervals overstates the uncertainty of a total.
The zero value of each field stands for its default; DefaultBacktest returns them spelled out.
func DefaultBacktest ¶
func DefaultBacktest() Backtest
DefaultBacktest returns the defaults, which suit monthly data: 36 origins, 12 periods ahead, at least 48 observations to train, the average of the two best models, intervals of 80% and 95%.
func (Backtest) Run ¶
Run evaluates the candidates on y, chooses one and forecasts Horizon periods with every candidate that went through the whole backtest.
It returns ErrFewOrigins when the series is too short for at least one pair at the longest horizon, and ErrNoCandidate when no candidate could be fitted at every origin.
type Band ¶
type Band struct {
Level, Lower, Upper float64
}
Band holds the quantiles of a relative error r (actual/forecast − 1) for one coverage level: the interval is forecast × (1 + Lower) to forecast × (1 + Upper).
type Candidate ¶
Candidate is a model taking part in a backtest, under a name of the caller's choice.
Example ¶
A model of your own joins the backtest by implementing Model.
package main
import (
"fmt"
foresight "github.com/milkway/foresight-go"
)
// Eight years of monthly data with growth, a yearly pattern and some noise.
func sales() []float64 {
pattern := []float64{1.1, 0.8, 0.9, 1.0, 1.0, 0.9, 1.0, 1.0, 0.9, 1.0, 1.1, 1.3}
out := make([]float64, 96)
level := 100.0
for t := range out {
noise := float64((t*37)%11-5) * 1.5
out[t] = level*pattern[t%12] + noise
level += 0.5
}
return out
}
func main() {
candidates := []foresight.Candidate{
foresight.NewCandidate(foresight.SeasonalNaive{}).Named("same_month", "Same month of last year"),
foresight.NewCandidate(foresight.LogLinear{Window: 60}),
}
report, err := foresight.Backtest{Combine: 1}.Run(foresight.Monthly(sales(), 0), candidates)
if err != nil {
fmt.Println(err)
return
}
for _, c := range report.Candidates {
fmt.Printf("%s: MAPE %.1f%%\n", c.Name, c.Score)
}
}
Output: same_month: MAPE 6.6% log_linear: MAPE 3.3%
func Defaults ¶
func Defaults() []Candidate
Defaults returns a reasonable set of candidates for a backtest: the benchmarks and every model that needs no external data and fits in a moment.
func NewCandidate ¶
NewCandidate returns a candidate with the model's own name and description.
type CandidateReport ¶
type CandidateReport struct {
Name string
Description string
// Components are the names of the models involved: one, or several for
// an average.
Components []string
// Horizons has the measures by horizon; index 0 is horizon 1.
Horizons []HorizonStats
// Score is the ranking metric averaged over the horizons; +Inf when it
// could not be computed.
Score float64
// Params are the parameters fitted on the whole series (none for an
// average).
Params []Param
// Trajectories are the backtest forecasts, [origin][h − 1].
Trajectories [][]float64
// Forecast is the forecast from the whole series, with the intervals of
// the backtest.
Forecast []Point
}
CandidateReport is the backtest and the forecast of one candidate (a model or an average of models).
func (CandidateReport) Cumulative ¶
func (c CandidateReport) Cumulative(k int) (Point, bool)
Cumulative returns the forecast of the SUM of the first k periods, with intervals from the backtest of sums. It reports false when k is under 1 or beyond the horizon.
type Drift ¶
type Drift struct{}
Drift is the random walk with drift: the last observation plus the average change per period over the history.
func (Drift) Description ¶
type Fitted ¶
type Fitted interface {
// Forecast returns point forecasts for the h periods after the last
// observation.
Forecast(h int) []float64
// Params returns the estimated parameters.
Params() []Param
}
Fitted is a model estimated on a series.
type HoltWinters ¶
type HoltWinters struct{}
HoltWinters is exponential smoothing with level, damped additive trend (φ) and multiplicative seasonal indices.
α, β, γ and φ come from a grid search (3,040 combinations) minimising the sum of squared relative one-step-ahead errors on the training data, the first cycle left out as warm-up. It needs a seasonal period of at least 2, positive values and three full cycles.
func (HoltWinters) Description ¶
func (HoltWinters) Description() string
func (HoltWinters) Name ¶
func (HoltWinters) Name() string
type HorizonStats ¶
type HorizonStats struct {
// N is the number of forecast/actual pairs evaluated.
N int
// MAPE is the mean absolute percentage error, in percent.
MAPE float64
// Bias is the mean of (forecast − actual)/actual, in percent; positive
// when the forecasts ran high.
Bias float64
MAE float64
RMSE float64
// MASE is the mean absolute error scaled by the in-sample seasonal naive
// error of each training set.
MASE float64
// Bands are the quantiles of the relative error at this horizon, one per
// level.
Bands []Band
// Cumulative are the quantiles of the relative error of the SUM of the
// first h periods.
Cumulative []Band
}
HorizonStats is what the backtest measured at one horizon. Measures that could not be computed are NaN.
type Interval ¶
type Interval struct {
Level, Lower, Upper float64
}
Interval is an interval around a forecast.
type LogLinear ¶
type LogLinear struct {
// Window is the number of most recent observations used in the fit.
// Zero means six cycles, at least 24: enough to catch the recent trend
// without dragging old breaks along.
Window int
// Deflator is a price index aligned with the ORIGINAL data: Deflator[i]
// deflates the observation whose position is i (see [Series.Index]).
// Only positions inside the training data are read, so handing the full
// index to a backtest does not leak the future.
Deflator []float64
}
LogLinear is a log-linear regression: log(y/I) = a + b·t + seasonal dummies, by least squares over the last Window observations, where I is a price index (1 without a deflator).
The back-transformation is corrected for bias with Duan's smearing factor (the mean of exp(residual)). With a deflator, real forecasts are re-inflated by the index growth over the last cycle, held constant over the horizon. It needs positive values and three full cycles.
func (LogLinear) Description ¶
type Mean ¶
type Mean struct{}
Mean forecasts the mean of the history at every horizon.
func (Mean) Description ¶
type Metric ¶
type Metric int
Metric is the error measure averaged over the horizons to rank the candidates.
type Model ¶
type Model interface {
// Name is a short identifier, such as "holt_winters".
Name() string
// Description is a one-line description of the method.
Description() string
// Fit estimates the model. It returns an error when the series is too
// short or otherwise unsuitable.
Fit(y Series) (Fitted, error)
}
Model is a forecasting method, before seeing any data.
Fitting is separate from forecasting so that a fitted model can be inspected and asked for any horizon without being estimated again. Models are plain configuration and safe to share between goroutines.
type Naive ¶
type Naive struct{}
Naive forecasts the last observation at every horizon (a random walk).
func (Naive) Description ¶
type Point ¶
type Point struct {
// Horizon is the number of periods ahead (for a cumulative forecast, how
// many were added up).
Horizon int
Mean float64
Intervals []Interval
}
Point is a forecast with its intervals.
type Report ¶
type Report struct {
// Candidates are the models in the order given, then the average of the
// best ones.
Candidates []CandidateReport
// Chosen is the index of the chosen candidate.
Chosen int
// Origins is the number of origins actually used.
Origins int
// FirstOrigin is the position in the series of the first period
// forecast in the backtest.
FirstOrigin int
Horizon int
Metric Metric
}
Report is the result of a backtest.
type SeasonalNaive ¶
type SeasonalNaive struct {
// Growth scales the last cycle by the growth between the last two.
Growth bool
}
SeasonalNaive forecasts the same season of the last cycle, optionally scaled by recent growth.
With Growth, ŷ(T+k) = y(T+k−m) × g, where g is the sum of the last cycle over the sum of the one before; forecasts beyond one cycle compound g.
func (SeasonalNaive) Description ¶
func (s SeasonalNaive) Description() string
func (SeasonalNaive) Name ¶
func (s SeasonalNaive) Name() string
type Series ¶
type Series struct {
// contains filtered or unexported fields
}
Series is a view over regularly spaced observations, oldest first.
Besides the values, a series knows its seasonal period (12 for monthly data with a yearly cycle, 4 for quarterly, 1 for none) and where it sits in the original data: slicing keeps the absolute position, so the season of each observation and the alignment with external data (a price index, a regressor) survive any Head, Tail or Slice.
The values are not copied: a Series shares them with the slice it was made from.
func Monthly ¶
Monthly returns a series of monthly data with a yearly cycle; firstMonth is the calendar month of the first observation, 0 for January.
func NewSeries ¶
NewSeries returns a series with the given seasonal period; a period under 1 is read as 1 (no seasonality). The first observation is taken to be at season 0; see Series.WithPhase.
func NonSeasonal ¶
NonSeasonal returns a series without seasonality.
func Quarterly ¶
Quarterly returns a series of quarterly data with a yearly cycle; firstQuarter is 0 for the first quarter.
func (Series) Index ¶
Index returns the position in the original data of observation i, which may be past the end: Len() is the first period to be forecast.
func (Series) IsPositive ¶
IsPositive reports whether every value is finite and greater than zero, which is what multiplicative and logarithmic models need.
func (Series) Season ¶
Season returns the season (0 to Period()-1) of observation i, which, like in Index, may be past the end.
func (Series) Slice ¶
Slice returns the observations from position from up to, but not including, position to, clamped to the series.
type Theta ¶
type Theta struct{}
Theta is the Theta method (Assimakopoulos & Nikolopoulos, 2000), in the form shown by Hyndman & Billah (2003) to be simple exponential smoothing with drift: the smoothed level plus half the slope of the linear trend.
Seasonal series are first tested for seasonality (autocorrelation at the seasonal lag, 90% level); when the test passes and the values are positive, the series is seasonally adjusted by classical multiplicative decomposition and the forecasts are re-seasonalised. The smoothing parameter and the initial level minimise the sum of squared one-step errors.
Example ¶
One model on its own.
package main
import (
"fmt"
foresight "github.com/milkway/foresight-go"
)
func main() {
y := []float64{12, 14, 13, 15, 17, 16, 18, 20}
fit, err := foresight.Theta{}.Fit(foresight.NonSeasonal(y))
if err != nil {
fmt.Println(err)
return
}
for _, v := range fit.Forecast(3) {
fmt.Printf("%.2f\n", v)
}
}
Output: 20.52 21.04 21.55