Saturating minimum for logistic growth Py

This commit is contained in:
bletham 2017-08-28 09:06:00 -07:00
parent 8d27643339
commit cc3238acb7
3 changed files with 72 additions and 17 deletions

View file

@ -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([

View file

@ -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'],

View file

@ -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]