Bläddra i källkod

Saturating minimum for logistic growth Py

bletham 8 år sedan
förälder
incheckning
cc3238acb7
3 ändrade filer med 72 tillägg och 17 borttagningar
  1. 5 1
      python/fbprophet/diagnostics.py
  2. 38 16
      python/fbprophet/forecaster.py
  3. 29 0
      python/fbprophet/tests/test_prophet.py

+ 5 - 1
python/fbprophet/diagnostics.py

@@ -91,7 +91,11 @@ def simulated_historical_forecasts(model, horizon, k, period=None):
         m.fit(df[df['ds'] <= cutoff])
         # Calculate yhat
         index_predicted = (df['ds'] > cutoff) & (df['ds'] <= cutoff + horizon)
-        columns = ['ds'] + (['cap'] if m.growth == 'logistic' else [])
+        columns = ['ds']
+        if m.growth == 'logistic':
+            columns.append('cap')
+            if m.logistic_floor:
+                columns.append('floor')
         yhat = m.predict(df[index_predicted][columns])
         # Merge yhat(predicts), y(df, original data) and cutoff
         predicts.append(pd.concat([

+ 38 - 16
python/fbprophet/forecaster.py

@@ -134,6 +134,7 @@ class Prophet(object):
         # Set during fitting
         self.start = None
         self.y_scale = None
+        self.logistic_floor = False
         self.t_scale = None
         self.changepoints_t = None
         self.seasonalities = {}
@@ -184,7 +185,8 @@ class Prophet(object):
         rn_u = [n + '_upper' for n in reserved_names]
         reserved_names.extend(rn_l)
         reserved_names.extend(rn_u)
-        reserved_names.extend(['ds', 'y'])
+        reserved_names.extend([
+            'ds', 'y', 'cap', 'floor', 'y_scaled', 'cap_scaled'])
         if name in reserved_names:
             raise ValueError('Name "{}" is reserved.'.format(name))
         if (check_holidays and self.holidays is not None and
@@ -231,7 +233,12 @@ class Prophet(object):
         df.reset_index(inplace=True, drop=True)
 
         if initialize_scales:
-            self.y_scale = df['y'].abs().max()
+            if self.growth == 'logistic' and 'floor' in df:
+                self.logistic_floor = True
+                floor = df['floor']
+            else:
+                floor = 0.
+            self.y_scale = (df['y'] - floor).abs().max()
             if self.y_scale == 0:
                 self.y_scale = 1
             self.start = df['ds'].min()
@@ -252,13 +259,18 @@ class Prophet(object):
                     self.extra_regressors[name]['mu'] = mu
                     self.extra_regressors[name]['std'] = std
 
-        df['t'] = (df['ds'] - self.start) / self.t_scale
-        if 'y' in df:
-            df['y_scaled'] = df['y'] / self.y_scale
-
+        if self.logistic_floor:
+            if 'floor' not in df:
+                raise ValueError("Expected column 'floor'.")
+        else:
+            df['floor'] = 0
         if self.growth == 'logistic':
             assert 'cap' in df
-            df['cap_scaled'] = df['cap'] / self.y_scale
+            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
 
         for name, props in self.extra_regressors.items():
             df[name] = pd.to_numeric(df[name])
@@ -694,9 +706,14 @@ class Prophet(object):
         i0, i1 = df['ds'].idxmin(), df['ds'].idxmax()
         T = df['t'].iloc[i1] - df['t'].iloc[i0]
 
-        # Force valid values, in case y > cap.
-        r0 = max(1.01, df['cap_scaled'].iloc[i0] / df['y_scaled'].iloc[i0])
-        r1 = max(1.01, df['cap_scaled'].iloc[i1] / df['y_scaled'].iloc[i1])
+        # Force valid values, in case y > cap or y < 0
+        C0 = df['cap_scaled'].iloc[i0]
+        C1 = df['cap_scaled'].iloc[i1]
+        y0 = max(0.01 * C0, min(0.99 * C0, df['y_scaled'].iloc[i0]))
+        y1 = max(0.01 * C1, min(0.99 * C1, df['y_scaled'].iloc[i1]))
+
+        r0 = C0 / y0
+        r1 = C1 / y1
 
         if abs(r0 - r1) <= 0.01:
             r0 = 1.05 * r0
@@ -840,11 +857,12 @@ class Prophet(object):
         seasonal_components = self.predict_seasonal_components(df)
         intervals = self.predict_uncertainty(df)
 
-        # Drop columns except ds, cap, and trend
+        # Drop columns except ds, cap, floor, and trend
+        cols = ['ds', 'trend']
         if 'cap' in df:
-            cols = ['ds', 'cap', 'trend']
-        else:
-            cols = ['ds', 'trend']
+            cols.append('cap')
+        if self.logistic_floor:
+            cols.append('floor')
         # Add in forecast components
         df2 = pd.concat((df[cols], intervals, seasonal_components), axis=1)
         df2['yhat'] = df2['trend'] + df2['seasonal']
@@ -934,7 +952,7 @@ class Prophet(object):
             trend = self.piecewise_logistic(
                 t, cap, deltas, k, m, self.changepoints_t)
 
-        return trend * self.y_scale
+        return trend * self.y_scale + df['floor']
 
     def predict_seasonal_components(self, df):
         """Predict seasonality components, holidays, and added regressors.
@@ -1162,7 +1180,7 @@ class Prophet(object):
             trend = self.piecewise_logistic(t, cap, deltas, k, m,
                                             changepoint_ts)
 
-        return trend * self.y_scale
+        return trend * self.y_scale + df['floor']
 
     def make_future_dataframe(self, periods, freq='D', include_history=True):
         """Simulate the trend using the extrapolated generative model.
@@ -1219,6 +1237,8 @@ class Prophet(object):
         ax.plot(fcst['ds'].values, fcst['yhat'], ls='-', c='#0072B2')
         if 'cap' in fcst and plot_cap:
             ax.plot(fcst['ds'].values, fcst['cap'], ls='--', c='k')
+        if self.logistic_floor and 'floor' in fcst and plot_cap :
+            ax.plot(fcst['ds'].values, fcst['floor'], ls='--', c='k')
         if uncertainty:
             ax.fill_between(fcst['ds'].values, fcst['yhat_lower'],
                             fcst['yhat_upper'], color='#0072B2',
@@ -1313,6 +1333,8 @@ class Prophet(object):
         artists += ax.plot(fcst['ds'].values, fcst[name], ls='-', c='#0072B2')
         if 'cap' in fcst and plot_cap:
             artists += ax.plot(fcst['ds'].values, fcst['cap'], ls='--', c='k')
+        if self.logistic_floor and 'floor' in fcst and plot_cap :
+            ax.plot(fcst['ds'].values, fcst['floor'], ls='--', c='k')
         if uncertainty:
             artists += [ax.fill_between(
                 fcst['ds'].values, fcst[name + '_lower'],

+ 29 - 0
python/fbprophet/tests/test_prophet.py

@@ -109,6 +109,35 @@ class TestProphet(TestCase):
         self.assertTrue('y_scaled' in history)
         self.assertEqual(history['y_scaled'].max(), 1.0)
 
+    def test_logistic_floor(self):
+        m = Prophet(growth='logistic')
+        N = DATA.shape[0]
+        history = DATA.head(N // 2).copy()
+        history['floor'] = 10.
+        history['cap'] = 80.
+        future = DATA.tail(N // 2).copy()
+        future['cap'] = 80.
+        future['floor'] = 10.
+        m.fit(history)
+        self.assertTrue(m.logistic_floor)
+        self.assertTrue('floor' in m.history)
+        self.assertAlmostEqual(m.history['y_scaled'][0], 1.)
+        fcst1 = m.predict(future)
+
+        m2 = Prophet(growth='logistic')
+        history2 = history.copy()
+        history2['y'] += 10.
+        history2['floor'] += 10.
+        history2['cap'] += 10.
+        future['cap'] += 10.
+        future['floor'] += 10.
+        m2.fit(history2)
+        self.assertAlmostEqual(m2.history['y_scaled'][0], 1.)
+        fcst2 = m2.predict(future)
+        fcst2['yhat'] -= 10.
+        # Check for approximate shift invariance
+        self.assertTrue((np.abs(fcst1['yhat'] - fcst2['yhat']) < 1).all())
+
     def test_get_changepoints(self):
         m = Prophet()
         N = DATA.shape[0]