class ProphetForecaster(Forecaster): """ Example: >>> #The dataset is split into data, validation_data >>> model = ProphetForecaster(changepoint_prior_scale=0.05, seasonality_mode='additive') >>> model.fit(data, validation_data) >>> predict_result = model.predict(horizon=24) """ def __init__( self, changepoint_prior_scale=0.05, seasonality_prior_scale=10.0, holidays_prior_scale=10.0, seasonality_mode='additive', changepoint_range=0.8, metric="mse", ): """ Build a Prophet Forecast Model. User can customize changepoint_prior_scale, seasonality_prior_scale, holidays_prior_scale, seasonality_mode, changepoint_range and metric of the Prophet model, for details of the Prophet model hyperparameters, refer to https://facebook.github.io/prophet/docs/diagnostics.html#hyperparameter-tuning. :param changepoint_prior_scale: hyperparameter changepoint_prior_scale for the Prophet model. :param seasonality_prior_scale: hyperparameter seasonality_prior_scale for the Prophet model. :param holidays_prior_scale: hyperparameter holidays_prior_scale for the Prophet model. :param seasonality_mode: hyperparameter seasonality_mode for the Prophet model. :param changepoint_range: hyperparameter changepoint_range for the Prophet model. :param metric: the metric for validation and evaluation. For regression, we support Mean Squared Error: ("mean_squared_error", "MSE" or "mse"), Mean Absolute Error: ("mean_absolute_error","MAE" or "mae"), Mean Absolute Percentage Error: ("mean_absolute_percentage_error", "MAPE", "mape") Cosine Proximity: ("cosine_proximity", "cosine") """ self.model_config = { "changepoint_prior_scale": changepoint_prior_scale, "seasonality_prior_scale": seasonality_prior_scale, "holidays_prior_scale": holidays_prior_scale, "seasonality_mode": seasonality_mode, "changepoint_range": changepoint_range, "metric": metric } self.internal = ProphetModel() super().__init__() def fit(self, data, validation_data): """ Fit(Train) the forecaster. :param data: training data, a pandas dataframe with Td rows, and 2 columns, with column 'ds' indicating date and column 'y' indicating value and Td is the time dimension :param validation_data: evaluation data, should be the same type as x """ self._check_data(data, validation_data) data_dict = { 'x': data, 'y': None, 'val_x': None, 'val_y': validation_data } return self.internal.fit_eval(data=data_dict, **self.model_config) def _check_data(self, data, validation_data): assert 'ds' in data.columns and 'y' in data.columns, \ "data should be a pandas dataframe that has at least 2 columns 'ds' and 'y'." assert 'ds' in validation_data.columns and 'y' in validation_data.columns, \ "validation_data should be a dataframe that has at least 2 columns 'ds' and 'y'." def predict(self, horizon): """ Predict using a trained forecaster. :param horizon: the number of steps forward to predict """ if self.internal.model is None: raise RuntimeError( "You must call fit or restore first before calling predict!") return self.internal.predict(horizon=horizon) def evaluate(self, validation_data, metrics=['mse']): """ Evaluate using a trained forecaster. :param validation_data: evaluation data, a pandas dataframe with Td rows, and 2 columns, with column 'ds' indicating date and column 'y' indicating value and Td is the time dimension :param data: We don't support input data currently. :param metrics: A list contains metrics for test/valid data. """ if validation_data is None: raise ValueError("Input invalid validation_data of None") if self.internal.model is None: raise RuntimeError( "You must call fit or restore first before calling evaluate!") return self.internal.evaluate(None, validation_data, metrics=metrics) def save(self, checkpoint_file): """ Save the forecaster. :param checkpoint_file: The location you want to save the forecaster, should be a json file """ if self.internal.model is None: raise RuntimeError( "You must call fit or restore first before calling save!") self.internal.save(checkpoint_file) def restore(self, checkpoint_file): """ Restore the forecaster. :param checkpoint_file: The checkpoint file location you want to load the forecaster. """ self.internal.restore(checkpoint_file)
class TestProphetModel(ZooTestCase): def setup_method(self, method): self.seq_len = 480 self.config = { "changepoint_prior_scale": np.exp(np.random.uniform(np.log(0.001), np.log(0.5))), "seasonality_prior_scale": np.exp(np.random.uniform(np.log(0.01), np.log(10))), "holidays_prior_scale": np.exp(np.random.uniform(np.log(0.01), np.log(10))), "seasonality_mode": np.random.choice(['additive', 'multiplicative']), "changepoint_range": np.random.uniform(0.8, 0.95) } self.model = ProphetModel() self.data = pd.DataFrame(pd.date_range('20130101', periods=self.seq_len), columns=['ds']) self.data.insert(1, 'y', np.random.rand(self.seq_len)) self.horizon = np.random.randint(2, 50) self.validation_data = pd.DataFrame(pd.date_range( '20140426', periods=self.horizon), columns=['ds']) self.validation_data.insert(1, 'y', np.random.rand(self.horizon)) def teardown_method(self, method): del self.model del self.data del self.validation_data def test_prophet(self): # test fit_eval evaluate_result = self.model.fit_eval( data=self.data, validation_data=self.validation_data, **self.config) # test predict result = self.model.predict(horizon=self.horizon) assert result.shape[0] == self.horizon # test evaluate evaluate_result = self.model.evaluate(target=self.validation_data, metrics=['mae', 'smape']) assert len(evaluate_result) == 2 def test_error(self): with pytest.raises(ValueError, match="We don't support input data currently"): self.model.predict(data=1) with pytest.raises(ValueError, match="We don't support input data currently"): self.model.evaluate(target=self.validation_data, data=1) with pytest.raises(ValueError, match="Input invalid target of None"): self.model.evaluate(target=None) with pytest.raises( Exception, match= "Needs to call fit_eval or restore first before calling predict" ): self.model.predict() with pytest.raises( Exception, match= "Needs to call fit_eval or restore first before calling evaluate" ): self.model.evaluate(target=self.validation_data) with pytest.raises( Exception, match= "Needs to call fit_eval or restore first before calling save"): model_file = "tmp.json" self.model.save(model_file) def test_save_restore(self): self.model.fit_eval(data=self.data, validation_data=self.validation_data, **self.config) result_save = self.model.predict(horizon=self.horizon) model_file = "tmp.json" self.model.save(model_file) assert os.path.isfile(model_file) new_model = ProphetModel() new_model.restore(model_file) assert new_model.model result_restore = new_model.predict(horizon=self.horizon) assert_array_almost_equal(result_save['yhat'], result_restore['yhat'], decimal=2), \ "Prediction values are not the same after restore: " \ "predict before is {}, and predict after is {}".format(result_save, result_restore) os.remove(model_file)