foresight

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: MIT Imports: 7 Imported by: 0

README

foresight-go

Go Reference CI

Time series forecasting in Go that picks its model by what would have worked.

foresight fits several models to a series, replays the past to see how each would have done, chooses by out-of-sample error and reports intervals taken from the errors actually observed. It uses the standard library only and every result is deterministic.

This is the Go edition of the Rust crate foresight. It is written in Go, not wrapped: no cgo, nothing to link.

Website: https://milkway.github.io/foresight-go/ · Reference: https://pkg.go.dev/github.com/milkway/foresight-go · Português

Install

go get github.com/milkway/foresight-go

Go 1.22 or later.

Use

import foresight "github.com/milkway/foresight-go"

// monthly data whose first observation is in March
y := foresight.Monthly(values, 2)

// replay the last 36 months, 12 months ahead, with every built-in model
report, err := foresight.DefaultBacktest().Run(y, foresight.Defaults())
if err != nil {
	log.Fatal(err)
}

best := report.Best()
fmt.Printf("%s: MAPE %.1f%%\n", best.Name, best.Score)
for _, p := range best.Forecast {
	i, _ := p.Interval(0.80)
	fmt.Printf("%2d %.0f [%.0f, %.0f]\n", p.Horizon, p.Mean, i.Lower, i.Upper)
}

// the total of the next six months, with its own interval
halfYear, _ := best.Cumulative(6)

One model on its own:

fit, err := foresight.Theta{}.Fit(foresight.NonSeasonal(values))
next := fit.Forecast(3)

A model of your own joins the backtest by implementing Model.

What is in it

Piece What it does
Series values + seasonal period; slices keep season and position
Model / Fitted fit once, forecast any horizon, inspect parameters
Models Mean, Naive, Drift, SeasonalNaive, Theta, HoltWinters, LogLinear (optionally deflated by a price index)
Backtest rolling origin (expanding or fixed window) on all cores; MAPE, MAE, RMSE, MASE and bias by horizon; average of the best models; choice by out-of-sample error
Intervals empirical quantiles of the backtest errors, by horizon and for cumulative totals
Measures MAPE, Bias, MAE, RMSE, MASE, Quantile, ACF

The Rust crate has more: seasonal ARIMA with automatic orders, the exponential smoothing family, Prophet, TBATS, STL and MSTL, regression with ARIMA errors, intermittent demand, data cleaning and ensembles.

How it differs from the usual toolkits

Most forecasting libraries choose a model by an in-sample information criterion and derive intervals from distributional assumptions. Here the choice and the intervals both come from forecasts made without seeing the future they are judged against. The interval for a total (say, the rest of a fiscal year) is measured on totals, because adding up monthly limits overstates its uncertainty.

Checked

Against What Agreement
The Rust crate backtest of four models on two public series: errors and bias by horizon, quantiles, forecasts, intervals and the choice the same numbers (relative difference under 10⁻⁹)
R package forecast 9.0.2 seasonal naive, random walk with drift exact
R package forecast 9.0.2 Theta forecasts within 0.1%

The tests are in rust_test.go and r_test.go.

Data

  • testdata/piaui_revenue.csv: monthly ICMS and FPE revenue of the state of Piauí, Brazil, March 2017 to June 2026 (Siconfi/STN, RREO Anexo 03), with the chained IPCA price index (BCB/SGS 433). Public data.
  • testdata/air_passengers.csv: the classic monthly airline passengers series (Box & Jenkins, 1976).

Status

Early: the API may change before 1.0.

Development

scripts/dev-check.sh   # gofmt, go vet, go test

License

MIT. See LICENSE.

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

Examples

Constants

This section is empty.

Variables

View Source
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.

View Source
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.

View Source
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 ACF

func ACF(y []float64, maxLag int) []float64

ACF returns the autocorrelations of y at lags 1 to maxLag.

func Bias

func Bias(actual, forecast []float64) float64

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 Forecast

func Forecast(m Model, y Series, h int) ([]float64, error)

Forecast fits the model and forecasts the h periods after the last observation.

func MAE

func MAE(actual, forecast []float64) float64

MAE is the mean absolute error.

func MAPE

func MAPE(actual, forecast []float64) float64

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 MASE

func MASE(actual, forecast []float64, scale float64) float64

MASE is the mean absolute scaled error given the scale from MASEScale.

func MASEScale

func MASEScale(train []float64, period int) (float64, bool)

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.

func Quantile

func Quantile(v []float64, p float64) (float64, bool)

Quantile returns the quantile p of v with linear interpolation (type 7, the default in R). v does not need to be sorted. It reports false for an empty slice.

func RMSE

func RMSE(actual, forecast []float64) float64

RMSE is the root mean squared error.

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.

  1. 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.
  2. Selection: the candidate with the lowest error averaged over the horizons, the simple average of the best few models included.
  3. 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).
  4. 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

func (b Backtest) Run(y Series, candidates []Candidate) (*Report, error)

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

type Candidate struct {
	Name        string
	Description string
	Model       Model
}

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

func NewCandidate(m Model) Candidate

NewCandidate returns a candidate with the model's own name and description.

func (Candidate) Named

func (c Candidate) Named(name, description string) Candidate

Named returns the candidate under another 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

func (Drift) Description() string

func (Drift) Fit

func (Drift) Fit(y Series) (Fitted, error)

func (Drift) Name

func (Drift) Name() string

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) Fit

func (HoltWinters) Fit(y Series) (Fitted, error)

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

func (l LogLinear) Description() string

func (LogLinear) Fit

func (l LogLinear) Fit(y Series) (Fitted, error)

func (LogLinear) Name

func (l LogLinear) Name() string

type Mean

type Mean struct{}

Mean forecasts the mean of the history at every horizon.

func (Mean) Description

func (Mean) Description() string

func (Mean) Fit

func (Mean) Fit(y Series) (Fitted, error)

func (Mean) Name

func (Mean) Name() string

type Metric

type Metric int

Metric is the error measure averaged over the horizons to rank the candidates.

const (
	// RankByMAPE ranks by mean absolute percentage error (the default).
	RankByMAPE Metric = iota
	// RankByMAE ranks by mean absolute error.
	RankByMAE
	// RankByRMSE ranks by root mean squared error.
	RankByRMSE
	// RankByMASE ranks by mean absolute scaled error.
	RankByMASE
)

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

func (Naive) Description() string

func (Naive) Fit

func (Naive) Fit(y Series) (Fitted, error)

func (Naive) Name

func (Naive) Name() string

type Param

type Param struct {
	Name  string
	Value float64
}

Param is a named number estimated by a model, for display and audit trails.

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.

func (Point) Interval

func (p Point) Interval(level float64) (Interval, bool)

Interval returns the interval with the given coverage, if it was asked for.

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.

func (*Report) Best

func (r *Report) Best() *CandidateReport

Best returns the chosen candidate.

func (*Report) Candidate

func (r *Report) Candidate(name string) (*CandidateReport, bool)

Candidate looks a candidate up by name.

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) Fit

func (s SeasonalNaive) Fit(y Series) (Fitted, error)

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

func Monthly(values []float64, firstMonth int) Series

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

func NewSeries(values []float64, period int) Series

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

func NonSeasonal(values []float64) Series

NonSeasonal returns a series without seasonality.

func Quarterly

func Quarterly(values []float64, firstQuarter int) Series

Quarterly returns a series of quarterly data with a yearly cycle; firstQuarter is 0 for the first quarter.

func (Series) Head

func (s Series) Head(n int) Series

Head returns the first n observations (all of them if n is larger).

func (Series) Index

func (s Series) Index(i int) int

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) IsFinite

func (s Series) IsFinite() bool

IsFinite reports whether every value is a finite number.

func (Series) IsPositive

func (s Series) IsPositive() bool

IsPositive reports whether every value is finite and greater than zero, which is what multiplicative and logarithmic models need.

func (Series) Len

func (s Series) Len() int

Len returns the number of observations.

func (Series) Period

func (s Series) Period() int

Period returns the seasonal period.

func (Series) Season

func (s Series) Season(i int) int

Season returns the season (0 to Period()-1) of observation i, which, like in Index, may be past the end.

func (Series) Slice

func (s Series) Slice(from, to int) Series

Slice returns the observations from position from up to, but not including, position to, clamped to the series.

func (Series) Start

func (s Series) Start() int

Start returns the position, in the original data, of the first observation.

func (Series) Tail

func (s Series) Tail(n int) Series

Tail returns the last n observations (all of them if n is larger).

func (Series) Values

func (s Series) Values() []float64

Values returns the observations. The slice is shared, not copied.

func (Series) WithPhase

func (s Series) WithPhase(phase int) Series

WithPhase sets the season of the first observation of this view.

func (Series) WithValues

func (s Series) WithValues(values []float64) (Series, error)

WithValues returns the same series (period, season and position) over other values, such as a transformation of the original ones.

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

func (Theta) Description

func (Theta) Description() string

func (Theta) Fit

func (Theta) Fit(y Series) (Fitted, error)

func (Theta) Name

func (Theta) Name() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL