From 3a6a338ac2c204b85c0246b42fd8d0199372d718 Mon Sep 17 00:00:00 2001 From: baogorek Date: Tue, 19 Nov 2019 11:26:12 -0500 Subject: [PATCH] Style modifications to fbprophet --- python/fbprophet/forecaster.py | 155 ++++++++++----------- python/fbprophet/tests/test_diagnostics.py | 6 +- python/fbprophet/tests/test_prophet.py | 13 +- 3 files changed, 86 insertions(+), 88 deletions(-) diff --git a/python/fbprophet/forecaster.py b/python/fbprophet/forecaster.py index 68ee1c6..f6a7878 100644 --- a/python/fbprophet/forecaster.py +++ b/python/fbprophet/forecaster.py @@ -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 diff --git a/python/fbprophet/tests/test_diagnostics.py b/python/fbprophet/tests/test_diagnostics.py index ae59e67..148252a 100644 --- a/python/fbprophet/tests/test_diagnostics.py +++ b/python/fbprophet/tests/test_diagnostics.py @@ -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) diff --git a/python/fbprophet/tests/test_prophet.py b/python/fbprophet/tests/test_prophet.py index acb0e33..f8241bc 100644 --- a/python/fbprophet/tests/test_prophet.py +++ b/python/fbprophet/tests/test_prophet.py @@ -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)