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 }