Skip to content

Univariate Prophet

Bases: ExogenousEffectMixin, BaseBayesianForecaster

Univariate Prophet model, similar to the one implemented in the prophet library.

The main difference between the mathematical model here and from the original one is the logistic trend. Here, another parametrization is considered, and the capacity is not passed as input, but inferred from the data.

With respect to API, this one follows sktime convention where all hiperparameters are passed during init, and uses changepoint_interval instead of n_changepoints to set the changepoints. There's no weekly_seasonality/yearly_seasonality, but instead, the user can pass a feature_transformer that will be used to generate the fourier terms. The same for holidays.

This model accepts configurations where each exogenous variable has a different function relating it to its additive effect on the time series. One can, for example, set different priors for a group of feature, or use a Hill function to model the effect of a feature.

Parameters:

Name Type Description Default
changepoint_interval int

The number of points between each potential changepoint.

25
changepoint_range float

Proportion of the history in which trend changepoints will be estimated. If a float between 0 and 1, the range will be that proportion of the history. If an int, the range will be that number of points. A negative int indicates number of points counting from the end of the history.

0.8
changepoint_prior_scale float

Parameter controlling the flexibility of the automatic changepoint selection.

0.001
offset_prior_scale float

Scale parameter for the prior distribution of the offset. The offset is the constant term in the piecewise trend equation. Default is 0.1.

0.1
feature_transformer BaseTransformer

Transformer object to generate Fourier terms, holiday or other features. Should be a sktime's Transformer

None
capacity_prior_scale float

Scale parameter for the prior distribution of the capacity.

0.2
capacity_prior_loc float

Location parameter for the prior distribution of the capacity.

1.1
noise_scale float

Scale parameter for the observation noise.

0.05
trend str

Type of trend to use. Can be "linear" or "logistic".

'linear'
mcmc_samples int

Number of MCMC samples to draw.

2000
mcmc_warmup int

Number of MCMC warmup steps.

200
mcmc_chains int

Number of MCMC chains to run in parallel.

4
inference_method str

Inference method to use. Can be "mcmc" or "map".

'map'
optimizer_name str

Name of the optimizer to use for variational inference.

'Adam'
optimizer_kwargs dict

Additional keyword arguments to pass to the optimizer.

None
optimizer_steps int

Number of optimization steps to perform for variational inference.

100000
exogenous_effects List[AbstractEffect]

A list defining the exogenous effects to be used in the model.

None
default_effect AbstractEffect

The default effect to be used when no effect is specified for a variable.

None
default_exogenous_prior tuple

Default prior distribution for exogenous effects.

required
rng_key PRNGKey

Random number generator key.

None
Source code in src/prophetverse/sktime/univariate.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
class Prophet(ExogenousEffectMixin, BaseBayesianForecaster):
    """

    Univariate Prophet model, similar to the one implemented in the `prophet` library.

    The main difference between the mathematical model here and from the original one is the
    logistic trend. Here, another parametrization is considered, and the capacity is not
    passed as input, but inferred from the data.

    With respect to API, this one follows sktime convention where all hiperparameters are passed during
    __init__, and uses `changepoint_interval` instead of `n_changepoints` to set the changepoints. There's no
    weekly_seasonality/yearly_seasonality, but instead, the user can pass a `feature_transformer` that
    will be used to generate the fourier terms. The same for holidays.

    This model accepts configurations where each exogenous variable has a different function relating it to its additive
    effect on the time series. One can, for example, set different priors for a group of feature, or use a Hill function
    to model the effect of a feature.


    Args:
        changepoint_interval (int): The number of points between each potential changepoint.
        changepoint_range (float): Proportion of the history in which trend changepoints will be estimated. 
                                   If a float between 0 and 1, the range will be that proportion of the history. 
                                   If an int, the range will be that number of points. A negative int indicates number of points
                                   counting from the end of the history.
        changepoint_prior_scale (float): Parameter controlling the flexibility of the automatic changepoint selection.
        offset_prior_scale (float): Scale parameter for the prior distribution of the offset. The offset is the constant term in the piecewise trend equation. Default is 0.1.
        feature_transformer (BaseTransformer): Transformer object to generate Fourier terms, holiday or other features. Should be a sktime's Transformer
        capacity_prior_scale (float): Scale parameter for the prior distribution of the capacity.
        capacity_prior_loc (float): Location parameter for the prior distribution of the capacity.
        noise_scale (float): Scale parameter for the observation noise.
        trend (str): Type of trend to use. Can be "linear" or "logistic".
        mcmc_samples (int): Number of MCMC samples to draw.
        mcmc_warmup (int): Number of MCMC warmup steps.
        mcmc_chains (int): Number of MCMC chains to run in parallel.
        inference_method (str): Inference method to use. Can be "mcmc" or "map".
        optimizer_name (str): Name of the optimizer to use for variational inference.
        optimizer_kwargs (dict): Additional keyword arguments to pass to the optimizer.
        optimizer_steps (int): Number of optimization steps to perform for variational inference.
        exogenous_effects (List[AbstractEffect]): A list defining the exogenous effects to be used in the model.
        default_effect (AbstractEffect): The default effect to be used when no effect is specified for a variable.
        default_exogenous_prior (tuple): Default prior distribution for exogenous effects.
        rng_key (jax.random.PRNGKey): Random number generator key.


    """

    _tags = {
        "requires-fh-in-fit": False,
        "y_inner_mtype": "pd.DataFrame",
    } 

    def __init__(
        self,
        changepoint_interval=25,
        changepoint_range=0.8,
        changepoint_prior_scale=0.001,
        offset_prior_scale=0.1,
        feature_transformer=None,
        capacity_prior_scale=0.2,
        capacity_prior_loc=1.1,
        noise_scale=0.05,
        trend="linear",
        mcmc_samples=2000,
        mcmc_warmup=200,
        mcmc_chains=4,
        inference_method="map",
        optimizer_name="Adam",
        optimizer_kwargs=None,
        optimizer_steps=100_000,
        exogenous_effects=None,
        default_effect=None,
        scale=None,
        rng_key=None,
    ):
        """
        Initializes the Prophet model.
        """

        self.changepoint_interval = changepoint_interval
        self.changepoint_range = changepoint_range
        self.changepoint_prior_scale = changepoint_prior_scale
        self.offset_prior_scale = offset_prior_scale
        self.noise_scale = noise_scale
        self.feature_transformer = feature_transformer
        self.capacity_prior_scale = capacity_prior_scale
        self.capacity_prior_loc = capacity_prior_loc
        self.trend = trend

        super().__init__(
            rng_key=rng_key,
            # ExogenousEffectMixin
            default_effect=default_effect,
            exogenous_effects=exogenous_effects,
            # BaseBayesianForecaster
            inference_method=inference_method,
            mcmc_samples=mcmc_samples,
            mcmc_warmup=mcmc_warmup,
            mcmc_chains=mcmc_chains,
            optimizer_name=optimizer_name,
            optimizer_kwargs=optimizer_kwargs,
            optimizer_steps=optimizer_steps,
            scale=scale,
        )

        self.model = univariate_model
        self._validate_hyperparams()

    def _validate_hyperparams(self):
        """
        Validate the hyperparameters
        """
        if self.changepoint_interval <= 0:
            raise ValueError("changepoint_interval must be greater than 0.")

        if self.changepoint_prior_scale <= 0:
            raise ValueError("changepoint_prior_scale must be greater than 0.")
        if self.noise_scale <= 0:
            raise ValueError("noise_scale must be greater than 0.")
        if self.capacity_prior_scale <= 0:
            raise ValueError("capacity_prior_scale must be greater than 0.")
        if self.capacity_prior_loc <= 0:
            raise ValueError("capacity_prior_loc must be greater than 0.")
        if self.offset_prior_scale <= 0:
            raise ValueError("offset_prior_scale must be greater than 0.")
        if self.trend not in ["linear", "logistic", "flat"] and not isinstance(self.trend, TrendModel):
            raise ValueError('trend must be either "linear" or "logistic".')

    def _get_fit_data(self, y, X, fh):
        """
        Prepares the data for the Numpyro model.

        Args:
            y (pd.DataFrame): Time series data.
            X (pd.DataFrame): Exogenous variables.
            fh (ForecastingHorizon): Forecasting horizon.

        Returns:
            dict: Dictionary of data for the Numpyro model.
        """

        fh = y.index.get_level_values(-1).unique()

        self.trend_model_ = self._get_trend_model()
        self.trend_model_.initialize(y)

        trend_data = self.trend_model_.prepare_input_data(fh)

        ## Exogenous features

        if X is None:
            X = pd.DataFrame(index=y.index)

        if self.feature_transformer is not None:

            X = self.feature_transformer.fit_transform(X)

        self._has_exogenous = ~X.columns.empty
        X = X.loc[y.index]

        self._set_custom_effects(X.columns)
        exogenous_data = self._get_exogenous_data_array(X)

        y_array = jnp.array(y.values.flatten()).reshape((-1, 1))

        ## Inputs that also are used in predict
        self.fit_and_predict_data_ = {

            "trend_model": self.trend_model_,
            "noise_scale" : self.noise_scale,
            "exogenous_effects": (
                self.exogenous_effect_dict if self._has_exogenous else None
            ),
        }

        inputs = {
            "y": y_array,
            "data": exogenous_data,
            "trend_data": trend_data,
            **self.fit_and_predict_data_,
        }

        return inputs

    def _get_predict_data(
        self, X: pd.DataFrame, fh: ForecastingHorizon
    ) -> dict:
        """
        Prepares the data for making predictions.

        Args:
            X (pd.DataFrame): Exogenous variables.
            fh (ForecastingHorizon): Forecasting horizon.

        Returns:
            dict: Dictionary of data for the Numpyro model.
        """

        fh_dates = self.fh_to_index(fh)
        fh_as_index = pd.Index(list(fh_dates.to_numpy()))

        trend_data = self.trend_model_.prepare_input_data(fh_as_index)

        if X is None:
            X = pd.DataFrame(index=fh_as_index)

        if self.feature_transformer is not None:
            X = self.feature_transformer.transform(X)

        exogenous_data = (
            self._get_exogenous_data_array(X.loc[fh_as_index]) if self._has_exogenous else None
        )

        return dict(
            y=None,
            data=exogenous_data,
            trend_data=trend_data,
            **self.fit_and_predict_data_,
        )

    def _get_trend_model(self):
        """
        Returns the trend model based on the specified trend parameter.

        Returns:
            TrendModel: The trend model based on the specified trend parameter.

        Raises:
            ValueError: If the trend parameter is not one of 'linear', 'logistic', 'flat' or a TrendModel instance.
        """

        ## Changepoints and trend
        if self.trend == "linear":
            return PiecewiseLinearTrend(
                changepoint_interval=self.changepoint_interval,
                changepoint_range=self.changepoint_range,
                changepoint_prior_scale=self.changepoint_prior_scale,
                offset_prior_scale=self.offset_prior_scale,
            )

        elif self.trend == "logistic":
            return PiecewiseLogisticTrend(
                changepoint_interval=self.changepoint_interval,
                changepoint_range=self.changepoint_range,
                changepoint_prior_scale=self.changepoint_prior_scale,
                offset_prior_scale=self.offset_prior_scale,
                capacity_prior=dist.TransformedDistribution(
                    dist.HalfNormal(self.capacity_prior_scale),
                    dist.transforms.AffineTransform(
                        loc=self.capacity_prior_loc, scale=1
                    ),
                ),
            )
        elif self.trend == "flat":
            return FlatTrend(
                changepoint_prior_scale=self.changepoint_prior_scale
            )

        elif isinstance(self.trend, TrendModel):
            return self.trend

        raise ValueError(
            "trend must be either 'linear', 'logistic' or a TrendModel instance."
        )

    @classmethod
    def get_test_params(cls, parameter_set="default"):

        return [{
            "optimizer_steps": 1_000,
        }]

__init__(changepoint_interval=25, changepoint_range=0.8, changepoint_prior_scale=0.001, offset_prior_scale=0.1, feature_transformer=None, capacity_prior_scale=0.2, capacity_prior_loc=1.1, noise_scale=0.05, trend='linear', mcmc_samples=2000, mcmc_warmup=200, mcmc_chains=4, inference_method='map', optimizer_name='Adam', optimizer_kwargs=None, optimizer_steps=100000, exogenous_effects=None, default_effect=None, scale=None, rng_key=None)

Initializes the Prophet model.

Source code in src/prophetverse/sktime/univariate.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def __init__(
    self,
    changepoint_interval=25,
    changepoint_range=0.8,
    changepoint_prior_scale=0.001,
    offset_prior_scale=0.1,
    feature_transformer=None,
    capacity_prior_scale=0.2,
    capacity_prior_loc=1.1,
    noise_scale=0.05,
    trend="linear",
    mcmc_samples=2000,
    mcmc_warmup=200,
    mcmc_chains=4,
    inference_method="map",
    optimizer_name="Adam",
    optimizer_kwargs=None,
    optimizer_steps=100_000,
    exogenous_effects=None,
    default_effect=None,
    scale=None,
    rng_key=None,
):
    """
    Initializes the Prophet model.
    """

    self.changepoint_interval = changepoint_interval
    self.changepoint_range = changepoint_range
    self.changepoint_prior_scale = changepoint_prior_scale
    self.offset_prior_scale = offset_prior_scale
    self.noise_scale = noise_scale
    self.feature_transformer = feature_transformer
    self.capacity_prior_scale = capacity_prior_scale
    self.capacity_prior_loc = capacity_prior_loc
    self.trend = trend

    super().__init__(
        rng_key=rng_key,
        # ExogenousEffectMixin
        default_effect=default_effect,
        exogenous_effects=exogenous_effects,
        # BaseBayesianForecaster
        inference_method=inference_method,
        mcmc_samples=mcmc_samples,
        mcmc_warmup=mcmc_warmup,
        mcmc_chains=mcmc_chains,
        optimizer_name=optimizer_name,
        optimizer_kwargs=optimizer_kwargs,
        optimizer_steps=optimizer_steps,
        scale=scale,
    )

    self.model = univariate_model
    self._validate_hyperparams()