Change to time-based estimator functions with exponential parameter smoothing.
This commit is contained in:
parent
a20355bc17
commit
b39deb1986
|
@ -216,7 +216,7 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
var weights, reps []float64
|
var weights, reps []float64
|
||||||
var date []time.Time
|
var dates []time.Time
|
||||||
isbw := review.IsBodyweight(ps[0].Exercise, ps[0].Type)
|
isbw := review.IsBodyweight(ps[0].Exercise, ps[0].Type)
|
||||||
|
|
||||||
for _, p := range (ps) {
|
for _, p := range (ps) {
|
||||||
|
@ -227,16 +227,14 @@ func main() {
|
||||||
if p.RIR[i] == 0 {
|
if p.RIR[i] == 0 {
|
||||||
weights = append(weights, w)
|
weights = append(weights, w)
|
||||||
reps = append(reps, float64(p.Reps[i]))
|
reps = append(reps, float64(p.Reps[i]))
|
||||||
date = append(date, p.Times[i].UTC())
|
dates = append(dates, p.Times[i].UTC())
|
||||||
fmt.Printf("Set: weight %0.0f, reps %0.0f\n", w, float64(p.Reps[i]))
|
fmt.Printf("Set: weight %0.0f, reps %0.0f\n", w, float64(p.Reps[i]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//a, b := review.FitPowerLaw(weights, reps, date, 14.0)
|
//a, b := review.FitPowerLaw(weights, reps, date, 14.0)
|
||||||
est, err := review.Fit(weights, reps, date,
|
est := review.NewEstimator(review.WithHalfLife(14.0))
|
||||||
review.WithAllModels(),
|
err = est.Fit(weights, reps, dates)
|
||||||
review.WithHalfLife(14.0),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error estimating performance: %v\n", err)
|
fmt.Printf("Error estimating performance: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -246,7 +244,7 @@ func main() {
|
||||||
if isbw {
|
if isbw {
|
||||||
adj = ps[len(ps)-1].Bodyweight
|
adj = ps[len(ps)-1].Bodyweight
|
||||||
}
|
}
|
||||||
fmt.Printf("%d: %0.0f\n", i, est.EstimateMaxWeight(float64(i)) - adj)
|
fmt.Printf("%d: %0.0f\n", i, est.EstimateMaxWeight(time.Now(), float64(i)) - adj)
|
||||||
}
|
}
|
||||||
case "predict":
|
case "predict":
|
||||||
if flag.NArg() != 3 {
|
if flag.NArg() != 3 {
|
||||||
|
|
|
@ -2,199 +2,170 @@ package review
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"math"
|
"math"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gonum.org/v1/gonum/optimize"
|
"gonum.org/v1/gonum/optimize"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Estimator encapsulates a fitted model and exposes estimation methods.
|
// Estimator is the main interface for incremental, time-aware strength estimation.
|
||||||
type Estimator interface {
|
type Estimator interface {
|
||||||
Estimate1RM() float64
|
Fit(weight, reps []float64, dates []time.Time) error
|
||||||
EstimateReps(targetWeight float64) float64
|
Estimate1RM(t time.Time) float64
|
||||||
EstimateMaxWeight(nReps float64) float64
|
EstimateReps(t time.Time, targetWeight float64) float64
|
||||||
ModelType() string
|
EstimateMaxWeight(t time.Time, nReps float64) float64
|
||||||
Params() []float64
|
Params(t time.Time) []float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Supported model types
|
// --- Functional Options ---
|
||||||
const (
|
|
||||||
ModelPowerLaw = "powerlaw"
|
|
||||||
ModelLinear = "linear"
|
|
||||||
ModelExponential = "exponential"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FitOption is a functional option for configuring the Fit process.
|
type estimatorConfig struct {
|
||||||
type FitOption func(*fitConfig)
|
modelType string
|
||||||
|
halfLife float64
|
||||||
// fitConfig holds configuration for fitting.
|
smoothAlpha float64 // Exponential smoothing factor (0 < alpha <= 1)
|
||||||
type fitConfig struct {
|
|
||||||
modelTypes []string
|
|
||||||
halfLifeDays float64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithModel specifies which model(s) to fit. If multiple, Fit selects the best.
|
type EstimatorOption func(*estimatorConfig)
|
||||||
func WithModel(models ...string) FitOption {
|
|
||||||
return func(cfg *fitConfig) {
|
// WithModel sets the model type (currently only "powerlaw" is implemented).
|
||||||
cfg.modelTypes = models
|
func WithModel(model string) EstimatorOption {
|
||||||
|
return func(cfg *estimatorConfig) {
|
||||||
|
cfg.modelType = model
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAllModels configures Fit to try all built-in model types.
|
// WithHalfLife sets the half-life for time weighting in curve fitting.
|
||||||
func WithAllModels() FitOption {
|
func WithHalfLife(days float64) EstimatorOption {
|
||||||
return func(cfg *fitConfig) {
|
return func(cfg *estimatorConfig) {
|
||||||
cfg.modelTypes = []string{ModelPowerLaw, ModelLinear, ModelExponential}
|
cfg.halfLife = days
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithHalfLife sets the half-life (in days) for time weighting.
|
// WithSmoothingAlpha sets the exponential smoothing factor for parameter smoothing.
|
||||||
func WithHalfLife(days float64) FitOption {
|
func WithSmoothingAlpha(alpha float64) EstimatorOption {
|
||||||
return func(cfg *fitConfig) {
|
return func(cfg *estimatorConfig) {
|
||||||
cfg.halfLifeDays = days
|
cfg.smoothAlpha = alpha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithModelSelection is an alias for WithModel, for clarity.
|
// --- Estimator Implementation ---
|
||||||
func WithModelSelection(models []string) FitOption {
|
|
||||||
return WithModel(models...)
|
type timePoint struct {
|
||||||
|
date time.Time
|
||||||
|
a float64
|
||||||
|
b float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default settings
|
type estimatorImpl struct {
|
||||||
const defaultHalfLife = 30.0
|
cfg estimatorConfig
|
||||||
var defaultModelTypes = []string{ModelPowerLaw}
|
data []timePoint // sorted by date
|
||||||
|
smoothedA []float64 // smoothed a for each timePoint
|
||||||
// Fit fits the specified model(s) to the data and returns an Estimator.
|
smoothedB []float64 // smoothed b for each timePoint
|
||||||
// If multiple models are specified, Fit selects the best based on residual sum of squares.
|
|
||||||
func Fit(weight, reps []float64, dates []time.Time, opts ...FitOption) (Estimator, error) {
|
|
||||||
if len(weight) != len(reps) || len(weight) != len(dates) {
|
|
||||||
return nil, errors.New("weight, reps, and dates must have the same length")
|
|
||||||
}
|
|
||||||
if len(weight) < 2 {
|
|
||||||
return nil, errors.New("at least two data points are required")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply options
|
// NewEstimator creates a new Estimator with the given options.
|
||||||
cfg := &fitConfig{
|
func NewEstimator(opts ...EstimatorOption) Estimator {
|
||||||
modelTypes: defaultModelTypes,
|
cfg := estimatorConfig{
|
||||||
halfLifeDays: defaultHalfLife,
|
modelType: "powerlaw",
|
||||||
|
halfLife: 30.0,
|
||||||
|
smoothAlpha: 0.3,
|
||||||
}
|
}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(cfg)
|
opt(&cfg)
|
||||||
|
}
|
||||||
|
return &estimatorImpl{
|
||||||
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
if len(cfg.modelTypes) == 0 {
|
|
||||||
cfg.modelTypes = defaultModelTypes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fit each model and select the best (lowest residual)
|
// Fit adds new data and updates the parameter time series and smoothing.
|
||||||
var best Estimator
|
func (e *estimatorImpl) Fit(weight, reps []float64, dates []time.Time) error {
|
||||||
var bestResidual float64 = math.Inf(1)
|
if len(weight) != len(reps) || len(weight) != len(dates) {
|
||||||
now := time.Now()
|
return errors.New("weight, reps, and dates must have the same length")
|
||||||
|
}
|
||||||
|
// Add new data points
|
||||||
|
for i := range weight {
|
||||||
|
e.data = append(e.data, timePoint{
|
||||||
|
date: dates[i],
|
||||||
|
a: math.NaN(), // to be filled in
|
||||||
|
b: math.NaN(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Sort all data points by date
|
||||||
|
sort.Slice(e.data, func(i, j int) bool {
|
||||||
|
return e.data[i].date.Before(e.data[j].date)
|
||||||
|
})
|
||||||
|
|
||||||
for _, model := range cfg.modelTypes {
|
// For each time point, fit the model to all data up to that point
|
||||||
var est Estimator
|
for i := range e.data {
|
||||||
var residual float64
|
var w, r []float64
|
||||||
var err error
|
var d []time.Time
|
||||||
|
for j := 0; j <= i; j++ {
|
||||||
switch model {
|
w = append(w, weight[j])
|
||||||
case ModelPowerLaw:
|
r = append(r, reps[j])
|
||||||
est, residual, err = fitPowerLaw(weight, reps, dates, now, cfg.halfLifeDays)
|
d = append(d, dates[j])
|
||||||
case ModelLinear:
|
|
||||||
est, residual, err = fitLinear(weight, reps, dates, now, cfg.halfLifeDays)
|
|
||||||
case ModelExponential:
|
|
||||||
est, residual, err = fitExponential(weight, reps, dates, now, cfg.halfLifeDays)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown model type: %s", model)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
a, b := fitPowerLaw(w, r, d, e.cfg.halfLife)
|
||||||
continue // Skip models that fail to fit
|
e.data[i].a = a
|
||||||
}
|
e.data[i].b = b
|
||||||
if residual < bestResidual {
|
|
||||||
best = est
|
|
||||||
bestResidual = residual
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if best == nil {
|
|
||||||
return nil, errors.New("no model could be fitted to the data")
|
|
||||||
}
|
|
||||||
return best, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Model Implementations ---
|
// Smooth the parameter time series
|
||||||
|
e.smoothedA = exponentialSmoothing(extractA(e.data), e.cfg.smoothAlpha)
|
||||||
// PowerLawEstimator: w = a * reps^b
|
e.smoothedB = exponentialSmoothing(extractB(e.data), e.cfg.smoothAlpha)
|
||||||
type PowerLawEstimator struct {
|
return nil
|
||||||
a, b float64
|
|
||||||
halfLife float64
|
|
||||||
modelType string
|
|
||||||
residualSum float64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *PowerLawEstimator) Estimate1RM() float64 {
|
// Estimate1RM returns the smoothed 1RM estimate at time t.
|
||||||
return e.a * math.Pow(1, e.b)
|
func (e *estimatorImpl) Estimate1RM(t time.Time) float64 {
|
||||||
|
a, b := e.smoothedParamsAt(t)
|
||||||
|
return a * math.Pow(1, b)
|
||||||
}
|
}
|
||||||
func (e *PowerLawEstimator) EstimateReps(targetWeight float64) float64 {
|
|
||||||
if e.a == 0 || e.b == 0 {
|
// EstimateReps returns the predicted number of reps at a given weight and time.
|
||||||
|
func (e *estimatorImpl) EstimateReps(t time.Time, targetWeight float64) float64 {
|
||||||
|
a, b := e.smoothedParamsAt(t)
|
||||||
|
if a == 0 || b == 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return math.Pow(targetWeight/e.a, 1/e.b)
|
return math.Pow(targetWeight/a, 1/b)
|
||||||
}
|
|
||||||
func (e *PowerLawEstimator) EstimateMaxWeight(nReps float64) float64 {
|
|
||||||
return e.a * math.Pow(nReps, e.b)
|
|
||||||
}
|
|
||||||
func (e *PowerLawEstimator) ModelType() string { return e.modelType }
|
|
||||||
func (e *PowerLawEstimator) Params() []float64 { return []float64{e.a, e.b} }
|
|
||||||
|
|
||||||
// LinearEstimator: w = a + b*reps
|
|
||||||
type LinearEstimator struct {
|
|
||||||
a, b float64
|
|
||||||
halfLife float64
|
|
||||||
modelType string
|
|
||||||
residualSum float64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *LinearEstimator) Estimate1RM() float64 {
|
// EstimateMaxWeight returns the predicted max weight for a given number of reps at time t.
|
||||||
return e.a + e.b*1
|
func (e *estimatorImpl) EstimateMaxWeight(t time.Time, nReps float64) float64 {
|
||||||
}
|
a, b := e.smoothedParamsAt(t)
|
||||||
func (e *LinearEstimator) EstimateReps(targetWeight float64) float64 {
|
return a * math.Pow(nReps, b)
|
||||||
if e.b == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return (targetWeight - e.a) / e.b
|
|
||||||
}
|
|
||||||
func (e *LinearEstimator) EstimateMaxWeight(nReps float64) float64 {
|
|
||||||
return e.a + e.b*nReps
|
|
||||||
}
|
|
||||||
func (e *LinearEstimator) ModelType() string { return e.modelType }
|
|
||||||
func (e *LinearEstimator) Params() []float64 { return []float64{e.a, e.b} }
|
|
||||||
|
|
||||||
// ExponentialEstimator: w = a * exp(b * reps)
|
|
||||||
type ExponentialEstimator struct {
|
|
||||||
a, b float64
|
|
||||||
halfLife float64
|
|
||||||
modelType string
|
|
||||||
residualSum float64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ExponentialEstimator) Estimate1RM() float64 {
|
// Params returns the smoothed model parameters at time t.
|
||||||
return e.a * math.Exp(e.b*1)
|
func (e *estimatorImpl) Params(t time.Time) []float64 {
|
||||||
|
a, b := e.smoothedParamsAt(t)
|
||||||
|
return []float64{a, b}
|
||||||
}
|
}
|
||||||
func (e *ExponentialEstimator) EstimateReps(targetWeight float64) float64 {
|
|
||||||
if e.a == 0 || e.b == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return math.Log(targetWeight/e.a) / e.b
|
|
||||||
}
|
|
||||||
func (e *ExponentialEstimator) EstimateMaxWeight(nReps float64) float64 {
|
|
||||||
return e.a * math.Exp(e.b*nReps)
|
|
||||||
}
|
|
||||||
func (e *ExponentialEstimator) ModelType() string { return e.modelType }
|
|
||||||
func (e *ExponentialEstimator) Params() []float64 { return []float64{e.a, e.b} }
|
|
||||||
|
|
||||||
// --- Fitting Functions ---
|
// --- Internal Helpers ---
|
||||||
|
|
||||||
// fitPowerLaw fits w = a * reps^b
|
// smoothedParamsAt returns the smoothed parameters for the closest time point <= t.
|
||||||
func fitPowerLaw(weight, reps []float64, dates []time.Time, now time.Time, halfLifeDays float64) (Estimator, float64, error) {
|
func (e *estimatorImpl) smoothedParamsAt(t time.Time) (float64, float64) {
|
||||||
|
if len(e.data) == 0 {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
idx := sort.Search(len(e.data), func(i int) bool {
|
||||||
|
return !e.data[i].date.Before(t)
|
||||||
|
})
|
||||||
|
if idx == 0 {
|
||||||
|
return e.smoothedA[0], e.smoothedB[0]
|
||||||
|
}
|
||||||
|
if idx >= len(e.data) {
|
||||||
|
return e.smoothedA[len(e.data)-1], e.smoothedB[len(e.data)-1]
|
||||||
|
}
|
||||||
|
return e.smoothedA[idx-1], e.smoothedB[idx-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// fitPowerLaw fits a power law model to the data.
|
||||||
|
func fitPowerLaw(weight, reps []float64, dates []time.Time, halfLifeDays float64) (a, b float64) {
|
||||||
|
now := dates[len(dates)-1]
|
||||||
params := []float64{max(weight), -0.1}
|
params := []float64{max(weight), -0.1}
|
||||||
problem := optimize.Problem{
|
problem := optimize.Problem{
|
||||||
Func: func(x []float64) float64 {
|
Func: func(x []float64) float64 {
|
||||||
|
@ -203,16 +174,9 @@ func fitPowerLaw(weight, reps []float64, dates []time.Time, now time.Time, halfL
|
||||||
}
|
}
|
||||||
result, err := optimize.Minimize(problem, params, nil, nil)
|
result, err := optimize.Minimize(problem, params, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return 0, 0
|
||||||
}
|
}
|
||||||
residual := weightedResidualsPowerLaw(result.X, weight, reps, dates, now, halfLifeDays)
|
return result.X[0], result.X[1]
|
||||||
return &PowerLawEstimator{
|
|
||||||
a: result.X[0],
|
|
||||||
b: result.X[1],
|
|
||||||
halfLife: halfLifeDays,
|
|
||||||
modelType: ModelPowerLaw,
|
|
||||||
residualSum: residual,
|
|
||||||
}, residual, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func weightedResidualsPowerLaw(params, weight, reps []float64, dates []time.Time, now time.Time, halfLifeDays float64) float64 {
|
func weightedResidualsPowerLaw(params, weight, reps []float64, dates []time.Time, now time.Time, halfLifeDays float64) float64 {
|
||||||
|
@ -228,79 +192,6 @@ func weightedResidualsPowerLaw(params, weight, reps []float64, dates []time.Time
|
||||||
return sum
|
return sum
|
||||||
}
|
}
|
||||||
|
|
||||||
// fitLinear fits w = a + b*reps
|
|
||||||
func fitLinear(weight, reps []float64, dates []time.Time, now time.Time, halfLifeDays float64) (Estimator, float64, error) {
|
|
||||||
params := []float64{weight[0], 0.0}
|
|
||||||
problem := optimize.Problem{
|
|
||||||
Func: func(x []float64) float64 {
|
|
||||||
return weightedResidualsLinear(x, weight, reps, dates, now, halfLifeDays)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
result, err := optimize.Minimize(problem, params, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
residual := weightedResidualsLinear(result.X, weight, reps, dates, now, halfLifeDays)
|
|
||||||
return &LinearEstimator{
|
|
||||||
a: result.X[0],
|
|
||||||
b: result.X[1],
|
|
||||||
halfLife: halfLifeDays,
|
|
||||||
modelType: ModelLinear,
|
|
||||||
residualSum: residual,
|
|
||||||
}, residual, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func weightedResidualsLinear(params, weight, reps []float64, dates []time.Time, now time.Time, halfLifeDays float64) float64 {
|
|
||||||
a, b := params[0], params[1]
|
|
||||||
var sum float64
|
|
||||||
for i := range weight {
|
|
||||||
daysAgo := now.Sub(dates[i]).Hours() / 24
|
|
||||||
weightDecay := math.Exp(-math.Ln2 * daysAgo / halfLifeDays)
|
|
||||||
predicted := a + b*reps[i]
|
|
||||||
residual := weight[i] - predicted
|
|
||||||
sum += weightDecay * residual * residual
|
|
||||||
}
|
|
||||||
return sum
|
|
||||||
}
|
|
||||||
|
|
||||||
// fitExponential fits w = a * exp(b*reps)
|
|
||||||
func fitExponential(weight, reps []float64, dates []time.Time, now time.Time, halfLifeDays float64) (Estimator, float64, error) {
|
|
||||||
params := []float64{max(weight), -0.01}
|
|
||||||
problem := optimize.Problem{
|
|
||||||
Func: func(x []float64) float64 {
|
|
||||||
return weightedResidualsExponential(x, weight, reps, dates, now, halfLifeDays)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
result, err := optimize.Minimize(problem, params, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
residual := weightedResidualsExponential(result.X, weight, reps, dates, now, halfLifeDays)
|
|
||||||
return &ExponentialEstimator{
|
|
||||||
a: result.X[0],
|
|
||||||
b: result.X[1],
|
|
||||||
halfLife: halfLifeDays,
|
|
||||||
modelType: ModelExponential,
|
|
||||||
residualSum: residual,
|
|
||||||
}, residual, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func weightedResidualsExponential(params, weight, reps []float64, dates []time.Time, now time.Time, halfLifeDays float64) float64 {
|
|
||||||
a, b := params[0], params[1]
|
|
||||||
var sum float64
|
|
||||||
for i := range weight {
|
|
||||||
daysAgo := now.Sub(dates[i]).Hours() / 24
|
|
||||||
weightDecay := math.Exp(-math.Ln2 * daysAgo / halfLifeDays)
|
|
||||||
predicted := a * math.Exp(b*reps[i])
|
|
||||||
residual := weight[i] - predicted
|
|
||||||
sum += weightDecay * residual * residual
|
|
||||||
}
|
|
||||||
return sum
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Utility Functions ---
|
|
||||||
|
|
||||||
// max returns the maximum value in a slice
|
|
||||||
func max(slice []float64) float64 {
|
func max(slice []float64) float64 {
|
||||||
m := slice[0]
|
m := slice[0]
|
||||||
for _, v := range slice {
|
for _, v := range slice {
|
||||||
|
@ -311,3 +202,32 @@ func max(slice []float64) float64 {
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractA(data []timePoint) []float64 {
|
||||||
|
out := make([]float64, len(data))
|
||||||
|
for i, d := range data {
|
||||||
|
out[i] = d.a
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractB(data []timePoint) []float64 {
|
||||||
|
out := make([]float64, len(data))
|
||||||
|
for i, d := range data {
|
||||||
|
out[i] = d.b
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// exponentialSmoothing applies exponential smoothing to a time series.
|
||||||
|
func exponentialSmoothing(series []float64, alpha float64) []float64 {
|
||||||
|
if len(series) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
smoothed := make([]float64, len(series))
|
||||||
|
smoothed[0] = series[0]
|
||||||
|
for i := 1; i < len(series); i++ {
|
||||||
|
smoothed[i] = alpha*series[i] + (1-alpha)*smoothed[i-1]
|
||||||
|
}
|
||||||
|
return smoothed
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user