test_diagnostics.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. # Copyright (c) 2017-present, Facebook, Inc.
  2. # All rights reserved.
  3. #
  4. # This source code is licensed under the BSD-style license found in the
  5. # LICENSE file in the root directory of this source tree. An additional grant
  6. # of patent rights can be found in the PATENTS file in the same directory.
  7. from __future__ import absolute_import
  8. from __future__ import division
  9. from __future__ import print_function
  10. from __future__ import unicode_literals
  11. import itertools
  12. import numpy as np
  13. import pandas as pd
  14. # fb-block 1 start
  15. import os
  16. from unittest import TestCase
  17. from fbprophet import Prophet
  18. from fbprophet import diagnostics
  19. DATA_all = pd.read_csv(
  20. os.path.join(os.path.dirname(__file__), 'data.csv'), parse_dates=['ds']
  21. )
  22. DATA = DATA_all.head(100)
  23. # fb-block 1 end
  24. # fb-block 2
  25. class TestDiagnostics(TestCase):
  26. def __init__(self, *args, **kwargs):
  27. super(TestDiagnostics, self).__init__(*args, **kwargs)
  28. # Use first 100 record in data.csv
  29. self.__df = DATA
  30. def test_cross_validation(self):
  31. m = Prophet()
  32. m.fit(self.__df)
  33. # Calculate the number of cutoff points(k)
  34. horizon = pd.Timedelta('4 days')
  35. period = pd.Timedelta('10 days')
  36. initial = pd.Timedelta('115 days')
  37. df_cv = diagnostics.cross_validation(
  38. m, horizon='4 days', period='10 days', initial='115 days')
  39. self.assertEqual(len(np.unique(df_cv['cutoff'])), 3)
  40. self.assertEqual(max(df_cv['ds'] - df_cv['cutoff']), horizon)
  41. self.assertTrue(min(df_cv['cutoff']) >= min(self.__df['ds']) + initial)
  42. dc = df_cv['cutoff'].diff()
  43. dc = dc[dc > pd.Timedelta(0)].min()
  44. self.assertTrue(dc >= period)
  45. self.assertTrue((df_cv['cutoff'] < df_cv['ds']).all())
  46. # Each y in df_cv and self.__df with same ds should be equal
  47. df_merged = pd.merge(df_cv, self.__df, 'left', on='ds')
  48. self.assertAlmostEqual(
  49. np.sum((df_merged['y_x'] - df_merged['y_y']) ** 2), 0.0)
  50. df_cv = diagnostics.cross_validation(
  51. m, horizon='4 days', period='10 days', initial='135 days')
  52. self.assertEqual(len(np.unique(df_cv['cutoff'])), 1)
  53. with self.assertRaises(ValueError):
  54. diagnostics.cross_validation(
  55. m, horizon='10 days', period='10 days', initial='140 days')
  56. def test_cross_validation_logistic(self):
  57. df = self.__df.copy()
  58. df['cap'] = 40
  59. m = Prophet(growth='logistic').fit(df)
  60. df_cv = diagnostics.cross_validation(
  61. m, horizon='1 days', period='1 days', initial='140 days')
  62. self.assertEqual(len(np.unique(df_cv['cutoff'])), 2)
  63. self.assertTrue((df_cv['cutoff'] < df_cv['ds']).all())
  64. df_merged = pd.merge(df_cv, self.__df, 'left', on='ds')
  65. self.assertAlmostEqual(
  66. np.sum((df_merged['y_x'] - df_merged['y_y']) ** 2), 0.0)
  67. def test_cross_validation_extra_regressors(self):
  68. df = self.__df.copy()
  69. df['extra'] = range(df.shape[0])
  70. m = Prophet()
  71. m.add_seasonality(name='monthly', period=30.5, fourier_order=5)
  72. m.add_regressor('extra')
  73. m.fit(df)
  74. df_cv = diagnostics.cross_validation(
  75. m, horizon='4 days', period='4 days', initial='135 days')
  76. self.assertEqual(len(np.unique(df_cv['cutoff'])), 2)
  77. period = pd.Timedelta('4 days')
  78. dc = df_cv['cutoff'].diff()
  79. dc = dc[dc > pd.Timedelta(0)].min()
  80. self.assertTrue(dc >= period)
  81. self.assertTrue((df_cv['cutoff'] < df_cv['ds']).all())
  82. df_merged = pd.merge(df_cv, self.__df, 'left', on='ds')
  83. self.assertAlmostEqual(
  84. np.sum((df_merged['y_x'] - df_merged['y_y']) ** 2), 0.0)
  85. def test_cross_validation_default_value_check(self):
  86. m = Prophet()
  87. m.fit(self.__df)
  88. # Default value of initial should be equal to 3 * horizon
  89. df_cv1 = diagnostics.cross_validation(
  90. m, horizon='32 days', period='10 days')
  91. df_cv2 = diagnostics.cross_validation(
  92. m, horizon='32 days', period='10 days', initial='96 days')
  93. self.assertAlmostEqual(
  94. ((df_cv1['y'] - df_cv2['y']) ** 2).sum(), 0.0)
  95. self.assertAlmostEqual(
  96. ((df_cv1['yhat'] - df_cv2['yhat']) ** 2).sum(), 0.0)
  97. def test_performance_metrics(self):
  98. m = Prophet()
  99. m.fit(self.__df)
  100. df_cv = diagnostics.cross_validation(
  101. m, horizon='4 days', period='10 days', initial='90 days')
  102. # Aggregation level none
  103. df_none = diagnostics.performance_metrics(df_cv, rolling_window=0)
  104. self.assertEqual(
  105. set(df_none.columns),
  106. {'horizon', 'coverage', 'mae', 'mape', 'mse', 'rmse'},
  107. )
  108. self.assertEqual(df_none.shape[0], 16)
  109. # Aggregation level 0.2
  110. df_horizon = diagnostics.performance_metrics(df_cv, rolling_window=0.2)
  111. self.assertEqual(len(df_horizon['horizon'].unique()), 4)
  112. self.assertEqual(df_horizon.shape[0], 14)
  113. # Aggregation level all
  114. df_all = diagnostics.performance_metrics(df_cv, rolling_window=1)
  115. self.assertEqual(df_all.shape[0], 1)
  116. for metric in ['mse', 'mape', 'mae', 'coverage']:
  117. self.assertEqual(df_all[metric].values[0], df_none[metric].mean())
  118. # Custom list of metrics
  119. df_horizon = diagnostics.performance_metrics(
  120. df_cv, metrics=['coverage', 'mse'],
  121. )
  122. self.assertEqual(
  123. set(df_horizon.columns),
  124. {'coverage', 'mse', 'horizon'},
  125. )
  126. def test_copy(self):
  127. df = DATA_all.copy()
  128. df['cap'] = 200.
  129. df['binary_feature'] = [0] * 255 + [1] * 255
  130. # These values are created except for its default values
  131. holiday = pd.DataFrame(
  132. {'ds': pd.to_datetime(['2016-12-25']), 'holiday': ['x']})
  133. products = itertools.product(
  134. ['linear', 'logistic'], # growth
  135. [None, pd.to_datetime(['2016-12-25'])], # changepoints
  136. [3], # n_changepoints
  137. [0.9], # changepoint_range
  138. [True, False], # yearly_seasonality
  139. [True, False], # weekly_seasonality
  140. [True, False], # daily_seasonality
  141. [None, holiday], # holidays
  142. ['additive', 'multiplicative'], # seasonality_mode
  143. [1.1], # seasonality_prior_scale
  144. [1.1], # holidays_prior_scale
  145. [0.1], # changepoint_prior_scale
  146. [100], # mcmc_samples
  147. [0.9], # interval_width
  148. [200] # uncertainty_samples
  149. )
  150. # Values should be copied correctly
  151. for product in products:
  152. m1 = Prophet(*product)
  153. m1.history = m1.setup_dataframe(
  154. df.copy(), initialize_scales=True)
  155. m1.set_auto_seasonalities()
  156. m2 = diagnostics.prophet_copy(m1)
  157. self.assertEqual(m1.growth, m2.growth)
  158. self.assertEqual(m1.n_changepoints, m2.n_changepoints)
  159. self.assertEqual(m1.changepoint_range, m2.changepoint_range)
  160. self.assertEqual(m1.changepoints, m2.changepoints)
  161. self.assertEqual(False, m2.yearly_seasonality)
  162. self.assertEqual(False, m2.weekly_seasonality)
  163. self.assertEqual(False, m2.daily_seasonality)
  164. self.assertEqual(
  165. m1.yearly_seasonality, 'yearly' in m2.seasonalities)
  166. self.assertEqual(
  167. m1.weekly_seasonality, 'weekly' in m2.seasonalities)
  168. self.assertEqual(
  169. m1.daily_seasonality, 'daily' in m2.seasonalities)
  170. if m1.holidays is None:
  171. self.assertEqual(m1.holidays, m2.holidays)
  172. else:
  173. self.assertTrue((m1.holidays == m2.holidays).values.all())
  174. self.assertEqual(m1.seasonality_mode, m2.seasonality_mode)
  175. self.assertEqual(m1.seasonality_prior_scale, m2.seasonality_prior_scale)
  176. self.assertEqual(m1.changepoint_prior_scale, m2.changepoint_prior_scale)
  177. self.assertEqual(m1.holidays_prior_scale, m2.holidays_prior_scale)
  178. self.assertEqual(m1.mcmc_samples, m2.mcmc_samples)
  179. self.assertEqual(m1.interval_width, m2.interval_width)
  180. self.assertEqual(m1.uncertainty_samples, m2.uncertainty_samples)
  181. # Check for cutoff and custom seasonality and extra regressors
  182. changepoints = pd.date_range('2012-06-15', '2012-09-15')
  183. cutoff = pd.Timestamp('2012-07-25')
  184. m1 = Prophet(changepoints=changepoints)
  185. m1.add_seasonality('custom', 10, 5)
  186. m1.add_regressor('binary_feature')
  187. m1.fit(df)
  188. m2 = diagnostics.prophet_copy(m1, cutoff=cutoff)
  189. changepoints = changepoints[changepoints <= cutoff]
  190. self.assertTrue((changepoints == m2.changepoints).all())
  191. self.assertTrue('custom' in m2.seasonalities)
  192. self.assertTrue('binary_feature' in m2.extra_regressors)