mirror of
https://github.com/saymrwulf/prophet.git
synced 2026-09-16 22:20:23 +00:00
Saturating minimum for logistic growth Py
This commit is contained in:
parent
8d27643339
commit
cc3238acb7
3 changed files with 72 additions and 17 deletions
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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
|
||||
if 'cap' in df:
|
||||
cols = ['ds', 'cap', 'trend']
|
||||
else:
|
||||
# Drop columns except ds, cap, floor, and trend
|
||||
cols = ['ds', 'trend']
|
||||
if 'cap' in df:
|
||||
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'],
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue