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