Browse Source

Multiplicative seasonality (Py)

Ben Letham 7 years ago
parent
commit
8d8c5b41ce

+ 1 - 0
python/fbprophet/diagnostics.py

@@ -184,6 +184,7 @@ def prophet_copy(m, cutoff=None):
         weekly_seasonality=False,
         weekly_seasonality=False,
         daily_seasonality=False,
         daily_seasonality=False,
         holidays=m.holidays,
         holidays=m.holidays,
+        seasonality_mode=m.seasonality_mode,
         seasonality_prior_scale=m.seasonality_prior_scale,
         seasonality_prior_scale=m.seasonality_prior_scale,
         changepoint_prior_scale=m.changepoint_prior_scale,
         changepoint_prior_scale=m.changepoint_prior_scale,
         holidays_prior_scale=m.holidays_prior_scale,
         holidays_prior_scale=m.holidays_prior_scale,

+ 177 - 73
python/fbprophet/forecaster.py

@@ -70,6 +70,7 @@ class Prophet(object):
         lower_window=-2 will include 2 days prior to the date as holidays. Also
         lower_window=-2 will include 2 days prior to the date as holidays. Also
         optionally can have a column prior_scale specifying the prior scale for
         optionally can have a column prior_scale specifying the prior scale for
         that holiday.
         that holiday.
+    seasonality_mode: 'additive' (default) or 'multiplicative'.
     seasonality_prior_scale: Parameter modulating the strength of the
     seasonality_prior_scale: Parameter modulating the strength of the
         seasonality model. Larger values allow the model to fit larger seasonal
         seasonality model. Larger values allow the model to fit larger seasonal
         fluctuations, smaller values dampen the seasonality. Can be specified
         fluctuations, smaller values dampen the seasonality. Can be specified
@@ -100,6 +101,7 @@ class Prophet(object):
             weekly_seasonality='auto',
             weekly_seasonality='auto',
             daily_seasonality='auto',
             daily_seasonality='auto',
             holidays=None,
             holidays=None,
+            seasonality_mode='additive',
             seasonality_prior_scale=10.0,
             seasonality_prior_scale=10.0,
             holidays_prior_scale=10.0,
             holidays_prior_scale=10.0,
             changepoint_prior_scale=0.05,
             changepoint_prior_scale=0.05,
@@ -132,6 +134,7 @@ class Prophet(object):
             holidays['ds'] = pd.to_datetime(holidays['ds'])
             holidays['ds'] = pd.to_datetime(holidays['ds'])
         self.holidays = holidays
         self.holidays = holidays
 
 
+        self.seasonality_mode = seasonality_mode
         self.seasonality_prior_scale = float(seasonality_prior_scale)
         self.seasonality_prior_scale = float(seasonality_prior_scale)
         self.changepoint_prior_scale = float(changepoint_prior_scale)
         self.changepoint_prior_scale = float(changepoint_prior_scale)
         self.holidays_prior_scale = float(holidays_prior_scale)
         self.holidays_prior_scale = float(holidays_prior_scale)
@@ -173,6 +176,10 @@ class Prophet(object):
                     raise ValueError('Holiday upper_window should be >= 0')
                     raise ValueError('Holiday upper_window should be >= 0')
             for h in self.holidays['holiday'].unique():
             for h in self.holidays['holiday'].unique():
                 self.validate_column_name(h, check_holidays=False)
                 self.validate_column_name(h, check_holidays=False)
+        if self.seasonality_mode not in ['additive', 'multiplicative']:
+            raise ValueError(
+                "seasonality_mode must be 'additive' or 'multiplicative'"
+            )
 
 
     def validate_column_name(self, name, check_holidays=True,
     def validate_column_name(self, name, check_holidays=True,
                              check_seasonalities=True, check_regressors=True):
                              check_seasonalities=True, check_regressors=True):
@@ -189,7 +196,8 @@ class Prophet(object):
             raise ValueError('Name cannot contain "_delim_"')
             raise ValueError('Name cannot contain "_delim_"')
         reserved_names = [
         reserved_names = [
             'trend', 'additive_terms', 'daily', 'weekly', 'yearly',
             'trend', 'additive_terms', 'daily', 'weekly', 'yearly',
-            'holidays', 'zeros', 'extra_regressors', 'yhat'
+            'holidays', 'zeros', 'extra_regressors_additive',
+            'extra_regressors_multiplicative', 'yhat',
         ]
         ]
         rn_l = [n + '_lower' for n in reserved_names]
         rn_l = [n + '_lower' for n in reserved_names]
         rn_u = [n + '_upper' for n in reserved_names]
         rn_u = [n + '_upper' for n in reserved_names]
@@ -410,6 +418,7 @@ class Prophet(object):
         -------
         -------
         holiday_features: pd.DataFrame with a column for each holiday.
         holiday_features: pd.DataFrame with a column for each holiday.
         prior_scale_list: List of prior scales for each holiday column.
         prior_scale_list: List of prior scales for each holiday column.
+        holiday_names: List of names of holidays
         """
         """
         # Holds columns of our future matrix.
         # Holds columns of our future matrix.
         expanded_holidays = defaultdict(lambda: np.zeros(dates.shape[0]))
         expanded_holidays = defaultdict(lambda: np.zeros(dates.shape[0]))
@@ -461,9 +470,11 @@ class Prophet(object):
             prior_scales[h.split('_delim_')[0]]
             prior_scales[h.split('_delim_')[0]]
             for h in holiday_features.columns
             for h in holiday_features.columns
         ]
         ]
-        return holiday_features, prior_scale_list
+        return holiday_features, prior_scale_list, list(prior_scales.keys())
 
 
-    def add_regressor(self, name, prior_scale=None, standardize='auto'):
+    def add_regressor(
+        self, name, prior_scale=None, standardize='auto', mode=None
+    ):
         """Add an additional regressor to be used for fitting and predicting.
         """Add an additional regressor to be used for fitting and predicting.
 
 
         The dataframe passed to `fit` and `predict` will have a column with the
         The dataframe passed to `fit` and `predict` will have a column with the
@@ -472,6 +483,10 @@ class Prophet(object):
         coefficient is given a prior with the specified scale parameter.
         coefficient is given a prior with the specified scale parameter.
         Decreasing the prior scale will add additional regularization. If no
         Decreasing the prior scale will add additional regularization. If no
         prior scale is provided, self.holidays_prior_scale will be used.
         prior scale is provided, self.holidays_prior_scale will be used.
+        Mode can be specified as either 'additive' or 'multiplicative'. If not
+        specified, self.seasonality_mode will be used. 'additive' means the
+        effect of the regressor will be added to the trend, 'multiplicative'
+        means it will multiply the trend.
 
 
         Parameters
         Parameters
         ----------
         ----------
@@ -481,6 +496,8 @@ class Prophet(object):
         standardize: optional, specify whether this regressor will be
         standardize: optional, specify whether this regressor will be
             standardized prior to fitting. Can be 'auto' (standardize if not
             standardized prior to fitting. Can be 'auto' (standardize if not
             binary), True, or False.
             binary), True, or False.
+        mode: optional, 'additive' or 'multiplicative'. Defaults to
+            self.seasonality_mode.
 
 
         Returns
         Returns
         -------
         -------
@@ -492,16 +509,23 @@ class Prophet(object):
         self.validate_column_name(name, check_regressors=False)
         self.validate_column_name(name, check_regressors=False)
         if prior_scale is None:
         if prior_scale is None:
             prior_scale = float(self.holidays_prior_scale)
             prior_scale = float(self.holidays_prior_scale)
+        if mode is None:
+            mode = self.seasonality_mode
         assert prior_scale > 0
         assert prior_scale > 0
+        if mode not in ['additive', 'multiplicative']:
+            raise ValueError("mode must be 'additive' or 'multiplicative'")
         self.extra_regressors[name] = {
         self.extra_regressors[name] = {
             'prior_scale': prior_scale,
             'prior_scale': prior_scale,
             'standardize': standardize,
             'standardize': standardize,
             'mu': 0.,
             'mu': 0.,
             'std': 1.,
             'std': 1.,
+            'mode': mode,
         }
         }
         return self
         return self
 
 
-    def add_seasonality(self, name, period, fourier_order, prior_scale=None):
+    def add_seasonality(
+        self, name, period, fourier_order, prior_scale=None, mode=None
+    ):
         """Add a seasonal component with specified period, number of Fourier
         """Add a seasonal component with specified period, number of Fourier
         components, and prior scale.
         components, and prior scale.
 
 
@@ -514,12 +538,18 @@ class Prophet(object):
         seasonality_prior_scale provided on Prophet initialization (defaults
         seasonality_prior_scale provided on Prophet initialization (defaults
         to 10).
         to 10).
 
 
+        Mode can be specified as either 'additive' or 'multiplicative'. If not
+        specified, self.seasonality_mode will be used (defaults to additive).
+        Additive means the seasonality will be added to the trend,
+        multiplicative means it will multiply the trend.
+
         Parameters
         Parameters
         ----------
         ----------
         name: string name of the seasonality component.
         name: string name of the seasonality component.
         period: float number of days in one period.
         period: float number of days in one period.
         fourier_order: int number of Fourier components to use.
         fourier_order: int number of Fourier components to use.
-        prior_scale: float prior scale for this component.
+        prior_scale: optional float prior scale for this component.
+        mode: optional 'additive' or 'multiplicative'
 
 
         Returns
         Returns
         -------
         -------
@@ -537,10 +567,15 @@ class Prophet(object):
             ps = float(prior_scale)
             ps = float(prior_scale)
         if ps <= 0:
         if ps <= 0:
             raise ValueError('Prior scale must be > 0')
             raise ValueError('Prior scale must be > 0')
+        if mode is None:
+            mode = self.seasonality_mode
+        if mode not in ['additive', 'multiplicative']:
+            raise ValueError("mode must be 'additive' or 'multiplicative'")
         self.seasonalities[name] = {
         self.seasonalities[name] = {
             'period': period,
             'period': period,
             'fourier_order': fourier_order,
             'fourier_order': fourier_order,
             'prior_scale': ps,
             'prior_scale': ps,
+            'mode': mode,
         }
         }
         return self
         return self
 
 
@@ -560,9 +595,12 @@ class Prophet(object):
         list of prior scales for each column of the features dataframe.
         list of prior scales for each column of the features dataframe.
         Dataframe with indicators for which regression components correspond to
         Dataframe with indicators for which regression components correspond to
             which columns.
             which columns.
+        Dictionary with keys 'additive' and 'multiplicative' listing the
+            component names for each mode of seasonality.
         """
         """
         seasonal_features = []
         seasonal_features = []
         prior_scales = []
         prior_scales = []
+        modes = {'additive': [], 'multiplicative': []}
 
 
         # Seasonality features
         # Seasonality features
         for name, props in self.seasonalities.items():
         for name, props in self.seasonalities.items():
@@ -575,26 +613,120 @@ class Prophet(object):
             seasonal_features.append(features)
             seasonal_features.append(features)
             prior_scales.extend(
             prior_scales.extend(
                 [props['prior_scale']] * features.shape[1])
                 [props['prior_scale']] * features.shape[1])
+            modes[props['mode']].append(name)
 
 
         # Holiday features
         # Holiday features
         if self.holidays is not None:
         if self.holidays is not None:
-            features, holiday_priors = self.make_holiday_features(df['ds'])
+            features, holiday_priors, holiday_names = (
+                self.make_holiday_features(df['ds'])
+            )
             seasonal_features.append(features)
             seasonal_features.append(features)
             prior_scales.extend(holiday_priors)
             prior_scales.extend(holiday_priors)
+            modes[self.seasonality_mode].extend(holiday_names)
 
 
         # Additional regressors
         # Additional regressors
         for name, props in self.extra_regressors.items():
         for name, props in self.extra_regressors.items():
             seasonal_features.append(pd.DataFrame(df[name]))
             seasonal_features.append(pd.DataFrame(df[name]))
             prior_scales.append(props['prior_scale'])
             prior_scales.append(props['prior_scale'])
+            modes[props['mode']].append(name)
 
 
+        # Dummy to prevent empty X
         if len(seasonal_features) == 0:
         if len(seasonal_features) == 0:
             seasonal_features.append(
             seasonal_features.append(
                 pd.DataFrame({'zeros': np.zeros(df.shape[0])}))
                 pd.DataFrame({'zeros': np.zeros(df.shape[0])}))
             prior_scales.append(1.)
             prior_scales.append(1.)
 
 
         seasonal_features = pd.concat(seasonal_features, axis=1)
         seasonal_features = pd.concat(seasonal_features, axis=1)
-        component_cols = self.regressor_column_matrix(seasonal_features)
-        return seasonal_features, prior_scales, component_cols
+        component_cols, modes = self.regressor_column_matrix(
+            seasonal_features, modes
+        )
+        return seasonal_features, prior_scales, component_cols, modes
+
+    def regressor_column_matrix(self, seasonal_features, modes):
+        """Dataframe indicating which columns of the feature matrix correspond
+        to which seasonality/regressor components.
+
+        Includes combination components, like 'additive_terms'. These
+        combination components will be added to the 'modes' input.
+
+        Parameters
+        ----------
+        seasonal_features: Constructed seasonal features dataframe
+        modes: Dictionary with keys 'additive' and 'multiplicative' listing the
+            component names for each mode of seasonality.
+
+        Returns
+        -------
+        component_cols: A binary indicator dataframe with columns seasonal
+            components and rows columns in seasonal_features. Entry is 1 if
+            that columns is used in that component.
+        modes: Updated input with combination components.
+        """
+        components = pd.DataFrame({
+            'col': np.arange(seasonal_features.shape[1]),
+            'component': [
+                x.split('_delim_')[0] for x in seasonal_features.columns
+            ],
+        })
+        # Add total for holidays
+        if self.holidays is not None:
+            components = self.add_group_component(
+                components, 'holidays', self.holidays['holiday'].unique())
+        # Add totals additive and multiplicative components, and regressors
+        for mode in ['additive', 'multiplicative']:
+            components = self.add_group_component(
+                components, mode + '_terms', modes[mode]
+            )
+            regressors_by_mode = [
+                r for r, props in self.extra_regressors.items()
+                if props['mode'] == mode
+            ]
+            components = self.add_group_component(
+                components, 'extra_regressors_' + mode, regressors_by_mode)
+            # Add combination components to modes
+            modes[mode].append(mode + '_terms')
+            modes[mode].append('extra_regressors_' + mode)
+        # Convert to a binary matrix
+        component_cols = pd.crosstab(
+            components['col'], components['component'],
+        )
+        # Add columns for additive and multiplicative terms, if missing
+        for name in ['additive_terms', 'multiplicative_terms']:
+            if name not in component_cols:
+                component_cols[name] = 0
+        # Remove the placeholder
+        component_cols.drop('zeros', axis=1, inplace=True, errors='ignore')
+        # Validation
+        if (
+            max(component_cols['additive_terms']
+            + component_cols['multiplicative_terms']) > 1
+        ):
+            raise Exception('A bug occurred in seasonal components.')
+        # Compare to the training, if set.
+        if self.train_component_cols is not None:
+            component_cols = component_cols[self.train_component_cols.columns]
+            if not component_cols.equals(self.train_component_cols):
+                raise Exception('A bug occurred in constructing regressors.')
+        return component_cols, modes
+
+    def add_group_component(self, components, name, group):
+        """Adds a component with given name that contains all of the components
+        in group.
+
+        Parameters
+        ----------
+        components: Dataframe with components.
+        name: Name of new group component.
+        group: List of components that form the group.
+
+        Returns
+        -------
+        Dataframe with components.
+        """
+        new_comp = components[components['component'].isin(set(group))].copy()
+        new_comp['component'] = name
+        components = components.append(new_comp)
+        return components
 
 
     def parse_seasonality_args(self, name, arg, auto_disable, default_order):
     def parse_seasonality_args(self, name, arg, auto_disable, default_order):
         """Get number of fourier components for built-in seasonalities.
         """Get number of fourier components for built-in seasonalities.
@@ -656,6 +788,7 @@ class Prophet(object):
                 'period': 365.25,
                 'period': 365.25,
                 'fourier_order': fourier_order,
                 'fourier_order': fourier_order,
                 'prior_scale': self.seasonality_prior_scale,
                 'prior_scale': self.seasonality_prior_scale,
+                'mode': self.seasonality_mode,
             }
             }
 
 
         # Weekly seasonality
         # Weekly seasonality
@@ -668,6 +801,7 @@ class Prophet(object):
                 'period': 7,
                 'period': 7,
                 'fourier_order': fourier_order,
                 'fourier_order': fourier_order,
                 'prior_scale': self.seasonality_prior_scale,
                 'prior_scale': self.seasonality_prior_scale,
+                'mode': self.seasonality_mode,
             }
             }
 
 
         # Daily seasonality
         # Daily seasonality
@@ -680,6 +814,7 @@ class Prophet(object):
                 'period': 1,
                 'period': 1,
                 'fourier_order': fourier_order,
                 'fourier_order': fourier_order,
                 'prior_scale': self.seasonality_prior_scale,
                 'prior_scale': self.seasonality_prior_scale,
+                'mode': self.seasonality_mode,
             }
             }
 
 
     @staticmethod
     @staticmethod
@@ -785,7 +920,7 @@ class Prophet(object):
         history = self.setup_dataframe(history, initialize_scales=True)
         history = self.setup_dataframe(history, initialize_scales=True)
         self.history = history
         self.history = history
         self.set_auto_seasonalities()
         self.set_auto_seasonalities()
-        seasonal_features, prior_scales, component_cols = (
+        seasonal_features, prior_scales, component_cols, _ = (
             self.make_all_seasonality_features(history))
             self.make_all_seasonality_features(history))
         self.train_component_cols = component_cols
         self.train_component_cols = component_cols
 
 
@@ -802,6 +937,8 @@ class Prophet(object):
             'sigmas': prior_scales,
             'sigmas': prior_scales,
             'tau': self.changepoint_prior_scale,
             'tau': self.changepoint_prior_scale,
             'trend_indicator': int(self.growth == 'logistic'),
             'trend_indicator': int(self.growth == 'logistic'),
+            's_a': component_cols['additive_terms'],
+            's_m': component_cols['multiplicative_terms'],
         }
         }
 
 
         if self.growth == 'linear':
         if self.growth == 'linear':
@@ -891,7 +1028,10 @@ class Prophet(object):
             cols.append('floor')
             cols.append('floor')
         # Add in forecast components
         # Add in forecast components
         df2 = pd.concat((df[cols], intervals, seasonal_components), axis=1)
         df2 = pd.concat((df[cols], intervals, seasonal_components), axis=1)
-        df2['yhat'] = df2['trend'] + df2['additive_terms']
+        df2['yhat'] = (
+            df2['trend'] * (1 + df2['multiplicative_terms'])
+            + df2['additive_terms']
+        )
         return df2
         return df2
 
 
     @staticmethod
     @staticmethod
@@ -991,7 +1131,7 @@ class Prophet(object):
         -------
         -------
         Dataframe with seasonal components.
         Dataframe with seasonal components.
         """
         """
-        seasonal_features, _, component_cols = (
+        seasonal_features, _, component_cols, modes = (
             self.make_all_seasonality_features(df)
             self.make_all_seasonality_features(df)
         )
         )
         lower_p = 100 * (1.0 - self.interval_width) / 2
         lower_p = 100 * (1.0 - self.interval_width) / 2
@@ -1002,7 +1142,9 @@ class Prophet(object):
         for component in component_cols.columns:
         for component in component_cols.columns:
             beta_c = self.params['beta'] * component_cols[component].values
             beta_c = self.params['beta'] * component_cols[component].values
 
 
-            comp = np.matmul(X, beta_c.transpose()) * self.y_scale
+            comp = np.matmul(X, beta_c.transpose())
+            if component in modes['additive']:
+                 comp *= self.y_scale
             data[component] = np.nanmean(comp, axis=1)
             data[component] = np.nanmean(comp, axis=1)
             data[component + '_lower'] = np.nanpercentile(
             data[component + '_lower'] = np.nanpercentile(
                 comp, lower_p, axis=1,
                 comp, lower_p, axis=1,
@@ -1012,54 +1154,6 @@ class Prophet(object):
             )
             )
         return pd.DataFrame(data)
         return pd.DataFrame(data)
 
 
-    def regressor_column_matrix(self, seasonal_features):
-        components = pd.DataFrame({
-            'col': np.arange(seasonal_features.shape[1]),
-            'component': [x.split('_delim_')[0] for x in seasonal_features.columns],
-        })
-        # Add total for all additive components
-        components = components.append(pd.DataFrame({
-            'col': np.arange(seasonal_features.shape[1]),
-            'component': 'additive_terms',
-        }))
-        # Add totals for holidays and extra regressors
-        if self.holidays is not None:
-            components = self.add_group_component(
-                components, 'holidays', self.holidays['holiday'].unique())
-        components = self.add_group_component(
-            components, 'extra_regressors', self.extra_regressors.keys())
-        # Remove the placeholder
-        components = components[components['component'] != 'zeros']
-        # Convert to a binary matrix
-        component_cols = pd.crosstab(
-            components['col'], components['component'],
-        )
-        # Compare to the training, if set.
-        if self.train_component_cols is not None:
-            component_cols = component_cols[self.train_component_cols.columns]
-            if not component_cols.equals(self.train_component_cols):
-                raise Exception('A bug occurred in constructing regressors.')
-        return component_cols
-
-    def add_group_component(self, components, name, group):
-        """Adds a component with given name that contains all of the components
-        in group.
-
-        Parameters
-        ----------
-        components: Dataframe with components.
-        name: Name of new group component.
-        group: List of components that form the group.
-
-        Returns
-        -------
-        Dataframe with components.
-        """
-        new_comp = components[components['component'].isin(set(group))].copy()
-        new_comp['component'] = name
-        components = components.append(new_comp)
-        return components
-
     def sample_posterior_predictive(self, df):
     def sample_posterior_predictive(self, df):
         """Prophet posterior predictive samples.
         """Prophet posterior predictive samples.
 
 
@@ -1069,7 +1163,8 @@ class Prophet(object):
 
 
         Returns
         Returns
         -------
         -------
-        Dictionary with posterior predictive samples for each component.
+        Dictionary with posterior predictive samples for the forecast yhat and
+        for the trend component.
         """
         """
         n_iterations = self.params['k'].shape[0]
         n_iterations = self.params['k'].shape[0]
         samp_per_iter = max(1, int(np.ceil(
         samp_per_iter = max(1, int(np.ceil(
@@ -1077,12 +1172,20 @@ class Prophet(object):
         )))
         )))
 
 
         # Generate seasonality features once so we can re-use them.
         # Generate seasonality features once so we can re-use them.
-        seasonal_features, _, _ = self.make_all_seasonality_features(df)
+        seasonal_features, _, component_cols, _ = (
+            self.make_all_seasonality_features(df)
+        )
 
 
-        sim_values = {'yhat': [], 'trend': [], 'seasonal': []}
+        sim_values = {'yhat': [], 'trend': []}
         for i in range(n_iterations):
         for i in range(n_iterations):
             for _j in range(samp_per_iter):
             for _j in range(samp_per_iter):
-                sim = self.sample_model(df, seasonal_features, i)
+                sim = self.sample_model(
+                    df=df,
+                    seasonal_features=seasonal_features,
+                    iteration=i,
+                    s_a=component_cols['additive_terms'],
+                    s_m=component_cols['multiplicative_terms'],
+                )
                 for key in sim_values:
                 for key in sim_values:
                     sim_values[key].append(sim[key])
                     sim_values[key].append(sim[key])
         for k, v in sim_values.items():
         for k, v in sim_values.items():
@@ -1099,9 +1202,8 @@ class Prophet(object):
 
 
         Returns
         Returns
         -------
         -------
-        Dictionary with keys "trend", "seasonal", and "yhat" containing
-        posterior predictive samples for that component. "seasonal" is the sum
-        of seasonalities, holidays, and added regressors.
+        Dictionary with keys "trend" and "yhat" containing
+        posterior predictive samples for that component.
         """
         """
         df = self.setup_dataframe(df.copy())
         df = self.setup_dataframe(df.copy())
         sim_values = self.sample_posterior_predictive(df)
         sim_values = self.sample_posterior_predictive(df)
@@ -1132,7 +1234,7 @@ class Prophet(object):
 
 
         return pd.DataFrame(series)
         return pd.DataFrame(series)
 
 
-    def sample_model(self, df, seasonal_features, iteration):
+    def sample_model(self, df, seasonal_features, iteration, s_a, s_m):
         """Simulate observations from the extrapolated generative model.
         """Simulate observations from the extrapolated generative model.
 
 
         Parameters
         Parameters
@@ -1143,20 +1245,22 @@ class Prophet(object):
 
 
         Returns
         Returns
         -------
         -------
-        Dataframe with trend, seasonality, and yhat, each like df['t'].
+        Dataframe with trend and yhat, each like df['t'].
         """
         """
         trend = self.sample_predictive_trend(df, iteration)
         trend = self.sample_predictive_trend(df, iteration)
 
 
         beta = self.params['beta'][iteration]
         beta = self.params['beta'][iteration]
-        seasonal = np.matmul(seasonal_features.as_matrix(), beta) * self.y_scale
+        Xb_a = (
+            np.matmul(seasonal_features.as_matrix(), beta * s_a) * self.y_scale
+        )
+        Xb_m = np.matmul(seasonal_features.as_matrix(), beta * s_m)
 
 
         sigma = self.params['sigma_obs'][iteration]
         sigma = self.params['sigma_obs'][iteration]
         noise = np.random.normal(0, sigma, df.shape[0]) * self.y_scale
         noise = np.random.normal(0, sigma, df.shape[0]) * self.y_scale
 
 
         return pd.DataFrame({
         return pd.DataFrame({
-            'yhat': trend + seasonal + noise,
-            'trend': trend,
-            'seasonal': seasonal,
+            'yhat': trend * (1 + Xb_m) + Xb_a + noise,
+            'trend': trend
         })
         })
 
 
     def sample_predictive_trend(self, df, iteration):
     def sample_predictive_trend(self, df, iteration):

+ 18 - 14
python/fbprophet/plot.py

@@ -77,7 +77,8 @@ def plot_components(
     """Plot the Prophet forecast components.
     """Plot the Prophet forecast components.
 
 
     Will plot whichever are available of: trend, holidays, weekly
     Will plot whichever are available of: trend, holidays, weekly
-    seasonality, and yearly seasonality.
+    seasonality, yearly seasonality, and additive and multiplicative extra
+    regressors.
 
 
     Parameters
     Parameters
     ----------
     ----------
@@ -103,8 +104,12 @@ def plot_components(
         components.append('holidays')
         components.append('holidays')
     components.extend([name for name in m.seasonalities
     components.extend([name for name in m.seasonalities
                     if name in fcst])
                     if name in fcst])
-    if len(m.extra_regressors) > 0 and 'extra_regressors' in fcst:
-        components.append('extra_regressors')
+    regressors = {'additive': False, 'multiplicative': False}
+    for name, props in m.extra_regressors.items():
+        regressors[props['mode']] = True
+    for mode in ['additive', 'multiplicative']:
+        if regressors[mode] and 'extra_regressors_{}'.format(mode) in fcst:
+            components.append('extra_regressors_{}'.format(mode))
     npanel = len(components)
     npanel = len(components)
 
 
     fig, axes = plt.subplots(npanel, 1, facecolor='w',
     fig, axes = plt.subplots(npanel, 1, facecolor='w',
@@ -119,11 +124,6 @@ def plot_components(
                 m=m, fcst=fcst, name='trend', ax=ax, uncertainty=uncertainty,
                 m=m, fcst=fcst, name='trend', ax=ax, uncertainty=uncertainty,
                 plot_cap=plot_cap,
                 plot_cap=plot_cap,
             )
             )
-        elif plot_name == 'holidays':
-            plot_forecast_component(
-                m=m, fcst=fcst, name='holidays', ax=ax,
-                uncertainty=uncertainty, plot_cap=False,
-            )
         elif plot_name == 'weekly':
         elif plot_name == 'weekly':
             plot_weekly(
             plot_weekly(
                 m=m, ax=ax, uncertainty=uncertainty, weekly_start=weekly_start,
                 m=m, ax=ax, uncertainty=uncertainty, weekly_start=weekly_start,
@@ -132,10 +132,14 @@ def plot_components(
             plot_yearly(
             plot_yearly(
                 m=m, ax=ax, uncertainty=uncertainty, yearly_start=yearly_start,
                 m=m, ax=ax, uncertainty=uncertainty, yearly_start=yearly_start,
             )
             )
-        elif plot_name == 'extra_regressors':
+        elif plot_name in [
+            'holidays',
+            'extra_regressors_additive',
+            'extra_regressors_multiplicative',
+        ]:
             plot_forecast_component(
             plot_forecast_component(
-                m=m, fcst=fcst, name='extra_regressors', ax=ax,
-                uncertainty=uncertainty, plot_cap=False,
+                m=m, fcst=fcst, name=plot_name, ax=ax, uncertainty=uncertainty,
+                plot_cap=False,
             )
             )
         else:
         else:
             plot_seasonality(
             plot_seasonality(
@@ -242,7 +246,7 @@ def plot_weekly(m, ax=None, uncertainty=True, weekly_start=0):
     ax.set_xticks(range(len(days)))
     ax.set_xticks(range(len(days)))
     ax.set_xticklabels(days)
     ax.set_xticklabels(days)
     ax.set_xlabel('Day of week')
     ax.set_xlabel('Day of week')
-    ax.set_ylabel('weekly')
+    ax.set_ylabel('weekly ({})'.format(m.seasonalities['weekly']['mode']))
     return artists
     return artists
 
 
 
 
@@ -284,7 +288,7 @@ def plot_yearly(m, ax=None, uncertainty=True, yearly_start=0):
         lambda x, pos=None: '{dt:%B} {dt.day}'.format(dt=num2date(x))))
         lambda x, pos=None: '{dt:%B} {dt.day}'.format(dt=num2date(x))))
     ax.xaxis.set_major_locator(months)
     ax.xaxis.set_major_locator(months)
     ax.set_xlabel('Day of year')
     ax.set_xlabel('Day of year')
-    ax.set_ylabel('yearly')
+    ax.set_ylabel('yearly ({})'.format(m.seasonalities['yearly']['mode']))
     return artists
     return artists
 
 
 
 
@@ -334,7 +338,7 @@ def plot_seasonality(m, name, ax=None, uncertainty=True):
     ax.xaxis.set_major_formatter(FuncFormatter(
     ax.xaxis.set_major_formatter(FuncFormatter(
         lambda x, pos=None: fmt_str.format(dt=num2date(x))))
         lambda x, pos=None: fmt_str.format(dt=num2date(x))))
     ax.set_xlabel('ds')
     ax.set_xlabel('ds')
-    ax.set_ylabel(name)
+    ax.set_ylabel('{} ({})'.format(name, m.seasonalities[name]['mode']))
     return artists
     return artists
 
 
 
 

+ 2 - 0
python/fbprophet/tests/test_diagnostics.py

@@ -184,6 +184,7 @@ class TestDiagnostics(TestCase):
             [True, False],  # weekly_seasonality
             [True, False],  # weekly_seasonality
             [True, False],  # daily_seasonality
             [True, False],  # daily_seasonality
             [None, holiday],  # holidays
             [None, holiday],  # holidays
+            ['additive', 'multiplicative'],  # seasonality_mode
             [1.1],  # seasonality_prior_scale
             [1.1],  # seasonality_prior_scale
             [1.1],  # holidays_prior_scale
             [1.1],  # holidays_prior_scale
             [0.1],  # changepoint_prior_scale
             [0.1],  # changepoint_prior_scale
@@ -214,6 +215,7 @@ class TestDiagnostics(TestCase):
                 self.assertEqual(m1.holidays, m2.holidays)
                 self.assertEqual(m1.holidays, m2.holidays)
             else:
             else:
                 self.assertTrue((m1.holidays == m2.holidays).values.all())
                 self.assertTrue((m1.holidays == m2.holidays).values.all())
+            self.assertEqual(m1.seasonality_mode, m2.seasonality_mode)
             self.assertEqual(m1.seasonality_prior_scale, m2.seasonality_prior_scale)
             self.assertEqual(m1.seasonality_prior_scale, m2.seasonality_prior_scale)
             self.assertEqual(m1.changepoint_prior_scale, m2.changepoint_prior_scale)
             self.assertEqual(m1.changepoint_prior_scale, m2.changepoint_prior_scale)
             self.assertEqual(m1.holidays_prior_scale, m2.holidays_prior_scale)
             self.assertEqual(m1.holidays_prior_scale, m2.holidays_prior_scale)

+ 152 - 27
python/fbprophet/tests/test_prophet.py

@@ -261,11 +261,12 @@ class TestProphet(TestCase):
         df = pd.DataFrame({
         df = pd.DataFrame({
             'ds': pd.date_range('2016-12-20', '2016-12-31')
             'ds': pd.date_range('2016-12-20', '2016-12-31')
         })
         })
-        feats, priors = model.make_holiday_features(df['ds'])
+        feats, priors, names = model.make_holiday_features(df['ds'])
         # 11 columns generated even though only 8 overlap
         # 11 columns generated even though only 8 overlap
         self.assertEqual(feats.shape, (df.shape[0], 2))
         self.assertEqual(feats.shape, (df.shape[0], 2))
         self.assertEqual((feats.sum(0) - np.array([1.0, 1.0])).sum(), 0)
         self.assertEqual((feats.sum(0) - np.array([1.0, 1.0])).sum(), 0)
         self.assertEqual(priors, [10., 10.])  # Default prior
         self.assertEqual(priors, [10., 10.])  # Default prior
+        self.assertEqual(names, ['xmas'])
 
 
         holidays = pd.DataFrame({
         holidays = pd.DataFrame({
             'ds': pd.to_datetime(['2016-12-25']),
             'ds': pd.to_datetime(['2016-12-25']),
@@ -273,10 +274,12 @@ class TestProphet(TestCase):
             'lower_window': [-1],
             'lower_window': [-1],
             'upper_window': [10],
             'upper_window': [10],
         })
         })
-        feats, priors = Prophet(holidays=holidays).make_holiday_features(df['ds'])
+        m = Prophet(holidays=holidays)
+        feats, priors, names = m.make_holiday_features(df['ds'])
         # 12 columns generated even though only 8 overlap
         # 12 columns generated even though only 8 overlap
         self.assertEqual(feats.shape, (df.shape[0], 12))
         self.assertEqual(feats.shape, (df.shape[0], 12))
         self.assertEqual(priors, list(10. * np.ones(12)))
         self.assertEqual(priors, list(10. * np.ones(12)))
+        self.assertEqual(names, ['xmas'])
         # Check prior specifications
         # Check prior specifications
         holidays = pd.DataFrame({
         holidays = pd.DataFrame({
             'ds': pd.to_datetime(['2016-12-25', '2017-12-25']),
             'ds': pd.to_datetime(['2016-12-25', '2017-12-25']),
@@ -285,8 +288,10 @@ class TestProphet(TestCase):
             'upper_window': [0, 0],
             'upper_window': [0, 0],
             'prior_scale': [5., 5.],
             'prior_scale': [5., 5.],
         })
         })
-        feats, priors = Prophet(holidays=holidays).make_holiday_features(df['ds'])
+        m = Prophet(holidays=holidays)
+        feats, priors, names = m.make_holiday_features(df['ds'])
         self.assertEqual(priors, [5., 5.])
         self.assertEqual(priors, [5., 5.])
+        self.assertEqual(names, ['xmas'])
         # 2 different priors
         # 2 different priors
         holidays2 = pd.DataFrame({
         holidays2 = pd.DataFrame({
             'ds': pd.to_datetime(['2012-06-06', '2013-06-06']),
             'ds': pd.to_datetime(['2012-06-06', '2013-06-06']),
@@ -296,8 +301,10 @@ class TestProphet(TestCase):
             'prior_scale': [8] * 2,
             'prior_scale': [8] * 2,
         })
         })
         holidays2 = pd.concat((holidays, holidays2))
         holidays2 = pd.concat((holidays, holidays2))
-        feats, priors = Prophet(holidays=holidays2).make_holiday_features(df['ds'])
+        m = Prophet(holidays=holidays2)
+        feats, priors, names = m.make_holiday_features(df['ds'])
         self.assertEqual(priors, [8., 8., 5., 5.])
         self.assertEqual(priors, [8., 8., 5., 5.])
+        self.assertEqual(set(names), {'xmas', 'seans-bday'})
         holidays2 = pd.DataFrame({
         holidays2 = pd.DataFrame({
             'ds': pd.to_datetime(['2012-06-06', '2013-06-06']),
             'ds': pd.to_datetime(['2012-06-06', '2013-06-06']),
             'holiday': ['seans-bday'] * 2,
             'holiday': ['seans-bday'] * 2,
@@ -305,7 +312,7 @@ class TestProphet(TestCase):
             'upper_window': [1] * 2,
             'upper_window': [1] * 2,
         })
         })
         holidays2 = pd.concat((holidays, holidays2))
         holidays2 = pd.concat((holidays, holidays2))
-        feats, priors = Prophet(
+        feats, priors, names = Prophet(
             holidays=holidays2, holidays_prior_scale=4
             holidays=holidays2, holidays_prior_scale=4
         ).make_holiday_features(df['ds'])
         ).make_holiday_features(df['ds'])
         self.assertEqual(priors, [4., 4., 5., 5.])
         self.assertEqual(priors, [4., 4., 5., 5.])
@@ -357,8 +364,15 @@ class TestProphet(TestCase):
         self.assertEqual(m.weekly_seasonality, 'auto')
         self.assertEqual(m.weekly_seasonality, 'auto')
         m.fit(train)
         m.fit(train)
         self.assertIn('weekly', m.seasonalities)
         self.assertIn('weekly', m.seasonalities)
-        self.assertEqual(m.seasonalities['weekly'],
-                         {'period': 7, 'fourier_order': 3, 'prior_scale': 10.})
+        self.assertEqual(
+            m.seasonalities['weekly'],
+            {
+                'period': 7,
+                'fourier_order': 3,
+                'prior_scale': 10.,
+                'mode': 'additive',
+            },
+        )
         # Should be disabled due to too short history
         # Should be disabled due to too short history
         N = 9
         N = 9
         train = DATA.head(N)
         train = DATA.head(N)
@@ -375,8 +389,15 @@ class TestProphet(TestCase):
         self.assertNotIn('weekly', m.seasonalities)
         self.assertNotIn('weekly', m.seasonalities)
         m = Prophet(weekly_seasonality=2, seasonality_prior_scale=3.)
         m = Prophet(weekly_seasonality=2, seasonality_prior_scale=3.)
         m.fit(DATA)
         m.fit(DATA)
-        self.assertEqual(m.seasonalities['weekly'],
-                         {'period': 7, 'fourier_order': 2, 'prior_scale': 3.})
+        self.assertEqual(
+            m.seasonalities['weekly'],
+            {
+                'period': 7,
+                'fourier_order': 2,
+                'prior_scale': 3.,
+                'mode': 'additive',
+            },
+        )
 
 
     def test_auto_yearly_seasonality(self):
     def test_auto_yearly_seasonality(self):
         # Should be enabled
         # Should be enabled
@@ -386,7 +407,12 @@ class TestProphet(TestCase):
         self.assertIn('yearly', m.seasonalities)
         self.assertIn('yearly', m.seasonalities)
         self.assertEqual(
         self.assertEqual(
             m.seasonalities['yearly'],
             m.seasonalities['yearly'],
-            {'period': 365.25, 'fourier_order': 10, 'prior_scale': 10.},
+            {
+                'period': 365.25,
+                'fourier_order': 10,
+                'prior_scale': 10.,
+                'mode': 'additive',
+            },
         )
         )
         # Should be disabled due to too short history
         # Should be disabled due to too short history
         N = 240
         N = 240
@@ -401,7 +427,12 @@ class TestProphet(TestCase):
         m.fit(DATA)
         m.fit(DATA)
         self.assertEqual(
         self.assertEqual(
             m.seasonalities['yearly'],
             m.seasonalities['yearly'],
-            {'period': 365.25, 'fourier_order': 7, 'prior_scale': 3.},
+            {
+                'period': 365.25,
+                'fourier_order': 7,
+                'prior_scale': 3.,
+                'mode': 'additive',
+            },
         )
         )
 
 
     def test_auto_daily_seasonality(self):
     def test_auto_daily_seasonality(self):
@@ -410,8 +441,15 @@ class TestProphet(TestCase):
         self.assertEqual(m.daily_seasonality, 'auto')
         self.assertEqual(m.daily_seasonality, 'auto')
         m.fit(DATA2)
         m.fit(DATA2)
         self.assertIn('daily', m.seasonalities)
         self.assertIn('daily', m.seasonalities)
-        self.assertEqual(m.seasonalities['daily'],
-                         {'period': 1, 'fourier_order': 4, 'prior_scale': 10.})
+        self.assertEqual(
+            m.seasonalities['daily'],
+            {
+                'period': 1,
+                'fourier_order': 4,
+                'prior_scale': 10.,
+                'mode': 'additive',
+            },
+        )
         # Should be disabled due to too short history
         # Should be disabled due to too short history
         N = 430
         N = 430
         train = DATA2.head(N)
         train = DATA2.head(N)
@@ -423,8 +461,15 @@ class TestProphet(TestCase):
         self.assertIn('daily', m.seasonalities)
         self.assertIn('daily', m.seasonalities)
         m = Prophet(daily_seasonality=7, seasonality_prior_scale=3.)
         m = Prophet(daily_seasonality=7, seasonality_prior_scale=3.)
         m.fit(DATA2)
         m.fit(DATA2)
-        self.assertEqual(m.seasonalities['daily'],
-                         {'period': 1, 'fourier_order': 7, 'prior_scale': 3.})
+        self.assertEqual(
+            m.seasonalities['daily'],
+            {
+                'period': 1,
+                'fourier_order': 7,
+                'prior_scale': 3.,
+                'mode': 'additive',
+            },
+        )
         m = Prophet()
         m = Prophet()
         m.fit(DATA)
         m.fit(DATA)
         self.assertNotIn('daily', m.seasonalities)
         self.assertNotIn('daily', m.seasonalities)
@@ -448,24 +493,38 @@ class TestProphet(TestCase):
         m = Prophet(holidays=holidays)
         m = Prophet(holidays=holidays)
         m.add_seasonality(name='monthly', period=30, fourier_order=5,
         m.add_seasonality(name='monthly', period=30, fourier_order=5,
                           prior_scale=2.)
                           prior_scale=2.)
-        self.assertEqual(m.seasonalities['monthly'],
-                         {'period': 30, 'fourier_order': 5, 'prior_scale': 2.})
+        self.assertEqual(
+            m.seasonalities['monthly'],
+            {
+                'period': 30,
+                'fourier_order': 5,
+                'prior_scale': 2.,
+                'mode': 'additive',
+            },
+        )
         with self.assertRaises(ValueError):
         with self.assertRaises(ValueError):
             m.add_seasonality(name='special_day', period=30, fourier_order=5)
             m.add_seasonality(name='special_day', period=30, fourier_order=5)
         with self.assertRaises(ValueError):
         with self.assertRaises(ValueError):
             m.add_seasonality(name='trend', period=30, fourier_order=5)
             m.add_seasonality(name='trend', period=30, fourier_order=5)
         m.add_seasonality(name='weekly', period=30, fourier_order=5)
         m.add_seasonality(name='weekly', period=30, fourier_order=5)
         # Test priors
         # Test priors
-        m = Prophet(holidays=holidays, yearly_seasonality=False)
+        m = Prophet(
+            holidays=holidays, yearly_seasonality=False,
+            seasonality_mode='multiplicative',
+        )
         m.add_seasonality(name='monthly', period=30, fourier_order=5,
         m.add_seasonality(name='monthly', period=30, fourier_order=5,
-                          prior_scale=2.)
+                          prior_scale=2., mode='additive')
         m.fit(DATA.copy())
         m.fit(DATA.copy())
-        seasonal_features, prior_scales, component_cols = (
+        self.assertEqual(m.seasonalities['monthly']['mode'], 'additive')
+        self.assertEqual(m.seasonalities['weekly']['mode'], 'multiplicative')
+        seasonal_features, prior_scales, component_cols, modes = (
             m.make_all_seasonality_features(m.history)
             m.make_all_seasonality_features(m.history)
         )
         )
         self.assertEqual(sum(component_cols['monthly']), 10)
         self.assertEqual(sum(component_cols['monthly']), 10)
         self.assertEqual(sum(component_cols['special_day']), 1)
         self.assertEqual(sum(component_cols['special_day']), 1)
         self.assertEqual(sum(component_cols['weekly']), 6)
         self.assertEqual(sum(component_cols['weekly']), 6)
+        self.assertEqual(sum(component_cols['additive_terms']), 10)
+        self.assertEqual(sum(component_cols['multiplicative_terms']), 7)
         if seasonal_features.columns[0] == 'monthly_delim_1':
         if seasonal_features.columns[0] == 'monthly_delim_1':
             true = [2.] * 10 + [10.] * 6 + [4.]
             true = [2.] * 10 + [10.] * 6 + [4.]
             self.assertEqual(sum(component_cols['monthly'][:10]), 10)
             self.assertEqual(sum(component_cols['monthly'][:10]), 10)
@@ -480,10 +539,14 @@ class TestProphet(TestCase):
         m = Prophet()
         m = Prophet()
         m.add_regressor('binary_feature', prior_scale=0.2)
         m.add_regressor('binary_feature', prior_scale=0.2)
         m.add_regressor('numeric_feature', prior_scale=0.5)
         m.add_regressor('numeric_feature', prior_scale=0.5)
+        m.add_regressor(
+            'numeric_feature2', prior_scale=0.5, mode='multiplicative'
+        )
         m.add_regressor('binary_feature2', standardize=True)
         m.add_regressor('binary_feature2', standardize=True)
         df = DATA.copy()
         df = DATA.copy()
         df['binary_feature'] = [0] * 255 + [1] * 255
         df['binary_feature'] = [0] * 255 + [1] * 255
         df['numeric_feature'] = range(510)
         df['numeric_feature'] = range(510)
+        df['numeric_feature2'] = range(510)
         with self.assertRaises(ValueError):
         with self.assertRaises(ValueError):
             # Require all regressors in df
             # Require all regressors in df
             m.fit(df)
             m.fit(df)
@@ -492,7 +555,13 @@ class TestProphet(TestCase):
         # Check that standardizations are correctly set
         # Check that standardizations are correctly set
         self.assertEqual(
         self.assertEqual(
             m.extra_regressors['binary_feature'],
             m.extra_regressors['binary_feature'],
-            {'prior_scale': 0.2, 'mu': 0, 'std': 1, 'standardize': 'auto'},
+            {
+                'prior_scale': 0.2,
+                'mu': 0,
+                'std': 1,
+                'standardize': 'auto',
+                'mode': 'additive',
+            },
         )
         )
         self.assertEqual(
         self.assertEqual(
             m.extra_regressors['numeric_feature']['prior_scale'], 0.5)
             m.extra_regressors['numeric_feature']['prior_scale'], 0.5)
@@ -501,6 +570,8 @@ class TestProphet(TestCase):
         self.assertAlmostEqual(
         self.assertAlmostEqual(
             m.extra_regressors['numeric_feature']['std'], 147.368585, places=5)
             m.extra_regressors['numeric_feature']['std'], 147.368585, places=5)
         self.assertEqual(
         self.assertEqual(
+            m.extra_regressors['numeric_feature2']['mode'], 'multiplicative')
+        self.assertEqual(
             m.extra_regressors['binary_feature2']['prior_scale'], 10.)
             m.extra_regressors['binary_feature2']['prior_scale'], 10.)
         self.assertAlmostEqual(
         self.assertAlmostEqual(
             m.extra_regressors['binary_feature2']['mu'], 0.1960784, places=5)
             m.extra_regressors['binary_feature2']['mu'], 0.1960784, places=5)
@@ -512,10 +583,10 @@ class TestProphet(TestCase):
         self.assertAlmostEqual(df2['numeric_feature'][0], -1.726962, places=4)
         self.assertAlmostEqual(df2['numeric_feature'][0], -1.726962, places=4)
         self.assertAlmostEqual(df2['binary_feature2'][0], 2.022859, places=4)
         self.assertAlmostEqual(df2['binary_feature2'][0], 2.022859, places=4)
         # Check that feature matrix and prior scales are correctly constructed
         # Check that feature matrix and prior scales are correctly constructed
-        seasonal_features, prior_scales, component_cols = (
+        seasonal_features, prior_scales, component_cols, modes = (
             m.make_all_seasonality_features(df2)
             m.make_all_seasonality_features(df2)
         )
         )
-        self.assertEqual(seasonal_features.shape[1], 29)
+        self.assertEqual(seasonal_features.shape[1], 30)
         names = ['binary_feature', 'numeric_feature', 'binary_feature2']
         names = ['binary_feature', 'numeric_feature', 'binary_feature2']
         true_priors = [0.2, 0.5, 10.]
         true_priors = [0.2, 0.5, 10.]
         for i, name in enumerate(names):
         for i, name in enumerate(names):
@@ -530,24 +601,35 @@ class TestProphet(TestCase):
             'ds': ['2014-06-01'],
             'ds': ['2014-06-01'],
             'binary_feature': [0],
             'binary_feature': [0],
             'numeric_feature': [10],
             'numeric_feature': [10],
+            'numeric_feature2': [10],
         })
         })
         with self.assertRaises(ValueError):
         with self.assertRaises(ValueError):
             m.predict(future)
             m.predict(future)
         future['binary_feature2'] = 0
         future['binary_feature2'] = 0
         fcst = m.predict(future)
         fcst = m.predict(future)
-        self.assertEqual(fcst.shape[1], 28)
+        self.assertEqual(fcst.shape[1], 37)
         self.assertEqual(fcst['binary_feature'][0], 0)
         self.assertEqual(fcst['binary_feature'][0], 0)
         self.assertAlmostEqual(
         self.assertAlmostEqual(
-            fcst['extra_regressors'][0],
+            fcst['extra_regressors_additive'][0],
             fcst['numeric_feature'][0] + fcst['binary_feature2'][0],
             fcst['numeric_feature'][0] + fcst['binary_feature2'][0],
         )
         )
         self.assertAlmostEqual(
         self.assertAlmostEqual(
+            fcst['extra_regressors_multiplicative'][0],
+            fcst['numeric_feature2'][0],
+        )
+        self.assertAlmostEqual(
             fcst['additive_terms'][0],
             fcst['additive_terms'][0],
-            fcst['yearly'][0] + fcst['weekly'][0] + fcst['extra_regressors'][0]
+            fcst['yearly'][0] + fcst['weekly'][0]
+                + fcst['extra_regressors_additive'][0],
+        )
+        self.assertAlmostEqual(
+            fcst['multiplicative_terms'][0],
+            fcst['extra_regressors_multiplicative'][0],
         )
         )
         self.assertAlmostEqual(
         self.assertAlmostEqual(
             fcst['yhat'][0],
             fcst['yhat'][0],
-            fcst['trend'][0] + fcst['additive_terms'][0],
+            fcst['trend'][0] * (1 + fcst['multiplicative_terms'][0])
+                + fcst['additive_terms'][0],
         )
         )
         # Check fails if constant extra regressor
         # Check fails if constant extra regressor
         df['constant_feature'] = 5
         df['constant_feature'] = 5
@@ -555,3 +637,46 @@ class TestProphet(TestCase):
         m.add_regressor('constant_feature')
         m.add_regressor('constant_feature')
         with self.assertRaises(ValueError):
         with self.assertRaises(ValueError):
             m.fit(df.copy())
             m.fit(df.copy())
+
+    def test_set_seasonality_mode(self):
+        # Setting attribute
+        m = Prophet()
+        self.assertEqual(m.seasonality_mode, 'additive')
+        m = Prophet(seasonality_mode='multiplicative')
+        self.assertEqual(m.seasonality_mode, 'multiplicative')
+        with self.assertRaises(ValueError):
+            Prophet(seasonality_mode='batman')
+
+    def test_seasonality_modes(self):
+        # Model with holidays, seasonalities, and extra regressors
+        holidays = pd.DataFrame({
+            'ds': pd.to_datetime(['2016-12-25']),
+            'holiday': ['xmas'],
+            'lower_window': [-1],
+            'upper_window': [0],
+        })
+        m = Prophet(seasonality_mode='multiplicative', holidays=holidays)
+        m.add_seasonality('monthly', period=30, mode='additive', fourier_order=3)
+        m.add_regressor('binary_feature', mode='additive')
+        m.add_regressor('numeric_feature')
+        # Construct seasonal features
+        df = DATA.copy()
+        df['binary_feature'] = [0] * 255 + [1] * 255
+        df['numeric_feature'] = range(510)
+        df = m.setup_dataframe(df, initialize_scales=True)
+        m.history = df.copy()
+        m.set_auto_seasonalities()
+        seasonal_features, prior_scales, component_cols, modes = (
+            m.make_all_seasonality_features(df))
+        self.assertEqual(sum(component_cols['additive_terms']), 7)
+        self.assertEqual(sum(component_cols['multiplicative_terms']), 29)
+        self.assertEqual(
+            set(modes['additive']),
+            {'monthly', 'binary_feature', 'additive_terms',
+             'extra_regressors_additive'},
+        )
+        self.assertEqual(
+            set(modes['multiplicative']),
+            {'weekly', 'yearly', 'xmas', 'numeric_feature',
+             'multiplicative_terms', 'extra_regressors_multiplicative'},
+        )

+ 8 - 1
python/stan/unix/prophet.stan

@@ -85,6 +85,8 @@ data {
   vector[K] sigmas;     // Scale on seasonality prior
   vector[K] sigmas;     // Scale on seasonality prior
   real<lower=0> tau;    // Scale on changepoints prior
   real<lower=0> tau;    // Scale on changepoints prior
   int trend_indicator;  // 0 for linear, 1 for logistic
   int trend_indicator;  // 0 for linear, 1 for logistic
+  vector[K] s_a;        // Indicator of additive features
+  vector[K] s_m;        // Indicator of multiplicative features
 }
 }
 
 
 transformed data {
 transformed data {
@@ -102,12 +104,17 @@ parameters {
 
 
 transformed parameters {
 transformed parameters {
   vector[T] trend;
   vector[T] trend;
+  vector[T] Xb_a;
+  vector[T] Xb_m;
 
 
   if (trend_indicator == 0) {
   if (trend_indicator == 0) {
     trend = linear_trend(k, m, delta, t, A, t_change);
     trend = linear_trend(k, m, delta, t, A, t_change);
   } else if (trend_indicator == 1) {
   } else if (trend_indicator == 1) {
     trend = logistic_trend(k, m, delta, t, cap, A, t_change, S);
     trend = logistic_trend(k, m, delta, t, cap, A, t_change, S);
   }
   }
+
+  Xb_a = X * (beta .* s_a);
+  Xb_m = X * (beta .* s_m);
 }
 }
 
 
 model {
 model {
@@ -119,5 +126,5 @@ model {
   beta ~ normal(0, sigmas);
   beta ~ normal(0, sigmas);
 
 
   // Likelihood
   // Likelihood
-  y ~ normal(trend + X * beta, sigma_obs);
+  y ~ normal(trend .* (1 + Xb_m) + Xb_a, sigma_obs);
 }
 }

+ 9 - 3
python/stan/win/prophet.stan

@@ -59,7 +59,7 @@ functions {
 
 
     gamma = logistic_gamma(k, m, delta, t_change, S);
     gamma = logistic_gamma(k, m, delta, t_change, S);
     for (i in 1:T) {
     for (i in 1:T) {
-      Y[i] = cap[i] / (1 + exp(-(k + dot_product(A[i], delta)) * (t[i] - (m + dot_product(A[i], gamma)))))
+      Y[i] = cap[i] / (1 + exp(-(k + dot_product(A[i], delta)) * (t[i] - (m + dot_product(A[i], gamma)))));
     }
     }
     return Y;
     return Y;
   }
   }
@@ -81,7 +81,7 @@ functions {
     
     
     gamma = (-t_change .* delta);
     gamma = (-t_change .* delta);
     for (i in 1:T) {
     for (i in 1:T) {
-      Y[i] = (k + dot_product(A[i], delta)) * t[i] + (m + dot_product(A[i], gamma))
+      Y[i] = (k + dot_product(A[i], delta)) * t[i] + (m + dot_product(A[i], gamma));
     }
     }
     return Y;
     return Y;
   }
   }
@@ -99,6 +99,8 @@ data {
   vector[K] sigmas;     // Scale on seasonality prior
   vector[K] sigmas;     // Scale on seasonality prior
   real<lower=0> tau;    // Scale on changepoints prior
   real<lower=0> tau;    // Scale on changepoints prior
   int trend_indicator;  // 0 for linear, 1 for logistic
   int trend_indicator;  // 0 for linear, 1 for logistic
+  vector[K] s_a;        // Indicator of additive features
+  vector[K] s_m;        // Indicator of multiplicative features
 }
 }
 
 
 transformed data {
 transformed data {
@@ -117,6 +119,8 @@ parameters {
 transformed parameters {
 transformed parameters {
   vector[T] trend;
   vector[T] trend;
   vector[T] Y;
   vector[T] Y;
+  vector[T] Xb_a;
+  vector[T] Xb_m;
 
 
   if (trend_indicator == 0) {
   if (trend_indicator == 0) {
     trend = linear_trend(k, m, delta, t, A, t_change, S, T);
     trend = linear_trend(k, m, delta, t, A, t_change, S, T);
@@ -125,7 +129,9 @@ transformed parameters {
   }
   }
 
 
   for (i in 1:T) {
   for (i in 1:T) {
-    Y[i] = trend[i] + dot_product(X[i], beta);
+    Xb_a[i] = dot_product(X[i], beta .* s_a);
+    Xb_m[i] = dot_product(X[i], beta .* s_m);
+    Y[i] = trend[i] * (1 + Xb_m[i]) + Xb_a[i];
   }
   }
 }
 }