Skip to content

Commit

Permalink
Merge pull request #2228 from deniszh/backport/1.1.x/pr-2225_pr-2225_…
Browse files Browse the repository at this point in the history
…pr-2226

[1.1.x] Add seasonality parameter for holt-winters functions | fix indent | functions: fix holtWinterAberration with multiple targets
  • Loading branch information
deniszh authored Feb 13, 2018
2 parents 7842d97 + 8807f1f commit b7e51b4
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 17 deletions.
33 changes: 20 additions & 13 deletions webapp/graphite/render/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3466,11 +3466,12 @@ def holtWintersDeviation(gamma,actual,prediction,last_seasonal_dev):
prediction = 0
return gamma * math.fabs(actual - prediction) + (1 - gamma) * last_seasonal_dev

def holtWintersAnalysis(series):
def holtWintersAnalysis(series, seasonality='1d'):
alpha = gamma = 0.1
beta = 0.0035
# season is currently one day
season_length = (24*60*60) // series.step
seasonality_time = parseTimeOffset(seasonality)
season_length = (seasonality_time.seconds + (seasonality_time.days * 86400)) // series.step
intercept = 0
slope = 0
intercepts = list()
Expand Down Expand Up @@ -3559,7 +3560,7 @@ def getLastDeviation(i):
}
return results

def holtWintersForecast(requestContext, seriesList, bootstrapInterval='7d'):
def holtWintersForecast(requestContext, seriesList, bootstrapInterval='7d', seasonality='1d'):
"""
Performs a Holt-Winters forecast using the series as input data. Data from
`bootstrapInterval` (one week by default) previous to the series is used to bootstrap the initial forecast.
Expand All @@ -3573,7 +3574,7 @@ def holtWintersForecast(requestContext, seriesList, bootstrapInterval='7d'):
previewList = evaluateTarget(newContext, requestContext['args'][0])
results = []
for series in previewList:
analysis = holtWintersAnalysis(series)
analysis = holtWintersAnalysis(series, seasonality)
predictions = analysis['predictions']
windowPoints = previewSeconds // predictions.step
series.tags['holtWintersForecast'] = 1
Expand All @@ -3588,9 +3589,10 @@ def holtWintersForecast(requestContext, seriesList, bootstrapInterval='7d'):
holtWintersForecast.params = [
Param('seriesList', ParamTypes.seriesList, required=True),
Param('bootstrapInterval', ParamTypes.interval, default='7d', suggestions=['7d', '30d']),
Param('seasonality', ParamTypes.interval, default='1d', suggestions=['1d', '7d']),
]

def holtWintersConfidenceBands(requestContext, seriesList, delta=3, bootstrapInterval='7d'):
def holtWintersConfidenceBands(requestContext, seriesList, delta=3, bootstrapInterval='7d', seasonality='1d'):
"""
Performs a Holt-Winters forecast using the series as input data and plots
upper and lower bands with the predicted forecast deviations.
Expand All @@ -3604,8 +3606,7 @@ def holtWintersConfidenceBands(requestContext, seriesList, delta=3, bootstrapInt
previewList = evaluateTarget(newContext, requestContext['args'][0])
results = []
for series in previewList:
analysis = holtWintersAnalysis(series)

analysis = holtWintersAnalysis(series, seasonality)
data = analysis['predictions']
windowPoints = previewSeconds // data.step
forecast = TimeSeries(data.name, data.start + previewSeconds, data.end, data.step, data[windowPoints:], xFilesFactor=series.xFilesFactor)
Expand Down Expand Up @@ -3655,18 +3656,21 @@ def holtWintersConfidenceBands(requestContext, seriesList, delta=3, bootstrapInt
Param('seriesList', ParamTypes.seriesList, required=True),
Param('delta', ParamTypes.integer, default=3),
Param('bootstrapInterval', ParamTypes.interval, default='7d', suggestions=['7d', '30d']),
Param('seasonality', ParamTypes.interval, default='1d', suggestions=['1d', '7d']),
]

def holtWintersAberration(requestContext, seriesList, delta=3, bootstrapInterval='7d'):
def holtWintersAberration(requestContext, seriesList, delta=3, bootstrapInterval='7d', seasonality='1d'):
"""
Performs a Holt-Winters forecast using the series as input data and plots the
positive or negative deviation of the series data from the forecast.
"""
results = []
confidenceBands = holtWintersConfidenceBands(requestContext, seriesList, delta, bootstrapInterval, seasonality)
confidenceBands = {s.name: s for s in confidenceBands}

for series in seriesList:
confidenceBands = holtWintersConfidenceBands(requestContext, [series], delta, bootstrapInterval)
lowerBand = confidenceBands[0]
upperBand = confidenceBands[1]
lowerBand = confidenceBands['holtWintersConfidenceLower(%s)' % series.name]
upperBand = confidenceBands['holtWintersConfidenceUpper(%s)' % series.name]
aberration = list()
for i, actual in enumerate(series):
if series[i] is None:
Expand All @@ -3689,14 +3693,15 @@ def holtWintersAberration(requestContext, seriesList, delta=3, bootstrapInterval
Param('seriesList', ParamTypes.seriesList, required=True),
Param('delta', ParamTypes.integer, default=3),
Param('bootstrapInterval', ParamTypes.interval, default='7d', suggestions=['7d', '30d']),
Param('seasonality', ParamTypes.interval, default='1d', suggestions=['1d', '7d']),
]

def holtWintersConfidenceArea(requestContext, seriesList, delta=3, bootstrapInterval='7d'):
def holtWintersConfidenceArea(requestContext, seriesList, delta=3, bootstrapInterval='7d', seasonality='1d'):
"""
Performs a Holt-Winters forecast using the series as input data and plots the
area between the upper and lower bands of the predicted forecast deviations.
"""
bands = holtWintersConfidenceBands(requestContext, seriesList, delta, bootstrapInterval)
bands = holtWintersConfidenceBands(requestContext, seriesList, delta, bootstrapInterval, seasonality)
results = areaBetween(requestContext, bands)
for series in results:
if 'areaBetween' in series.tags:
Expand All @@ -3711,6 +3716,7 @@ def holtWintersConfidenceArea(requestContext, seriesList, delta=3, bootstrapInte
Param('seriesList', ParamTypes.seriesList, required=True),
Param('delta', ParamTypes.integer, default=3),
Param('bootstrapInterval', ParamTypes.interval, default='7d', suggestions=['7d', '30d']),
Param('seasonality', ParamTypes.interval, default='1d', suggestions=['1d', '7d']),
]

def linearRegressionAnalysis(series):
Expand Down Expand Up @@ -4912,6 +4918,7 @@ def timeFunction(requestContext, name, step=60):
Accepts optional second argument as 'step' parameter (default step is 60 sec)
"""
# TODO: align both startTime and endTime when creating the TimeSeries.
delta = timedelta(seconds=step)
when = requestContext["startTime"]
values = []
Expand Down
10 changes: 6 additions & 4 deletions webapp/tests/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5138,14 +5138,15 @@ def test_holtWintersAberration(self):
start_time=2678400 # 1970-02-01
week_seconds=7*86400

def hw_range(x,y,jump):
def hw_range(x,y,jump,t=0):
while x<y:
yield (x/jump)%10
yield t+(x/jump)%10
x+=jump

def gen_seriesList(start=0, points=10):
seriesList = [
TimeSeries('collectd.test-db0.load.value', start, start+(points*step), step, hw_range(0, points*step, step)),
TimeSeries('collectd.test-db0.load.value', start, start+(points*step), step, hw_range(0, points*step, step)),
TimeSeries('collectd.test-db1.load.value', start, start+(points*step), step, hw_range(0, points*step, step, t=10)),
]
for series in seriesList:
series.pathExpression = series.name
Expand All @@ -5157,7 +5158,8 @@ def mock_evaluateTarget(requestContext, targets):
return gen_seriesList(start_time-week_seconds, (week_seconds/step)+points)

expectedResults = [
TimeSeries('holtWintersAberration(collectd.test-db0.load.value)', start_time, start_time+(points*step), step, [-0.2841206166091448, -0.05810270987744115, 0, 0, 0, 0, 0, 0, 0, 0])
TimeSeries('holtWintersAberration(collectd.test-db0.load.value)', start_time, start_time+(points*step), step, [-0.2841206166091448, -0.05810270987744115, 0, 0, 0, 0, 0, 0, 0, 0]),
TimeSeries('holtWintersAberration(collectd.test-db1.load.value)', start_time, start_time+(points*step), step, [-0.284120616609151, -0.05810270987744737, 0, 0, 0, 0, 0, 0, 0, 0]),
]

with patch('graphite.render.functions.evaluateTarget', mock_evaluateTarget):
Expand Down

0 comments on commit b7e51b4

Please sign in to comment.