github.com/go-graphite/carbonapi@v0.17.0/expr/holtwinters/hw.go (about)

     1  package holtwinters
     2  
     3  // This holt-winters code copied from graphite's functions.py)
     4  // It's "mostly" the same as a standard HW forecast
     5  
     6  import (
     7  	"math"
     8  )
     9  
    10  const (
    11  	DefaultSeasonality       = 86400  // Seconds in 1 day
    12  	DefaultBootstrapInterval = 604800 // Seconds in 7 days
    13  	SecondsPerDay            = 86400
    14  )
    15  
    16  func holtWintersIntercept(alpha, actual, lastSeason, lastIntercept, lastSlope float64) float64 {
    17  	return alpha*(actual-lastSeason) + (1-alpha)*(lastIntercept+lastSlope)
    18  }
    19  
    20  func holtWintersSlope(beta, intercept, lastIntercept, lastSlope float64) float64 {
    21  	return beta*(intercept-lastIntercept) + (1-beta)*lastSlope
    22  }
    23  
    24  func holtWintersSeasonal(gamma, actual, intercept, lastSeason float64) float64 {
    25  	return gamma*(actual-intercept) + (1-gamma)*lastSeason
    26  }
    27  
    28  func holtWintersDeviation(gamma, actual, prediction, lastSeasonalDev float64) float64 {
    29  	if math.IsNaN(prediction) {
    30  		prediction = 0
    31  	}
    32  	return gamma*math.Abs(actual-prediction) + (1-gamma)*lastSeasonalDev
    33  }
    34  
    35  // HoltWintersAnalysis do Holt-Winters Analysis
    36  func HoltWintersAnalysis(series []float64, step int64, seasonality int64) ([]float64, []float64) {
    37  	const (
    38  		alpha = 0.1
    39  		beta  = 0.0035
    40  		gamma = 0.1
    41  	)
    42  
    43  	seasonLength := seasonality / step
    44  
    45  	// seasonLength needs to be at least 2, so we force it
    46  	// GraphiteWeb has the same problem, still needs fixing https://github.com/graphite-project/graphite-web/issues/2780
    47  	if seasonLength < 2 {
    48  		seasonLength = 2
    49  	}
    50  
    51  	var (
    52  		intercepts  []float64
    53  		slopes      []float64
    54  		seasonals   []float64
    55  		predictions []float64
    56  		deviations  []float64
    57  	)
    58  
    59  	getLastSeasonal := func(i int) float64 {
    60  		j := i - int(seasonLength)
    61  		if j >= 0 {
    62  			return seasonals[j]
    63  		}
    64  		return 0
    65  	}
    66  
    67  	getLastDeviation := func(i int) float64 {
    68  		j := i - int(seasonLength)
    69  		if j >= 0 {
    70  			return deviations[j]
    71  		}
    72  		return 0
    73  	}
    74  
    75  	var nextPred = math.NaN()
    76  
    77  	for i, actual := range series {
    78  		if math.IsNaN(actual) {
    79  			// missing input values break all the math
    80  			// do the best we can and move on
    81  			intercepts = append(intercepts, math.NaN())
    82  			slopes = append(slopes, 0)
    83  			seasonals = append(seasonals, 0)
    84  			predictions = append(predictions, nextPred)
    85  			deviations = append(deviations, 0)
    86  			nextPred = math.NaN()
    87  			continue
    88  		}
    89  
    90  		var (
    91  			lastSlope     float64
    92  			lastIntercept float64
    93  			prediction    float64
    94  		)
    95  		if i == 0 {
    96  			lastIntercept = actual
    97  			lastSlope = 0
    98  			// seed the first prediction as the first actual
    99  			prediction = actual
   100  		} else {
   101  			lastIntercept = intercepts[len(intercepts)-1]
   102  			lastSlope = slopes[len(slopes)-1]
   103  			if math.IsNaN(lastIntercept) {
   104  				lastIntercept = actual
   105  			}
   106  			prediction = nextPred
   107  		}
   108  
   109  		lastSeasonal := getLastSeasonal(i)
   110  		nextLastSeasonal := getLastSeasonal(i + 1)
   111  		lastSeasonalDev := getLastDeviation(i)
   112  
   113  		intercept := holtWintersIntercept(alpha, actual, lastSeasonal, lastIntercept, lastSlope)
   114  		slope := holtWintersSlope(beta, intercept, lastIntercept, lastSlope)
   115  		seasonal := holtWintersSeasonal(gamma, actual, intercept, lastSeasonal)
   116  		nextPred = intercept + slope + nextLastSeasonal
   117  		deviation := holtWintersDeviation(gamma, actual, prediction, lastSeasonalDev)
   118  
   119  		intercepts = append(intercepts, intercept)
   120  		slopes = append(slopes, slope)
   121  		seasonals = append(seasonals, seasonal)
   122  		predictions = append(predictions, prediction)
   123  		deviations = append(deviations, deviation)
   124  	}
   125  
   126  	return predictions, deviations
   127  }
   128  
   129  // HoltWintersConfidenceBands do Holt-Winters Confidence Bands
   130  func HoltWintersConfidenceBands(series []float64, step int64, delta float64, bootstrapInterval int64, seasonality int64) ([]float64, []float64) {
   131  	var lowerBand, upperBand []float64
   132  
   133  	predictions, deviations := HoltWintersAnalysis(series, step, seasonality)
   134  
   135  	windowPoints := int(bootstrapInterval / step)
   136  
   137  	var (
   138  		predictionsOfInterest []float64
   139  		deviationsOfInterest  []float64
   140  	)
   141  	if len(predictions) < windowPoints || len(deviations) < windowPoints {
   142  		predictionsOfInterest = predictions
   143  		deviationsOfInterest = deviations
   144  	} else {
   145  		predictionsOfInterest = predictions[windowPoints:]
   146  		deviationsOfInterest = deviations[windowPoints:]
   147  	}
   148  
   149  	for i := range predictionsOfInterest {
   150  		if math.IsNaN(predictionsOfInterest[i]) || math.IsNaN(deviationsOfInterest[i]) {
   151  			lowerBand = append(lowerBand, math.NaN())
   152  			upperBand = append(upperBand, math.NaN())
   153  		} else {
   154  			scaledDeviation := delta * deviationsOfInterest[i]
   155  			lowerBand = append(lowerBand, predictionsOfInterest[i]-scaledDeviation)
   156  			upperBand = append(upperBand, predictionsOfInterest[i]+scaledDeviation)
   157  		}
   158  	}
   159  
   160  	return lowerBand, upperBand
   161  }