Style modifications to fbprophet

This commit is contained in:
baogorek 2019-11-19 11:26:12 -05:00 committed by Ben Letham
parent 83f66dadfd
commit 3a6a338ac2
3 changed files with 86 additions and 88 deletions

View file

@ -144,17 +144,17 @@ class Prophet(object):
"""Validates the inputs to Prophet."""
if self.growth not in ('linear', 'logistic'):
raise ValueError(
"Parameter 'growth' should be 'linear' or 'logistic'.")
'Parameter "growth" should be "linear" or "logistic".')
if ((self.changepoint_range < 0) or (self.changepoint_range > 1)):
raise ValueError("Parameter 'changepoint_range' must be in [0, 1]")
raise ValueError('Parameter "changepoint_range" must be in [0, 1]')
if self.holidays is not None:
if not (
isinstance(self.holidays, pd.DataFrame)
and 'ds' in self.holidays # noqa W503
and 'holiday' in self.holidays # noqa W503
):
raise ValueError("holidays must be a DataFrame with 'ds' and "
"'holiday' columns.")
raise ValueError('holidays must be a DataFrame with "ds" and '
'"holiday" columns.')
self.holidays['ds'] = pd.to_datetime(self.holidays['ds'])
has_lower = 'lower_window' in self.holidays
has_upper = 'upper_window' in self.holidays
@ -170,7 +170,7 @@ class Prophet(object):
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'"
'seasonality_mode must be "additive" or "multiplicative"'
)
def validate_column_name(self, name, check_holidays=True,
@ -198,21 +198,21 @@ class Prophet(object):
reserved_names.extend([
'ds', 'y', 'cap', 'floor', 'y_scaled', 'cap_scaled'])
if name in reserved_names:
raise ValueError('Name "{}" is reserved.'.format(name))
raise ValueError(f'Name "{name}" is reserved.')
if (check_holidays and self.holidays is not None and
name in self.holidays['holiday'].unique()):
raise ValueError(
'Name "{}" already used for a holiday.'.format(name))
f'Name "{name}" already used for a holiday.')
if (check_holidays and self.country_holidays is not None and
name in get_holiday_names(self.country_holidays)):
raise ValueError(
'Name "{}" is a holiday name in {}.'.format(name, self.country_holidays))
f'Name "{name}" is a holiday name in {self.country_holidays}.')
if check_seasonalities and name in self.seasonalities:
raise ValueError(
'Name "{}" already used for a seasonality.'.format(name))
f'Name "{name}" already used for a seasonality.')
if check_regressors and name in self.extra_regressors:
raise ValueError(
'Name "{}" already used for an added regressor.'.format(name))
'Name "{name}" already used for an added regressor.')
def setup_dataframe(self, df, initialize_scales=False):
"""Prepare dataframe for fitting or predicting.
@ -231,10 +231,9 @@ class Prophet(object):
-------
pd.DataFrame prepared for fitting or predicting.
"""
if 'y' in df:
df['y'] = pd.to_numeric(df['y'])
if np.isinf(df['y'].values).any():
raise ValueError('Found infinity in column y.')
df['y'] = pd.to_numeric(df['y'])
if np.isinf(df['y'].values).any():
raise ValueError('Found infinity in column y.')
if df['ds'].dtype == np.int64:
df['ds'] = df['ds'].astype(str)
df['ds'] = pd.to_datetime(df['ds'])
@ -248,18 +247,19 @@ class Prophet(object):
for name in self.extra_regressors:
if name not in df:
raise ValueError(
'Regressor "{}" missing from dataframe'.format(name))
f'Regressor "{name}" missing from dataframe')
df[name] = pd.to_numeric(df[name])
if df[name].isnull().any():
raise ValueError('Found NaN in column ' + name)
raise ValueError(f'Found NaN in column {name}')
for props in self.seasonalities.values():
condition_name = props['condition_name']
if condition_name is not None:
if condition_name not in df:
raise ValueError(
'Condition "{}" missing from dataframe'.format(condition_name))
f'Condition "{condition}" missing from dataframe')
if not df[condition_name].isin([True, False]).all():
raise ValueError('Found non-boolean in column ' + condition_name)
raise ValueError(
f'Found non-boolean in column {condition_name}')
df[condition_name] = df[condition_name].astype('bool')
if df.index.name == 'ds':
@ -271,14 +271,14 @@ class Prophet(object):
if self.logistic_floor:
if 'floor' not in df:
raise ValueError("Expected column 'floor'.")
raise ValueError('Expected column "floor".')
else:
df['floor'] = 0
if self.growth == 'logistic':
if 'cap' not in df:
raise ValueError(
"Capacities must be supplied for logistic growth in "
"column 'cap'"
'Capacities must be supplied for logistic growth in '
'column "cap"'
)
if (df['cap'] <= df['floor']).any():
raise ValueError(
@ -287,8 +287,7 @@ class Prophet(object):
df['cap_scaled'] = (df['cap'] - df['floor']) / self.y_scale
df['t'] = (df['ds'] - self.start) / self.t_scale
if 'y' in df:
df['y_scaled'] = (df['y'] - df['floor']) / self.y_scale
df['y_scaled'] = (df['y'] - df['floor']) / self.y_scale
for name, props in self.extra_regressors.items():
df[name] = ((df[name] - props['mu']) / props['std'])
@ -323,8 +322,7 @@ class Prophet(object):
standardize = False
if standardize == 'auto':
if set(df[name].unique()) == set([1, 0]):
# Don't standardize binary variables.
standardize = False
standardize = False # Don't standardize binary variables.
else:
standardize = True
if standardize:
@ -354,14 +352,14 @@ class Prophet(object):
'Changepoints must fall within training data.')
else:
# Place potential changepoints evenly through first
# changepoint_range proportion of the history
# {changepoint_range} proportion of the history
hist_size = int(np.floor(
self.history.shape[0] * self.changepoint_range))
if self.n_changepoints + 1 > hist_size:
self.n_changepoints = hist_size - 1
logger.info(
'n_changepoints greater than number of observations.'
'Using {}.'.format(self.n_changepoints)
f'n_changepoints greater than number of observations. '
'Using {self.n_changepoints}.'
)
if self.n_changepoints > 0:
cp_indexes = (
@ -454,10 +452,10 @@ class Prophet(object):
country_holidays_df = make_holidays_df(
year_list=year_list, country=self.country_holidays
)
all_holidays = pd.concat((all_holidays, country_holidays_df), sort=False)
all_holidays = pd.concat((all_holidays, country_holidays_df),
sort=False)
all_holidays.reset_index(drop=True, inplace=True)
# If the model has already been fit with a certain set of holidays,
# make sure we are using those same ones.
# Drop holidays that were not seen in data used to fit model
if self.train_holiday_names is not None:
# Remove holiday names didn't show up in fit
index_to_drop = all_holidays.index[
@ -469,10 +467,12 @@ class Prophet(object):
# Add holiday names in fit but not in predict with ds as NA
holidays_to_add = pd.DataFrame({
'holiday': self.train_holiday_names[
np.logical_not(self.train_holiday_names.isin(all_holidays.holiday))
np.logical_not(self.train_holiday_names
.isin(all_holidays.holiday))
]
})
all_holidays = pd.concat((all_holidays, holidays_to_add), sort=False)
all_holidays = pd.concat((all_holidays, holidays_to_add),
sort=False)
all_holidays.reset_index(drop=True, inplace=True)
return all_holidays
@ -509,12 +509,10 @@ class Prophet(object):
ps = float(row.get('prior_scale', self.holidays_prior_scale))
if np.isnan(ps):
ps = float(self.holidays_prior_scale)
if (
row.holiday in prior_scales and prior_scales[row.holiday] != ps
):
if row.holiday in prior_scales and prior_scales[row.holiday] != ps:
raise ValueError(
'Holiday {} does not have consistent prior scale '
'specification.'.format(row.holiday))
f'Holiday {row.holiday} does not have consistent prior '
'scale specification.')
if ps <= 0:
raise ValueError('Prior scale must be > 0')
prior_scales[row.holiday] = ps
@ -525,7 +523,6 @@ class Prophet(object):
loc = row_index.get_loc(occurrence)
except KeyError:
loc = None
key = '{}_delim_{}{}'.format(
row.holiday,
'+' if offset >= 0 else '-',
@ -534,11 +531,11 @@ class Prophet(object):
if loc is not None:
expanded_holidays[key][loc] = 1.
else:
# Access key to generate value
expanded_holidays[key]
expanded_holidays[key] # Access key to generate value
holiday_features = pd.DataFrame(expanded_holidays)
# Make sure column order is consistent
holiday_features = holiday_features[sorted(holiday_features.columns.tolist())]
holiday_features = holiday_features[sorted(holiday_features.columns
.tolist())]
prior_scale_list = [
prior_scales[h.split('_delim_')[0]]
for h in holiday_features.columns
@ -549,9 +546,8 @@ class Prophet(object):
self.train_holiday_names = pd.Series(holiday_names)
return holiday_features, prior_scale_list, holiday_names
def add_regressor(
self, name, prior_scale=None, standardize='auto', mode=None
):
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
@ -601,9 +597,8 @@ class Prophet(object):
}
return self
def add_seasonality(
self, name, period, fourier_order, prior_scale=None, mode=None, condition_name=None
):
def add_seasonality(self, name, period, fourier_order, prior_scale=None,
mode=None, condition_name=None):
"""Add a seasonal component with specified period, number of Fourier
components, and prior scale.
@ -621,9 +616,9 @@ class Prophet(object):
Additive means the seasonality will be added to the trend,
multiplicative means it will multiply the trend.
If condition_name is provided, the dataframe passed to `fit` and `predict`
should have a column with the specified condition_name containing booleans
which decides when to apply seasonality.
If condition_name is provided, the dataframe passed to `fit` and
`predict` should have a column with the specified condition_name
containing booleans which decides when to apply seasonality.
Parameters
----------
@ -640,7 +635,7 @@ class Prophet(object):
"""
if self.history is not None:
raise Exception(
"Seasonality must be added prior to model fitting.")
'Seasonality must be added prior to model fitting.')
if name not in ['daily', 'weekly', 'yearly']:
# Allow overwriting built-in seasonalities
self.validate_column_name(name, check_seasonalities=False)
@ -655,7 +650,7 @@ class Prophet(object):
if mode is None:
mode = self.seasonality_mode
if mode not in ['additive', 'multiplicative']:
raise ValueError("mode must be 'additive' or 'multiplicative'")
raise ValueError('mode must be "additive" or "multiplicative"')
if condition_name is not None:
self.validate_column_name(condition_name)
self.seasonalities[name] = {
@ -698,9 +693,8 @@ class Prophet(object):
# Set the holidays.
if self.country_holidays is not None:
logger.warning(
'Changing country holidays from {} to {}'.format(
self.country_holidays, country_name
)
f'Changing country holidays from {self.country_holidays} to '
'{country_name}.'
)
self.country_holidays = country_name
return self
@ -828,10 +822,8 @@ class Prophet(object):
# 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
):
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:
@ -879,14 +871,13 @@ class Prophet(object):
fourier_order = 0
if name in self.seasonalities:
logger.info(
'Found custom seasonality named "{name}", '
'disabling built-in {name} seasonality.'.format(name=name)
f'Found custom seasonality named "{name}", '
'disabling built-in {name} seasonality.'
)
elif auto_disable:
logger.info(
'Disabling {name} seasonality. Run prophet with '
'{name}_seasonality=True to override this.'.format(
name=name)
f'Disabling {name} seasonality. Run prophet with '
'{name}_seasonality=True to override this.'
)
else:
fourier_order = default_order
@ -1049,8 +1040,8 @@ class Prophet(object):
'Instantiate a new object.')
if ('ds' not in df) or ('y' not in df):
raise ValueError(
"Dataframe must have columns 'ds' and 'y' with the dates and "
"values respectively."
'Dataframe must have columns "ds" and "y" with the dates and '
'values respectively.'
)
history = df[df['y'].notnull()].copy()
if history.shape[0] < 2:
@ -1100,10 +1091,8 @@ class Prophet(object):
'sigma_obs': 1,
}
if (
(history['y'].min() == history['y'].max())
and self.growth == 'linear'
):
if ((history['y'].min() == history['y'].max())
and (self.growth == 'linear')):
# Nothing to fit.
self.params = stan_init()
self.params['sigma_obs'] = 1e-9
@ -1120,7 +1109,8 @@ class Prophet(object):
for par in self.stan_fit.model_pars:
self.params[par] = self.stan_fit[par]
# Shape vector parameters
if par in ['delta', 'beta'] and len(self.params[par].shape) < 2:
if (par in ['delta', 'beta']
and len(self.params[par].shape) < 2):
self.params[par] = self.params[par].reshape((-1, 1))
else:
args = dict(
@ -1133,9 +1123,9 @@ class Prophet(object):
try:
self.stan_fit = model.optimizing(**args)
except RuntimeError:
# Fall back on Newton
logger.warning(
'Optimization terminated abnormally. Falling back to Newton.'
'Optimization terminated abnormally. '
'Falling back to Newton.'
)
args['algorithm'] = 'Newton'
self.stan_fit = model.optimizing(**args)
@ -1146,8 +1136,10 @@ class Prophet(object):
# If no changepoints were requested, replace delta with 0s
if len(self.changepoints) == 0:
# Fold delta into the base rate k
self.params['k'] = self.params['k'] + self.params['delta'].reshape(-1)
self.params['delta'] = np.zeros(self.params['delta'].shape).reshape((-1, 1))
self.params['k'] = (self.params['k']
+ self.params['delta'].reshape(-1))
self.params['delta'] = (np.zeros(self.params['delta'].shape)
.reshape((-1, 1)))
return self
@ -1165,7 +1157,7 @@ class Prophet(object):
A pd.DataFrame with the forecast components.
"""
if self.history is None:
raise Exception('Model must be fit before predictions can be made.')
raise Exception('Model has not been fit.')
if df is None:
df = self.history.copy()
@ -1415,7 +1407,8 @@ class Prophet(object):
trend = self.sample_predictive_trend(df, iteration)
beta = self.params['beta'][iteration]
Xb_a = np.matmul(seasonal_features.values, beta * s_a.values) * self.y_scale
Xb_a = np.matmul(seasonal_features.values,
beta * s_a.values) * self.y_scale
Xb_m = np.matmul(seasonal_features.values, beta * s_m.values)
sigma = self.params['sigma_obs'][iteration]
@ -1493,7 +1486,7 @@ class Prophet(object):
requested number of periods.
"""
if self.history_dates is None:
raise Exception('Model must be fit before this can be used.')
raise Exception('Model as not been fit.')
last_date = self.history_dates.max()
dates = pd.date_range(
start=last_date,
@ -1507,8 +1500,8 @@ class Prophet(object):
return pd.DataFrame({'ds': dates})
def plot(self, fcst, ax=None, uncertainty=True, plot_cap=True, xlabel='ds',
ylabel='y', figsize=(10, 6)):
def plot(self, fcst, ax=None, uncertainty=True, plot_cap=True,
xlabel='ds', ylabel='y', figsize=(10, 6)):
"""Plot the Prophet forecast.
Parameters

View file

@ -259,8 +259,10 @@ class TestDiagnostics(TestCase):
self.assertTrue((m1.holidays == m2.holidays).values.all())
self.assertEqual(m1.country_holidays, m2.country_holidays)
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.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)
self.assertEqual(m1.mcmc_samples, m2.mcmc_samples)
self.assertEqual(m1.interval_width, m2.interval_width)

View file

@ -42,7 +42,8 @@ class TestProphet(TestCase):
train = DATA.head(N // 2)
future = DATA.tail(N // 2)
forecaster = Prophet(weekly_seasonality=False, yearly_seasonality=False)
forecaster = Prophet(weekly_seasonality=False,
yearly_seasonality=False)
forecaster.fit(train)
forecaster.predict(future)
@ -102,8 +103,10 @@ class TestProphet(TestCase):
m = Prophet(uncertainty_samples=uncertainty)
m.fit(train)
fcst = m.predict(future)
expected_cols = ['ds', 'trend', 'additive_terms', 'multiplicative_terms', 'weekly', 'yhat']
self.assertTrue(all(col in expected_cols for col in fcst.columns.tolist()))
expected_cols = ['ds', 'trend', 'additive_terms',
'multiplicative_terms', 'weekly', 'yhat']
self.assertTrue(all(col in expected_cols
for col in fcst.columns.tolist()))
def test_setup_dataframe(self):
m = Prophet()
@ -223,7 +226,7 @@ class TestProphet(TestCase):
mat = Prophet.fourier_series(DATA['ds'], 7, 3)
# These are from the R forecast package directly.
true_values = np.array([
0.7818315, 0.6234898, 0.9749279, -0.2225209, 0.4338837, -0.9009689,
0.7818315, 0.6234898, 0.9749279, -0.2225209, 0.4338837, -0.9009689
])
self.assertAlmostEqual(np.sum((mat[0] - true_values)**2), 0.0)
@ -231,7 +234,7 @@ class TestProphet(TestCase):
mat = Prophet.fourier_series(DATA['ds'], 365.25, 3)
# These are from the R forecast package directly.
true_values = np.array([
0.7006152, -0.7135393, -0.9998330, 0.01827656, 0.7262249, 0.6874572,
0.7006152, -0.7135393, -0.9998330, 0.01827656, 0.7262249, 0.6874572
])
self.assertAlmostEqual(np.sum((mat[0] - true_values)**2), 0.0)