Multiplicative seasonality (Py)

This commit is contained in:
Ben Letham 2018-05-09 12:25:29 -07:00
parent b9923fd9fd
commit 8d8c5b41ce
7 changed files with 367 additions and 118 deletions

View file

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

View file

@ -70,6 +70,7 @@ class Prophet(object):
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
that holiday.
seasonality_mode: 'additive' (default) or 'multiplicative'.
seasonality_prior_scale: Parameter modulating the strength of the
seasonality model. Larger values allow the model to fit larger seasonal
fluctuations, smaller values dampen the seasonality. Can be specified
@ -100,6 +101,7 @@ class Prophet(object):
weekly_seasonality='auto',
daily_seasonality='auto',
holidays=None,
seasonality_mode='additive',
seasonality_prior_scale=10.0,
holidays_prior_scale=10.0,
changepoint_prior_scale=0.05,
@ -132,6 +134,7 @@ class Prophet(object):
holidays['ds'] = pd.to_datetime(holidays['ds'])
self.holidays = holidays
self.seasonality_mode = seasonality_mode
self.seasonality_prior_scale = float(seasonality_prior_scale)
self.changepoint_prior_scale = float(changepoint_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')
for h in self.holidays['holiday'].unique():
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,
check_seasonalities=True, check_regressors=True):
@ -189,7 +196,8 @@ class Prophet(object):
raise ValueError('Name cannot contain "_delim_"')
reserved_names = [
'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_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.
prior_scale_list: List of prior scales for each holiday column.
holiday_names: List of names of holidays
"""
# Holds columns of our future matrix.
expanded_holidays = defaultdict(lambda: np.zeros(dates.shape[0]))
@ -461,9 +470,11 @@ class Prophet(object):
prior_scales[h.split('_delim_')[0]]
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.
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.
Decreasing the prior scale will add additional regularization. If no
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
----------
@ -481,6 +496,8 @@ class Prophet(object):
standardize: optional, specify whether this regressor will be
standardized prior to fitting. Can be 'auto' (standardize if not
binary), True, or False.
mode: optional, 'additive' or 'multiplicative'. Defaults to
self.seasonality_mode.
Returns
-------
@ -492,16 +509,23 @@ class Prophet(object):
self.validate_column_name(name, check_regressors=False)
if prior_scale is None:
prior_scale = float(self.holidays_prior_scale)
if mode is None:
mode = self.seasonality_mode
assert prior_scale > 0
if mode not in ['additive', 'multiplicative']:
raise ValueError("mode must be 'additive' or 'multiplicative'")
self.extra_regressors[name] = {
'prior_scale': prior_scale,
'standardize': standardize,
'mu': 0.,
'std': 1.,
'mode': mode,
}
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
components, and prior scale.
@ -514,12 +538,18 @@ class Prophet(object):
seasonality_prior_scale provided on Prophet initialization (defaults
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
----------
name: string name of the seasonality component.
period: float number of days in one period.
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
-------
@ -537,10 +567,15 @@ class Prophet(object):
ps = float(prior_scale)
if ps <= 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] = {
'period': period,
'fourier_order': fourier_order,
'prior_scale': ps,
'mode': mode,
}
return self
@ -560,9 +595,12 @@ class Prophet(object):
list of prior scales for each column of the features dataframe.
Dataframe with indicators for which regression components correspond to
which columns.
Dictionary with keys 'additive' and 'multiplicative' listing the
component names for each mode of seasonality.
"""
seasonal_features = []
prior_scales = []
modes = {'additive': [], 'multiplicative': []}
# Seasonality features
for name, props in self.seasonalities.items():
@ -575,26 +613,120 @@ class Prophet(object):
seasonal_features.append(features)
prior_scales.extend(
[props['prior_scale']] * features.shape[1])
modes[props['mode']].append(name)
# Holiday features
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)
prior_scales.extend(holiday_priors)
modes[self.seasonality_mode].extend(holiday_names)
# Additional regressors
for name, props in self.extra_regressors.items():
seasonal_features.append(pd.DataFrame(df[name]))
prior_scales.append(props['prior_scale'])
modes[props['mode']].append(name)
# Dummy to prevent empty X
if len(seasonal_features) == 0:
seasonal_features.append(
pd.DataFrame({'zeros': np.zeros(df.shape[0])}))
prior_scales.append(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):
"""Get number of fourier components for built-in seasonalities.
@ -656,6 +788,7 @@ class Prophet(object):
'period': 365.25,
'fourier_order': fourier_order,
'prior_scale': self.seasonality_prior_scale,
'mode': self.seasonality_mode,
}
# Weekly seasonality
@ -668,6 +801,7 @@ class Prophet(object):
'period': 7,
'fourier_order': fourier_order,
'prior_scale': self.seasonality_prior_scale,
'mode': self.seasonality_mode,
}
# Daily seasonality
@ -680,6 +814,7 @@ class Prophet(object):
'period': 1,
'fourier_order': fourier_order,
'prior_scale': self.seasonality_prior_scale,
'mode': self.seasonality_mode,
}
@staticmethod
@ -785,7 +920,7 @@ class Prophet(object):
history = self.setup_dataframe(history, initialize_scales=True)
self.history = history
self.set_auto_seasonalities()
seasonal_features, prior_scales, component_cols = (
seasonal_features, prior_scales, component_cols, _ = (
self.make_all_seasonality_features(history))
self.train_component_cols = component_cols
@ -802,6 +937,8 @@ class Prophet(object):
'sigmas': prior_scales,
'tau': self.changepoint_prior_scale,
'trend_indicator': int(self.growth == 'logistic'),
's_a': component_cols['additive_terms'],
's_m': component_cols['multiplicative_terms'],
}
if self.growth == 'linear':
@ -891,7 +1028,10 @@ class Prophet(object):
cols.append('floor')
# Add in forecast components
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
@staticmethod
@ -991,7 +1131,7 @@ class Prophet(object):
-------
Dataframe with seasonal components.
"""
seasonal_features, _, component_cols = (
seasonal_features, _, component_cols, modes = (
self.make_all_seasonality_features(df)
)
lower_p = 100 * (1.0 - self.interval_width) / 2
@ -1002,7 +1142,9 @@ class Prophet(object):
for component in component_cols.columns:
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 + '_lower'] = np.nanpercentile(
comp, lower_p, axis=1,
@ -1012,54 +1154,6 @@ class Prophet(object):
)
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):
"""Prophet posterior predictive samples.
@ -1069,7 +1163,8 @@ class Prophet(object):
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]
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.
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 _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:
sim_values[key].append(sim[key])
for k, v in sim_values.items():
@ -1099,9 +1202,8 @@ class Prophet(object):
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())
sim_values = self.sample_posterior_predictive(df)
@ -1132,7 +1234,7 @@ class Prophet(object):
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.
Parameters
@ -1143,20 +1245,22 @@ class Prophet(object):
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)
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]
noise = np.random.normal(0, sigma, df.shape[0]) * self.y_scale
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):

View file

@ -77,7 +77,8 @@ def plot_components(
"""Plot the Prophet forecast components.
Will plot whichever are available of: trend, holidays, weekly
seasonality, and yearly seasonality.
seasonality, yearly seasonality, and additive and multiplicative extra
regressors.
Parameters
----------
@ -103,8 +104,12 @@ def plot_components(
components.append('holidays')
components.extend([name for name in m.seasonalities
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)
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,
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':
plot_weekly(
m=m, ax=ax, uncertainty=uncertainty, weekly_start=weekly_start,
@ -132,10 +132,14 @@ def plot_components(
plot_yearly(
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(
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:
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_xticklabels(days)
ax.set_xlabel('Day of week')
ax.set_ylabel('weekly')
ax.set_ylabel('weekly ({})'.format(m.seasonalities['weekly']['mode']))
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))))
ax.xaxis.set_major_locator(months)
ax.set_xlabel('Day of year')
ax.set_ylabel('yearly')
ax.set_ylabel('yearly ({})'.format(m.seasonalities['yearly']['mode']))
return artists
@ -334,7 +338,7 @@ def plot_seasonality(m, name, ax=None, uncertainty=True):
ax.xaxis.set_major_formatter(FuncFormatter(
lambda x, pos=None: fmt_str.format(dt=num2date(x))))
ax.set_xlabel('ds')
ax.set_ylabel(name)
ax.set_ylabel('{} ({})'.format(name, m.seasonalities[name]['mode']))
return artists

View file

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

View file

@ -261,11 +261,12 @@ class TestProphet(TestCase):
df = pd.DataFrame({
'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
self.assertEqual(feats.shape, (df.shape[0], 2))
self.assertEqual((feats.sum(0) - np.array([1.0, 1.0])).sum(), 0)
self.assertEqual(priors, [10., 10.]) # Default prior
self.assertEqual(names, ['xmas'])
holidays = pd.DataFrame({
'ds': pd.to_datetime(['2016-12-25']),
@ -273,10 +274,12 @@ class TestProphet(TestCase):
'lower_window': [-1],
'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
self.assertEqual(feats.shape, (df.shape[0], 12))
self.assertEqual(priors, list(10. * np.ones(12)))
self.assertEqual(names, ['xmas'])
# Check prior specifications
holidays = pd.DataFrame({
'ds': pd.to_datetime(['2016-12-25', '2017-12-25']),
@ -285,8 +288,10 @@ class TestProphet(TestCase):
'upper_window': [0, 0],
'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(names, ['xmas'])
# 2 different priors
holidays2 = pd.DataFrame({
'ds': pd.to_datetime(['2012-06-06', '2013-06-06']),
@ -296,8 +301,10 @@ class TestProphet(TestCase):
'prior_scale': [8] * 2,
})
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(set(names), {'xmas', 'seans-bday'})
holidays2 = pd.DataFrame({
'ds': pd.to_datetime(['2012-06-06', '2013-06-06']),
'holiday': ['seans-bday'] * 2,
@ -305,7 +312,7 @@ class TestProphet(TestCase):
'upper_window': [1] * 2,
})
holidays2 = pd.concat((holidays, holidays2))
feats, priors = Prophet(
feats, priors, names = Prophet(
holidays=holidays2, holidays_prior_scale=4
).make_holiday_features(df['ds'])
self.assertEqual(priors, [4., 4., 5., 5.])
@ -357,8 +364,15 @@ class TestProphet(TestCase):
self.assertEqual(m.weekly_seasonality, 'auto')
m.fit(train)
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
N = 9
train = DATA.head(N)
@ -375,8 +389,15 @@ class TestProphet(TestCase):
self.assertNotIn('weekly', m.seasonalities)
m = Prophet(weekly_seasonality=2, seasonality_prior_scale=3.)
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):
# Should be enabled
@ -386,7 +407,12 @@ class TestProphet(TestCase):
self.assertIn('yearly', m.seasonalities)
self.assertEqual(
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
N = 240
@ -401,7 +427,12 @@ class TestProphet(TestCase):
m.fit(DATA)
self.assertEqual(
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):
@ -410,8 +441,15 @@ class TestProphet(TestCase):
self.assertEqual(m.daily_seasonality, 'auto')
m.fit(DATA2)
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
N = 430
train = DATA2.head(N)
@ -423,8 +461,15 @@ class TestProphet(TestCase):
self.assertIn('daily', m.seasonalities)
m = Prophet(daily_seasonality=7, seasonality_prior_scale=3.)
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.fit(DATA)
self.assertNotIn('daily', m.seasonalities)
@ -448,24 +493,38 @@ class TestProphet(TestCase):
m = Prophet(holidays=holidays)
m.add_seasonality(name='monthly', period=30, fourier_order=5,
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):
m.add_seasonality(name='special_day', period=30, fourier_order=5)
with self.assertRaises(ValueError):
m.add_seasonality(name='trend', period=30, fourier_order=5)
m.add_seasonality(name='weekly', period=30, fourier_order=5)
# 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,
prior_scale=2.)
prior_scale=2., mode='additive')
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)
)
self.assertEqual(sum(component_cols['monthly']), 10)
self.assertEqual(sum(component_cols['special_day']), 1)
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':
true = [2.] * 10 + [10.] * 6 + [4.]
self.assertEqual(sum(component_cols['monthly'][:10]), 10)
@ -480,10 +539,14 @@ class TestProphet(TestCase):
m = Prophet()
m.add_regressor('binary_feature', prior_scale=0.2)
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)
df = DATA.copy()
df['binary_feature'] = [0] * 255 + [1] * 255
df['numeric_feature'] = range(510)
df['numeric_feature2'] = range(510)
with self.assertRaises(ValueError):
# Require all regressors in df
m.fit(df)
@ -492,7 +555,13 @@ class TestProphet(TestCase):
# Check that standardizations are correctly set
self.assertEqual(
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(
m.extra_regressors['numeric_feature']['prior_scale'], 0.5)
@ -500,6 +569,8 @@ class TestProphet(TestCase):
m.extra_regressors['numeric_feature']['mu'], 254.5)
self.assertAlmostEqual(
m.extra_regressors['numeric_feature']['std'], 147.368585, places=5)
self.assertEqual(
m.extra_regressors['numeric_feature2']['mode'], 'multiplicative')
self.assertEqual(
m.extra_regressors['binary_feature2']['prior_scale'], 10.)
self.assertAlmostEqual(
@ -512,10 +583,10 @@ class TestProphet(TestCase):
self.assertAlmostEqual(df2['numeric_feature'][0], -1.726962, places=4)
self.assertAlmostEqual(df2['binary_feature2'][0], 2.022859, places=4)
# 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)
)
self.assertEqual(seasonal_features.shape[1], 29)
self.assertEqual(seasonal_features.shape[1], 30)
names = ['binary_feature', 'numeric_feature', 'binary_feature2']
true_priors = [0.2, 0.5, 10.]
for i, name in enumerate(names):
@ -530,24 +601,35 @@ class TestProphet(TestCase):
'ds': ['2014-06-01'],
'binary_feature': [0],
'numeric_feature': [10],
'numeric_feature2': [10],
})
with self.assertRaises(ValueError):
m.predict(future)
future['binary_feature2'] = 0
fcst = m.predict(future)
self.assertEqual(fcst.shape[1], 28)
self.assertEqual(fcst.shape[1], 37)
self.assertEqual(fcst['binary_feature'][0], 0)
self.assertAlmostEqual(
fcst['extra_regressors'][0],
fcst['extra_regressors_additive'][0],
fcst['numeric_feature'][0] + fcst['binary_feature2'][0],
)
self.assertAlmostEqual(
fcst['extra_regressors_multiplicative'][0],
fcst['numeric_feature2'][0],
)
self.assertAlmostEqual(
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(
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
df['constant_feature'] = 5
@ -555,3 +637,46 @@ class TestProphet(TestCase):
m.add_regressor('constant_feature')
with self.assertRaises(ValueError):
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'},
)

View file

@ -85,6 +85,8 @@ data {
vector[K] sigmas; // Scale on seasonality prior
real<lower=0> tau; // Scale on changepoints prior
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 {
@ -102,12 +104,17 @@ parameters {
transformed parameters {
vector[T] trend;
vector[T] Xb_a;
vector[T] Xb_m;
if (trend_indicator == 0) {
trend = linear_trend(k, m, delta, t, A, t_change);
} else if (trend_indicator == 1) {
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 {
@ -119,5 +126,5 @@ model {
beta ~ normal(0, sigmas);
// Likelihood
y ~ normal(trend + X * beta, sigma_obs);
y ~ normal(trend .* (1 + Xb_m) + Xb_a, sigma_obs);
}

View file

@ -59,7 +59,7 @@ functions {
gamma = logistic_gamma(k, m, delta, t_change, S);
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;
}
@ -81,7 +81,7 @@ functions {
gamma = (-t_change .* delta);
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;
}
@ -99,6 +99,8 @@ data {
vector[K] sigmas; // Scale on seasonality prior
real<lower=0> tau; // Scale on changepoints prior
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 {
@ -117,6 +119,8 @@ parameters {
transformed parameters {
vector[T] trend;
vector[T] Y;
vector[T] Xb_a;
vector[T] Xb_m;
if (trend_indicator == 0) {
trend = linear_trend(k, m, delta, t, A, t_change, S, T);
@ -125,7 +129,9 @@ transformed parameters {
}
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];
}
}