mirror of
https://github.com/microsoft/autogen.git
synced 2025-09-26 08:34:10 +00:00
Add Holt-Winters exponential smoothing (#962)
* tentatively implement holt-winters-no covariates * fix forecast method, clean class * checking external regressors too * update test forecast * remove duplicated test file, re-add sarimax, search space cleanup * Update flaml/automl/model.py removed links. Most important one probably was: https://robjhyndman.com/hyndsight/ets-regressors/ Co-authored-by: Chi Wang <wang.chi@microsoft.com> * prevent short series * add docs --------- Co-authored-by: Andrea W <a.ruggerini@ammagamma.com> Co-authored-by: Chi Wang <wang.chi@microsoft.com>
This commit is contained in:
parent
4c20c85dfd
commit
7f9402b8fd
@ -38,6 +38,7 @@ from flaml.automl.model import (
|
|||||||
Prophet,
|
Prophet,
|
||||||
ARIMA,
|
ARIMA,
|
||||||
SARIMAX,
|
SARIMAX,
|
||||||
|
HoltWinters,
|
||||||
TransformersEstimator,
|
TransformersEstimator,
|
||||||
TemporalFusionTransformerEstimator,
|
TemporalFusionTransformerEstimator,
|
||||||
TransformersEstimatorModelSelection,
|
TransformersEstimatorModelSelection,
|
||||||
@ -156,6 +157,8 @@ def get_estimator_class(task: str, estimator_name: str) -> EstimatorSubclass:
|
|||||||
estimator_class = ARIMA
|
estimator_class = ARIMA
|
||||||
elif estimator_name == "sarimax":
|
elif estimator_name == "sarimax":
|
||||||
estimator_class = SARIMAX
|
estimator_class = SARIMAX
|
||||||
|
elif estimator_name == "holt-winters":
|
||||||
|
estimator_class = HoltWinters
|
||||||
elif estimator_name == "transformer":
|
elif estimator_name == "transformer":
|
||||||
estimator_class = TransformersEstimator
|
estimator_class = TransformersEstimator
|
||||||
elif estimator_name == "tft":
|
elif estimator_name == "tft":
|
||||||
|
@ -2377,6 +2377,94 @@ class SARIMAX(ARIMA):
|
|||||||
return train_time
|
return train_time
|
||||||
|
|
||||||
|
|
||||||
|
class HoltWinters(ARIMA):
|
||||||
|
"""
|
||||||
|
The class for tuning Holt Winters model, aka 'Triple Exponential Smoothing'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def search_space(cls, **params):
|
||||||
|
space = {
|
||||||
|
"damped_trend": {"domain": tune.choice([True, False]), "init_value": False},
|
||||||
|
"trend": {"domain": tune.choice(["add", "mul", None]), "init_value": "add"},
|
||||||
|
"seasonal": {
|
||||||
|
"domain": tune.choice(["add", "mul", None]),
|
||||||
|
"init_value": "add",
|
||||||
|
},
|
||||||
|
"use_boxcox": {"domain": tune.choice([False, True]), "init_value": False},
|
||||||
|
"seasonal_periods": { # statsmodels casts this to None if "seasonal" is None
|
||||||
|
"domain": tune.choice(
|
||||||
|
[7, 12, 4, 52, 6]
|
||||||
|
), # weekly, yearly, quarterly, weekly w yearly data
|
||||||
|
"init_value": 7,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return space
|
||||||
|
|
||||||
|
def fit(self, X_train, y_train, budget=None, free_mem_ratio=0, **kwargs):
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
from statsmodels.tsa.holtwinters import (
|
||||||
|
ExponentialSmoothing as HWExponentialSmoothing,
|
||||||
|
)
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
train_df = self._join(X_train, y_train)
|
||||||
|
train_df = self._preprocess(train_df)
|
||||||
|
regressors = list(train_df)
|
||||||
|
regressors.remove(TS_VALUE_COL)
|
||||||
|
if regressors:
|
||||||
|
logger.warning("Regressors are ignored for Holt-Winters ETS models.")
|
||||||
|
|
||||||
|
# Override incompatible parameters
|
||||||
|
if (
|
||||||
|
X_train.shape[0] < 2 * self.params["seasonal_periods"]
|
||||||
|
): # this would prevent heuristic initialization to work properly
|
||||||
|
self.params["seasonal"] = None
|
||||||
|
if (
|
||||||
|
self.params["seasonal"] == "mul" and (train_df.y == 0).sum() > 0
|
||||||
|
): # cannot have multiplicative seasonality in this case
|
||||||
|
self.params["seasonal"] = "add"
|
||||||
|
if self.params["trend"] == "mul" and (train_df.y == 0).sum() > 0:
|
||||||
|
self.params["trend"] = "add"
|
||||||
|
|
||||||
|
if not self.params["seasonal"] or not self.params["trend"] in [
|
||||||
|
"mul",
|
||||||
|
"add",
|
||||||
|
]:
|
||||||
|
self.params["damped_trend"] = False
|
||||||
|
|
||||||
|
model = HWExponentialSmoothing(
|
||||||
|
train_df[[TS_VALUE_COL]],
|
||||||
|
damped_trend=self.params["damped_trend"],
|
||||||
|
seasonal=self.params["seasonal"],
|
||||||
|
trend=self.params["trend"],
|
||||||
|
)
|
||||||
|
with suppress_stdout_stderr():
|
||||||
|
model = model.fit()
|
||||||
|
train_time = time.time() - current_time
|
||||||
|
self._model = model
|
||||||
|
return train_time
|
||||||
|
|
||||||
|
def predict(self, X, **kwargs):
|
||||||
|
if self._model is not None:
|
||||||
|
if isinstance(X, int):
|
||||||
|
forecast = self._model.forecast(steps=X)
|
||||||
|
elif isinstance(X, DataFrame):
|
||||||
|
start = X[TS_TIMESTAMP_COL].iloc[0]
|
||||||
|
end = X[TS_TIMESTAMP_COL].iloc[-1]
|
||||||
|
forecast = self._model.predict(start=start, end=end, **kwargs)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"X needs to be either a pandas Dataframe with dates as the first column"
|
||||||
|
" or an int number of periods for predict()."
|
||||||
|
)
|
||||||
|
return forecast
|
||||||
|
else:
|
||||||
|
return np.ones(X if isinstance(X, int) else X.shape[0])
|
||||||
|
|
||||||
|
|
||||||
class TS_SKLearn(SKLearnEstimator):
|
class TS_SKLearn(SKLearnEstimator):
|
||||||
"""The class for tuning SKLearn Regressors for time-series forecasting, using hcrystalball"""
|
"""The class for tuning SKLearn Regressors for time-series forecasting, using hcrystalball"""
|
||||||
|
|
||||||
|
@ -1055,9 +1055,14 @@ class GenericTask(Task):
|
|||||||
try:
|
try:
|
||||||
import prophet
|
import prophet
|
||||||
|
|
||||||
estimator_list += ["prophet", "arima", "sarimax"]
|
estimator_list += [
|
||||||
|
"prophet",
|
||||||
|
"arima",
|
||||||
|
"sarimax",
|
||||||
|
"holt-winters",
|
||||||
|
]
|
||||||
except ImportError:
|
except ImportError:
|
||||||
estimator_list += ["arima", "sarimax"]
|
estimator_list += ["arima", "sarimax", "holt-winters"]
|
||||||
elif not self.is_regression():
|
elif not self.is_regression():
|
||||||
estimator_list += ["lrl1"]
|
estimator_list += ["lrl1"]
|
||||||
|
|
||||||
|
@ -2,7 +2,9 @@ import numpy as np
|
|||||||
from flaml import AutoML
|
from flaml import AutoML
|
||||||
|
|
||||||
|
|
||||||
def test_forecast_automl(budget=5):
|
def test_forecast_automl(
|
||||||
|
budget=5, estimators_when_no_prophet=["arima", "sarimax", "holt-winters"]
|
||||||
|
):
|
||||||
# using dataframe
|
# using dataframe
|
||||||
import statsmodels.api as sm
|
import statsmodels.api as sm
|
||||||
|
|
||||||
@ -39,7 +41,7 @@ def test_forecast_automl(budget=5):
|
|||||||
automl.fit(
|
automl.fit(
|
||||||
dataframe=df,
|
dataframe=df,
|
||||||
**settings,
|
**settings,
|
||||||
estimator_list=["arima", "sarimax"],
|
estimator_list=estimators_when_no_prophet,
|
||||||
period=time_horizon,
|
period=time_horizon,
|
||||||
)
|
)
|
||||||
""" retrieve best config and best learner"""
|
""" retrieve best config and best learner"""
|
||||||
@ -89,7 +91,7 @@ def test_forecast_automl(budget=5):
|
|||||||
X_train=X_train,
|
X_train=X_train,
|
||||||
y_train=y_train,
|
y_train=y_train,
|
||||||
**settings,
|
**settings,
|
||||||
estimator_list=["arima", "sarimax"],
|
estimator_list=estimators_when_no_prophet,
|
||||||
period=time_horizon,
|
period=time_horizon,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -161,7 +163,9 @@ def load_multi_dataset():
|
|||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
def test_multivariate_forecast_num(budget=5):
|
def test_multivariate_forecast_num(
|
||||||
|
budget=5, estimators_when_no_prophet=["arima", "sarimax", "holt-winters"]
|
||||||
|
):
|
||||||
df = load_multi_dataset()
|
df = load_multi_dataset()
|
||||||
# split data into train and test
|
# split data into train and test
|
||||||
time_horizon = 180
|
time_horizon = 180
|
||||||
@ -193,7 +197,7 @@ def test_multivariate_forecast_num(budget=5):
|
|||||||
automl.fit(
|
automl.fit(
|
||||||
dataframe=train_df,
|
dataframe=train_df,
|
||||||
**settings,
|
**settings,
|
||||||
estimator_list=["arima", "sarimax"],
|
estimator_list=estimators_when_no_prophet,
|
||||||
period=time_horizon,
|
period=time_horizon,
|
||||||
)
|
)
|
||||||
""" retrieve best config and best learner"""
|
""" retrieve best config and best learner"""
|
||||||
@ -293,7 +297,9 @@ def load_multi_dataset_cat(time_horizon):
|
|||||||
return train_df, test_df
|
return train_df, test_df
|
||||||
|
|
||||||
|
|
||||||
def test_multivariate_forecast_cat(budget=5):
|
def test_multivariate_forecast_cat(
|
||||||
|
budget=5, estimators_when_no_prophet=["arima", "sarimax", "holt-winters"]
|
||||||
|
):
|
||||||
time_horizon = 180
|
time_horizon = 180
|
||||||
train_df, test_df = load_multi_dataset_cat(time_horizon)
|
train_df, test_df = load_multi_dataset_cat(time_horizon)
|
||||||
X_test = test_df[
|
X_test = test_df[
|
||||||
@ -320,7 +326,7 @@ def test_multivariate_forecast_cat(budget=5):
|
|||||||
automl.fit(
|
automl.fit(
|
||||||
dataframe=train_df,
|
dataframe=train_df,
|
||||||
**settings,
|
**settings,
|
||||||
estimator_list=["arima", "sarimax"],
|
estimator_list=estimators_when_no_prophet,
|
||||||
period=time_horizon,
|
period=time_horizon,
|
||||||
)
|
)
|
||||||
""" retrieve best config and best learner"""
|
""" retrieve best config and best learner"""
|
||||||
|
@ -125,6 +125,7 @@ The estimator list can contain one or more estimator names, each corresponding t
|
|||||||
- 'prophet': Prophet for task "ts_forecast". Hyperparameters: changepoint_prior_scale, seasonality_prior_scale, holidays_prior_scale, seasonality_mode.
|
- 'prophet': Prophet for task "ts_forecast". Hyperparameters: changepoint_prior_scale, seasonality_prior_scale, holidays_prior_scale, seasonality_mode.
|
||||||
- 'arima': ARIMA for task "ts_forecast". Hyperparameters: p, d, q.
|
- 'arima': ARIMA for task "ts_forecast". Hyperparameters: p, d, q.
|
||||||
- 'sarimax': SARIMAX for task "ts_forecast". Hyperparameters: p, d, q, P, D, Q, s.
|
- 'sarimax': SARIMAX for task "ts_forecast". Hyperparameters: p, d, q, P, D, Q, s.
|
||||||
|
- 'holt-winters': Holt-Winters (triple exponential smoothing) model for task "ts_forecast". Hyperparameters: seasonal_perdiods, seasonal, use_boxcox, trend, damped_trend.
|
||||||
- 'transformer': Huggingface transformer models for task "seq-classification", "seq-regression", "multichoice-classification", "token-classification" and "summarization". Hyperparameters: learning_rate, num_train_epochs, per_device_train_batch_size, warmup_ratio, weight_decay, adam_epsilon, seed.
|
- 'transformer': Huggingface transformer models for task "seq-classification", "seq-regression", "multichoice-classification", "token-classification" and "summarization". Hyperparameters: learning_rate, num_train_epochs, per_device_train_batch_size, warmup_ratio, weight_decay, adam_epsilon, seed.
|
||||||
- 'temporal_fusion_transformer': TemporalFusionTransformerEstimator for task "ts_forecast_panel". Hyperparameters: gradient_clip_val, hidden_size, hidden_continuous_size, attention_head_size, dropout, learning_rate. There is a [known issue](https://github.com/jdb78/pytorch-forecasting/issues/1145) with pytorch-forecast logging.
|
- 'temporal_fusion_transformer': TemporalFusionTransformerEstimator for task "ts_forecast_panel". Hyperparameters: gradient_clip_val, hidden_size, hidden_continuous_size, attention_head_size, dropout, learning_rate. There is a [known issue](https://github.com/jdb78/pytorch-forecasting/issues/1145) with pytorch-forecast logging.
|
||||||
* Custom estimator. Use custom estimator for:
|
* Custom estimator. Use custom estimator for:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user