From e081db52e13189e0ff35e77028f9b84e427b2186 Mon Sep 17 00:00:00 2001 From: Isaac Laughlin Date: Fri, 3 Mar 2017 11:29:12 -0800 Subject: [PATCH 1/4] Refactoring mpl code to address #62, #63 --- python/fbprophet/forecaster.py | 164 ++++++++++++++++++--------------- 1 file changed, 91 insertions(+), 73 deletions(-) diff --git a/python/fbprophet/forecaster.py b/python/fbprophet/forecaster.py index d940bd7..ac43274 100644 --- a/python/fbprophet/forecaster.py +++ b/python/fbprophet/forecaster.py @@ -34,6 +34,8 @@ except ImportError: # fb-block 2 class Prophet(object): + # Plotting default color for R/Python consistency + forecast_color = '#0072B2' def __init__( self, growth='linear', @@ -616,16 +618,15 @@ class Prophet(object): ------- a matplotlib figure. """ - forecast_color = '#0072B2' fig = plt.figure(facecolor='w', figsize=(10, 6)) ax = fig.add_subplot(111) ax.plot(self.history['ds'].values, self.history['y'], 'k.') - ax.plot(fcst['ds'].values, fcst['yhat'], ls='-', c=forecast_color) + ax.plot(fcst['ds'].values, fcst['yhat'], ls='-', c=self.forecast_color) if 'cap' in fcst: ax.plot(fcst['ds'].values, fcst['cap'], ls='--', c='k') if uncertainty: ax.fill_between(fcst['ds'].values, fcst['yhat_lower'], - fcst['yhat_upper'], color=forecast_color, alpha=0.2) + fcst['yhat_upper'], color=self.forecast_color, alpha=0.2) ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) @@ -648,87 +649,104 @@ class Prophet(object): a matplotlib figure. """ # Identify components to be plotted - plot_trend = True - plot_holidays = self.holidays is not None - plot_weekly = 'weekly' in fcst - plot_yearly = 'yearly' in fcst + components = [('plot_trend', True), + ('plot_holidays', self.holidays is not None), + ('plot_weekly', 'weekly' in fcst), + ('plot_yearly', 'yearly' in fcst)] + components = [(plot, cond) for plot, cond in components if cond] + npanel = len(components) + + fig, axes = plt.subplots(npanel, 1, facecolor='w', figsize=(9, 3 * npanel)) - npanel = plot_trend + plot_holidays + plot_weekly + plot_yearly - forecast_color = '#0072B2' - fig = plt.figure(facecolor='w', figsize=(9, 3 * npanel)) - panel_num = 1 - ax = fig.add_subplot(npanel, 1, panel_num) - ax.plot(fcst['ds'].values, fcst['trend'], ls='-', c=forecast_color) + artists = [] + for ax, plot in zip(axes, [getattr(self, plot) for plot, _ in components]): + artists += plot(fcst, ax=ax, uncertainty=uncertainty) + + fig.tight_layout() + return artists + + def plot_trend(self, fcst, ax=None, uncertainty=True, **plotargs): + artists = [] + if not ax: + ax = fig.add_subplot(111) + artists += ax.plot(fcst['ds'].values, fcst['trend'], ls='-', c=self.forecast_color) if 'cap' in fcst: - ax.plot(fcst['ds'].values, fcst['cap'], ls='--', c='k') + artists += ax.plot(fcst['ds'].values, fcst['cap'], ls='--', c='k') if uncertainty: - ax.fill_between( + artists += [ax.fill_between( fcst['ds'].values, fcst['trend_lower'], fcst['trend_upper'], - color=forecast_color, alpha=0.2) + color=self.forecast_color, alpha=0.2)] ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) ax.xaxis.set_major_locator(MaxNLocator(nbins=7)) ax.set_xlabel('ds') ax.set_ylabel('trend') + return artists - if plot_holidays: - panel_num += 1 + + def plot_holidays(self, fcst, ax=None, uncertainty=True): + artists = [] + if not ax: + ax = fig.add_subplot(111) + holiday_comps = self.holidays['holiday'].unique() + y_holiday = fcst[holiday_comps].sum(1) + y_holiday_l = fcst[[h + '_lower' for h in holiday_comps]].sum(1) + y_holiday_u = fcst[[h + '_upper' for h in holiday_comps]].sum(1) + # NOTE the above CI calculation is incorrect if holidays overlap + # in time. Since it is just for the visualization we will not + # worry about it now. + artists += ax.plot(fcst['ds'].values, y_holiday, ls='-', c=self.forecast_color) + if uncertainty: + artists += [ax.fill_between(fcst['ds'].values, y_holiday_l, y_holiday_u, + color=self.forecast_color, alpha=0.2)] + ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) + ax.xaxis.set_major_locator(MaxNLocator(nbins=7)) + ax.set_xlabel('ds') + ax.set_ylabel('holidays') + return artists + + def plot_weekly(self, fcst, ax=None, uncertainty=True): + artists = [] + if not ax: + ax = fig.add_subplot(111) + df_s = fcst.copy() + df_s['dow'] = df_s['ds'].dt.weekday_name + df_s = df_s.groupby('dow').first() + days = pd.date_range(start='2017-01-01', periods=7).weekday_name + y_weekly = [df_s.loc[d]['weekly'] for d in days] + y_weekly_l = [df_s.loc[d]['weekly_lower'] for d in days] + y_weekly_u = [df_s.loc[d]['weekly_upper'] for d in days] + artists += ax.plot(range(len(days)), y_weekly, ls='-', c=self.forecast_color) + if uncertainty: + artists += [ax.fill_between(range(len(days)), y_weekly_l, y_weekly_u, + color=self.forecast_color, alpha=0.2)] + ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) + ax.set_xticks(range(len(days))) + ax.set_xticklabels(days) + ax.set_xlabel('Day of week') + ax.set_ylabel('weekly') + return artists + + def plot_yearly(self, fcst, ax=None, uncertainty=True): + artists = [] + if not ax: ax = fig.add_subplot(npanel, 1, panel_num) - holiday_comps = self.holidays['holiday'].unique() - y_holiday = fcst[holiday_comps].sum(1) - y_holiday_l = fcst[[h + '_lower' for h in holiday_comps]].sum(1) - y_holiday_u = fcst[[h + '_upper' for h in holiday_comps]].sum(1) - # NOTE the above CI calculation is incorrect if holidays overlap - # in time. Since it is just for the visualization we will not - # worry about it now. - ax.plot(fcst['ds'].values, y_holiday, ls='-', c=forecast_color) - if uncertainty: - ax.fill_between(fcst['ds'].values, y_holiday_l, y_holiday_u, - color=forecast_color, alpha=0.2) - ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) - ax.xaxis.set_major_locator(MaxNLocator(nbins=7)) - ax.set_xlabel('ds') - ax.set_ylabel('holidays') + df_s = fcst.copy() + df_s['doy'] = df_s['ds'].map(lambda x: x.strftime('2000-%m-%d')) + df_s = df_s.groupby('doy').first().sort_index() + artists += ax.plot(pd.to_datetime(df_s.index), df_s['yearly'], ls='-', + c=self.forecast_color) + if uncertainty: + artists += [ax.fill_between( + pd.to_datetime(df_s.index), df_s['yearly_lower'], + df_s['yearly_upper'], color=self.forecast_color, alpha=0.2)] + ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) + months = MonthLocator(range(1, 13), bymonthday=1, interval=2) + ax.xaxis.set_major_formatter(DateFormatter('%B %-d')) + ax.xaxis.set_major_locator(months) + ax.set_xlabel('Day of year') + ax.set_ylabel('yearly') + return artists - if plot_weekly: - panel_num += 1 - ax = fig.add_subplot(npanel, 1, panel_num) - df_s = fcst.copy() - df_s['dow'] = df_s['ds'].dt.weekday_name - df_s = df_s.groupby('dow').first() - days = pd.date_range(start='2017-01-01', periods=7).weekday_name - y_weekly = [df_s.loc[d]['weekly'] for d in days] - y_weekly_l = [df_s.loc[d]['weekly_lower'] for d in days] - y_weekly_u = [df_s.loc[d]['weekly_upper'] for d in days] - ax.plot(range(len(days)), y_weekly, ls='-', c=forecast_color) - if uncertainty: - ax.fill_between(range(len(days)), y_weekly_l, y_weekly_u, - color=forecast_color, alpha=0.2) - ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) - ax.set_xticks(range(len(days))) - ax.set_xticklabels(days) - ax.set_xlabel('Day of week') - ax.set_ylabel('weekly') - if plot_yearly: - panel_num += 1 - ax = fig.add_subplot(npanel, 1, panel_num) - df_s = fcst.copy() - df_s['doy'] = df_s['ds'].map(lambda x: x.strftime('2000-%m-%d')) - df_s = df_s.groupby('doy').first().sort_index() - ax.plot(pd.to_datetime(df_s.index), df_s['yearly'], ls='-', - c=forecast_color) - if uncertainty: - ax.fill_between( - pd.to_datetime(df_s.index), df_s['yearly_lower'], - df_s['yearly_upper'], color=forecast_color, alpha=0.2) - ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) - months = MonthLocator(range(1, 13), bymonthday=1, interval=2) - ax.xaxis.set_major_formatter(DateFormatter('%B %-d')) - ax.xaxis.set_major_locator(months) - ax.set_xlabel('Day of year') - ax.set_ylabel('yearly') - - fig.tight_layout() - return fig # fb-block 9 From 9c82c8ed7aa349bee9df24efd843b845e7ead162 Mon Sep 17 00:00:00 2001 From: Isaac Laughlin Date: Fri, 3 Mar 2017 11:42:44 -0800 Subject: [PATCH 2/4] pep8 tweaks. --- python/fbprophet/forecaster.py | 43 +++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/python/fbprophet/forecaster.py b/python/fbprophet/forecaster.py index ac43274..8d6dc31 100644 --- a/python/fbprophet/forecaster.py +++ b/python/fbprophet/forecaster.py @@ -2,7 +2,7 @@ # All rights reserved. # # This source code is licensed under the BSD-style license found in the -# LICENSE file in the root directory of this source tree. An additional grant +# LICENSE file in the root directory of this source tree. An additional grant # of patent rights can be found in the PATENTS file in the same directory. from __future__ import absolute_import @@ -36,6 +36,7 @@ except ImportError: class Prophet(object): # Plotting default color for R/Python consistency forecast_color = '#0072B2' + def __init__( self, growth='linear', @@ -180,7 +181,6 @@ class Prophet(object): else: self.changepoints_t = np.array([0]) # dummy changepoint - def get_changepoint_matrix(self): A = np.zeros((self.history.shape[0], len(self.changepoints_t))) for i, t_i in enumerate(self.changepoints_t): @@ -261,7 +261,6 @@ class Prophet(object): # This relies pretty importantly on pandas keeping the columns in order. return pd.DataFrame(expanded_holidays) - def make_all_seasonality_features(self, df): seasonal_features = [ # Add a column of zeros in case no seasonality is used. @@ -626,7 +625,8 @@ class Prophet(object): ax.plot(fcst['ds'].values, fcst['cap'], ls='--', c='k') if uncertainty: ax.fill_between(fcst['ds'].values, fcst['yhat_lower'], - fcst['yhat_upper'], color=self.forecast_color, alpha=0.2) + fcst['yhat_upper'], color=self.forecast_color, + alpha=0.2) ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) @@ -655,21 +655,24 @@ class Prophet(object): ('plot_yearly', 'yearly' in fcst)] components = [(plot, cond) for plot, cond in components if cond] npanel = len(components) - - fig, axes = plt.subplots(npanel, 1, facecolor='w', figsize=(9, 3 * npanel)) + + fig, axes = plt.subplots(npanel, 1, facecolor='w', + figsize=(9, 3 * npanel)) artists = [] - for ax, plot in zip(axes, [getattr(self, plot) for plot, _ in components]): + for ax, plot in zip(axes, + [getattr(self, plot) for plot, _ in components]): artists += plot(fcst, ax=ax, uncertainty=uncertainty) - + fig.tight_layout() return artists - + def plot_trend(self, fcst, ax=None, uncertainty=True, **plotargs): artists = [] if not ax: ax = fig.add_subplot(111) - artists += ax.plot(fcst['ds'].values, fcst['trend'], ls='-', c=self.forecast_color) + artists += ax.plot(fcst['ds'].values, fcst['trend'], ls='-', + c=self.forecast_color) if 'cap' in fcst: artists += ax.plot(fcst['ds'].values, fcst['cap'], ls='--', c='k') if uncertainty: @@ -682,7 +685,6 @@ class Prophet(object): ax.set_ylabel('trend') return artists - def plot_holidays(self, fcst, ax=None, uncertainty=True): artists = [] if not ax: @@ -694,10 +696,12 @@ class Prophet(object): # NOTE the above CI calculation is incorrect if holidays overlap # in time. Since it is just for the visualization we will not # worry about it now. - artists += ax.plot(fcst['ds'].values, y_holiday, ls='-', c=self.forecast_color) + artists += ax.plot(fcst['ds'].values, y_holiday, ls='-', + c=self.forecast_color) if uncertainty: - artists += [ax.fill_between(fcst['ds'].values, y_holiday_l, y_holiday_u, - color=self.forecast_color, alpha=0.2)] + artists += [ax.fill_between(fcst['ds'].values, + y_holiday_l, y_holiday_u, + color=self.forecast_color, alpha=0.2)] ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) ax.xaxis.set_major_locator(MaxNLocator(nbins=7)) ax.set_xlabel('ds') @@ -715,10 +719,12 @@ class Prophet(object): y_weekly = [df_s.loc[d]['weekly'] for d in days] y_weekly_l = [df_s.loc[d]['weekly_lower'] for d in days] y_weekly_u = [df_s.loc[d]['weekly_upper'] for d in days] - artists += ax.plot(range(len(days)), y_weekly, ls='-', c=self.forecast_color) + artists += ax.plot(range(len(days)), y_weekly, ls='-', + c=self.forecast_color) if uncertainty: - artists += [ax.fill_between(range(len(days)), y_weekly_l, y_weekly_u, - color=self.forecast_color, alpha=0.2)] + artists += [ax.fill_between(range(len(days)), + y_weekly_l, y_weekly_u, + color=self.forecast_color, alpha=0.2)] ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) ax.set_xticks(range(len(days))) ax.set_xticklabels(days) @@ -734,7 +740,7 @@ class Prophet(object): df_s['doy'] = df_s['ds'].map(lambda x: x.strftime('2000-%m-%d')) df_s = df_s.groupby('doy').first().sort_index() artists += ax.plot(pd.to_datetime(df_s.index), df_s['yearly'], ls='-', - c=self.forecast_color) + c=self.forecast_color) if uncertainty: artists += [ax.fill_between( pd.to_datetime(df_s.index), df_s['yearly_lower'], @@ -748,5 +754,4 @@ class Prophet(object): return artists - # fb-block 9 From 13fc5c8ae4fd7f30121ee30d5f7c110563515d05 Mon Sep 17 00:00:00 2001 From: Isaac Laughlin Date: Fri, 3 Mar 2017 11:49:10 -0800 Subject: [PATCH 3/4] Changing my mind about forecast_color as a static class attr. --- python/fbprophet/forecaster.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/python/fbprophet/forecaster.py b/python/fbprophet/forecaster.py index 8d6dc31..1067880 100644 --- a/python/fbprophet/forecaster.py +++ b/python/fbprophet/forecaster.py @@ -34,9 +34,6 @@ except ImportError: # fb-block 2 class Prophet(object): - # Plotting default color for R/Python consistency - forecast_color = '#0072B2' - def __init__( self, growth='linear', @@ -620,12 +617,12 @@ class Prophet(object): fig = plt.figure(facecolor='w', figsize=(10, 6)) ax = fig.add_subplot(111) ax.plot(self.history['ds'].values, self.history['y'], 'k.') - ax.plot(fcst['ds'].values, fcst['yhat'], ls='-', c=self.forecast_color) + ax.plot(fcst['ds'].values, fcst['yhat'], ls='-', c='#0072B2') if 'cap' in fcst: ax.plot(fcst['ds'].values, fcst['cap'], ls='--', c='k') if uncertainty: ax.fill_between(fcst['ds'].values, fcst['yhat_lower'], - fcst['yhat_upper'], color=self.forecast_color, + fcst['yhat_upper'], color='#0072B2', alpha=0.2) ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) ax.set_xlabel(xlabel) @@ -672,13 +669,13 @@ class Prophet(object): if not ax: ax = fig.add_subplot(111) artists += ax.plot(fcst['ds'].values, fcst['trend'], ls='-', - c=self.forecast_color) + c='#0072B2') if 'cap' in fcst: artists += ax.plot(fcst['ds'].values, fcst['cap'], ls='--', c='k') if uncertainty: artists += [ax.fill_between( fcst['ds'].values, fcst['trend_lower'], fcst['trend_upper'], - color=self.forecast_color, alpha=0.2)] + color='#0072B2', alpha=0.2)] ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) ax.xaxis.set_major_locator(MaxNLocator(nbins=7)) ax.set_xlabel('ds') @@ -697,11 +694,11 @@ class Prophet(object): # in time. Since it is just for the visualization we will not # worry about it now. artists += ax.plot(fcst['ds'].values, y_holiday, ls='-', - c=self.forecast_color) + c='#0072B2') if uncertainty: artists += [ax.fill_between(fcst['ds'].values, y_holiday_l, y_holiday_u, - color=self.forecast_color, alpha=0.2)] + color='#0072B2', alpha=0.2)] ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) ax.xaxis.set_major_locator(MaxNLocator(nbins=7)) ax.set_xlabel('ds') @@ -720,11 +717,11 @@ class Prophet(object): y_weekly_l = [df_s.loc[d]['weekly_lower'] for d in days] y_weekly_u = [df_s.loc[d]['weekly_upper'] for d in days] artists += ax.plot(range(len(days)), y_weekly, ls='-', - c=self.forecast_color) + c='#0072B2') if uncertainty: artists += [ax.fill_between(range(len(days)), y_weekly_l, y_weekly_u, - color=self.forecast_color, alpha=0.2)] + color='#0072B2', alpha=0.2)] ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) ax.set_xticks(range(len(days))) ax.set_xticklabels(days) @@ -740,11 +737,11 @@ class Prophet(object): df_s['doy'] = df_s['ds'].map(lambda x: x.strftime('2000-%m-%d')) df_s = df_s.groupby('doy').first().sort_index() artists += ax.plot(pd.to_datetime(df_s.index), df_s['yearly'], ls='-', - c=self.forecast_color) + c='#0072B2') if uncertainty: artists += [ax.fill_between( pd.to_datetime(df_s.index), df_s['yearly_lower'], - df_s['yearly_upper'], color=self.forecast_color, alpha=0.2)] + df_s['yearly_upper'], color='#0072B2', alpha=0.2)] ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) months = MonthLocator(range(1, 13), bymonthday=1, interval=2) ax.xaxis.set_major_formatter(DateFormatter('%B %-d')) From 597fce143fcc752956e2d73135c1307bda5faf3f Mon Sep 17 00:00:00 2001 From: Isaac Laughlin Date: Fri, 3 Mar 2017 14:21:10 -0800 Subject: [PATCH 4/4] Adding docstrings to new plotting methods. --- python/fbprophet/forecaster.py | 54 +++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/python/fbprophet/forecaster.py b/python/fbprophet/forecaster.py index 1067880..d4b96f3 100644 --- a/python/fbprophet/forecaster.py +++ b/python/fbprophet/forecaster.py @@ -664,7 +664,20 @@ class Prophet(object): fig.tight_layout() return artists - def plot_trend(self, fcst, ax=None, uncertainty=True, **plotargs): + def plot_trend(self, fcst, ax=None, uncertainty=True): + """Plot the trend component of the forecast. + + Parameters + ---------- + fcst: pd.DataFrame output of self.predict. + ax: Optional matplotlib Axes to plot on. + uncertainty: Optional boolean to plot uncertainty intervals. + + Returns + ------- + a list of matplotlib artists + """ + artists = [] if not ax: ax = fig.add_subplot(111) @@ -683,6 +696,19 @@ class Prophet(object): return artists def plot_holidays(self, fcst, ax=None, uncertainty=True): + """Plot the holidays component of the forecast. + + Parameters + ---------- + fcst: pd.DataFrame output of self.predict. + ax: Optional matplotlib Axes to plot on. One will be created if this + is not provided. + uncertainty: Optional boolean to plot uncertainty intervals. + + Returns + ------- + a list of matplotlib artists + """ artists = [] if not ax: ax = fig.add_subplot(111) @@ -706,6 +732,19 @@ class Prophet(object): return artists def plot_weekly(self, fcst, ax=None, uncertainty=True): + """Plot the weekly component of the forecast. + + Parameters + ---------- + fcst: pd.DataFrame output of self.predict. + ax: Optional matplotlib Axes to plot on. One will be created if this + is not provided. + uncertainty: Optional boolean to plot uncertainty intervals. + + Returns + ------- + a list of matplotlib artists + """ artists = [] if not ax: ax = fig.add_subplot(111) @@ -730,6 +769,19 @@ class Prophet(object): return artists def plot_yearly(self, fcst, ax=None, uncertainty=True): + """Plot the yearly component of the forecast. + + Parameters + ---------- + fcst: pd.DataFrame output of self.predict. + ax: Optional matplotlib Axes to plot on. One will be created if + this is not provided. + uncertainty: Optional boolean to plot uncertainty intervals. + + Returns + ------- + a list of matplotlib artists + """ artists = [] if not ax: ax = fig.add_subplot(npanel, 1, panel_num)