diff --git a/python/fbprophet/tests/test_utilities.py b/python/fbprophet/tests/test_utilities.py new file mode 100644 index 0000000..d27aa06 --- /dev/null +++ b/python/fbprophet/tests/test_utilities.py @@ -0,0 +1,41 @@ +# Copyright (c) Facebook, Inc. and its affiliates. + +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import os +from unittest import TestCase + +import numpy as np +import pandas as pd +from fbprophet import Prophet +from fbprophet.utilities import regressor_coefficients + + +DATA = pd.read_csv( + os.path.join(os.path.dirname(__file__), 'data.csv'), + parse_dates=['ds'], +) + +class TestUtilities(TestCase): + def test_regressor_coefficients(self): + m = Prophet() + N = DATA.shape[0] + df = DATA.copy() + np.random.seed(123) + df['regr1'] = np.random.normal(size=N) + df['regr2'] = np.random.normal(size=N) + m.add_regressor('regr1', mode='additive') + m.add_regressor('regr2', mode='multiplicative') + m.fit(df) + + coefs = regressor_coefficients(m) + self.assertTrue(coefs.shape == (2, 6)) + # No MCMC sampling, so lower and upper should be the same as mean + self.assertTrue(np.array_equal(coefs['coef_lower'].values, coefs['coef'].values)) + self.assertTrue(np.array_equal(coefs['coef_upper'].values, coefs['coef'].values)) diff --git a/python/fbprophet/utilities.py b/python/fbprophet/utilities.py new file mode 100644 index 0000000..f59fb61 --- /dev/null +++ b/python/fbprophet/utilities.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Facebook, Inc. and its affiliates. + +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +from __future__ import absolute_import, division, print_function + +import numpy as np +import pandas as pd + +def regressor_index(m, name): + """Given the name of a regressor, return its (column) index in the `beta` matrix. + + Parameters + ---------- + m: Prophet model object, after fitting. + name: Name of the regressor, as passed into the `add_regressor` function. + + Returns + ------- + The column index of the regressor in the `beta` matrix. + """ + return np.extract( + m.train_component_cols[name] == 1, m.train_component_cols.index + )[0] + +def regressor_coefficients(m): + """Summarise the coefficients of the extra regressors used in the model. + + For additive regressors, the coefficient represents the incremental impact + on `y` of a unit increase in the regressor. For multiplicative regressors, + the incremental impact is equal to `trend(t)` multiplied by the coefficient. + + Coefficients are measured on the original scale of the training data. + + Parameters + ---------- + m: Prophet model object, after fitting. + + Returns + ------- + pd.DataFrame containing: + - `regressor`: Name of the regressor + - `regressor_mode`: Whether the regressor has an additive or multiplicative + effect on `y`. + - `center`: The mean of the regressor if it was standardized. Otherwise 0. + - `coef_lower`: Lower bound for the coefficient, estimated from the MCMC samples. + Only different to `coef` if `mcmc_samples > 0`. + - `coef`: Expected value of the coefficient. + - `coef_upper`: Upper bound for the coefficient, estimated from MCMC samples. + Only to different to `coef` if `mcmc_samples > 0`. + """ + assert len(m.extra_regressors) > 0, 'No extra regressors found.' + y_max = m.history['y'].max() + coefs = [] + for regressor, params in m.extra_regressors.items(): + beta = m.params['beta'][:, regressor_index(m, regressor)] + if params['mode'] == 'additive': + coef = beta * y_max / params['std'] + else: + coef = beta / params['std'] + percentiles = [ + (1 - m.interval_width) / 2, + 1 - (1 - m.interval_width) / 2, + ] + coef_bounds = np.quantile(coef, q=percentiles) + record = { + 'regressor': regressor, + 'regressor_mode': params['mode'], + 'center': params['mu'], + 'coef_lower': coef_bounds[0], + 'coef': np.mean(coef), + 'coef_upper': coef_bounds[1], + } + coefs.append(record) + + return pd.DataFrame(coefs)