├── .gitattributes ├── .gitignore ├── Examples ├── AutoARIMA Trend.ipynb ├── Exogenous Example.ipynb ├── Linear Changepoint Model.ipynb └── Thymeboost_LBF_M4.ipynb ├── LICENSE.txt ├── README.md ├── ThymeBoost ├── Datasets │ ├── AirPassengers.csv │ ├── Sales_and_Marketing.csv │ ├── __init__.py │ └── examples.py ├── Forecaster.py ├── ThymeBoost.py ├── __init__.py ├── __pycache__ │ ├── CostFunctions.cpython-37.pyc │ ├── FitExogenous.cpython-37.pyc │ ├── FitSeasonality.cpython-37.pyc │ ├── FitTrend.cpython-37.pyc │ ├── FitTrendTest.cpython-37.pyc │ ├── SplitProposals.cpython-37.pyc │ ├── ThymeBoost.cpython-37.pyc │ ├── ThymeBoostOptimizer.cpython-37.pyc │ ├── __init__.cpython-37.pyc │ ├── _booster.cpython-37.pyc │ ├── booster.cpython-37.pyc │ ├── cost_functions.cpython-37.pyc │ ├── ensemble.cpython-37.pyc │ ├── fit_seasonality_test.cpython-37.pyc │ ├── optimizer.cpython-37.pyc │ ├── param_iterator.cpython-37.pyc │ ├── predict.cpython-37.pyc │ ├── predict_functions.cpython-37.pyc │ ├── skylasso.cpython-37.pyc │ └── split_proposals.cpython-37.pyc ├── cost_functions.py ├── ensemble.py ├── exogenous_models │ ├── __init__.py │ ├── __pycache__ │ │ ├── decision_tree_exogenous.cpython-37.pyc │ │ ├── exogenous_base_class.cpython-37.pyc │ │ ├── glm_exogenous.cpython-37.pyc │ │ └── ols_exogenous.cpython-37.pyc │ ├── decision_tree_exogenous.py │ ├── exogenous_base_class.py │ ├── glm_exogenous.py │ └── ols_exogenous.py ├── fit_components │ ├── __init__.py │ ├── __pycache__ │ │ ├── fit_exogenous.cpython-37.pyc │ │ ├── fit_seasonality.cpython-37.pyc │ │ ├── fit_seasonality_test.cpython-37.pyc │ │ ├── fit_trend.cpython-37.pyc │ │ └── fit_trend_test.cpython-37.pyc │ ├── fit_exogenous.py │ ├── fit_seasonality.py │ └── fit_trend.py ├── fitter │ ├── __init__.py │ ├── __pycache__ │ │ ├── booster.cpython-37.pyc │ │ └── decompose.cpython-37.pyc │ ├── booster.py │ └── decompose.py ├── optimizer.py ├── param_iterator.py ├── predict_functions.py ├── seasonality_models │ ├── __init__.py │ ├── __pycache__ │ │ ├── SeasonalityBaseModel.cpython-37.pyc │ │ ├── __init__.cpython-37.pyc │ │ ├── classic_seasonality.cpython-37.pyc │ │ ├── fourier_seasonality.cpython-37.pyc │ │ └── seasonality_base_class.cpython-37.pyc │ ├── classic_seasonality.py │ ├── fourier_seasonality.py │ ├── naive_seasonality.py │ └── seasonality_base_class.py ├── split_proposals.py ├── tests │ └── unitTests.py ├── trend_models │ ├── __init__.py │ ├── __pycache__ │ │ ├── ArimaModel.cpython-37.pyc │ │ ├── EtsModel.cpython-37.pyc │ │ ├── EwmModel.cpython-37.pyc │ │ ├── LinearModel.cpython-37.pyc │ │ ├── LoessModel.cpython-37.pyc │ │ ├── MeanModel.cpython-37.pyc │ │ ├── MedianModel.cpython-37.pyc │ │ ├── RansacModel.cpython-37.pyc │ │ ├── TrendBaseModel.cpython-37.pyc │ │ ├── __init__.cpython-37.pyc │ │ ├── arima_trend.cpython-37.pyc │ │ ├── ets_trend.cpython-37.pyc │ │ ├── ewm_trend.cpython-37.pyc │ │ ├── linear_trend.cpython-37.pyc │ │ ├── loess_trend.cpython-37.pyc │ │ ├── mean_trend.cpython-37.pyc │ │ ├── median_trend.cpython-37.pyc │ │ ├── ransac_trend.cpython-37.pyc │ │ └── trend_base_class.cpython-37.pyc │ ├── arima_trend.py │ ├── croston_trend.py │ ├── decision_tree_trend.py │ ├── ets_trend.py │ ├── ewm_trend.py │ ├── fast_arima_trend.py │ ├── fast_ces_trend.py │ ├── fast_ets_trend.py │ ├── fast_imapa_trend.py │ ├── fast_ses_trend.py │ ├── fast_theta_trend.py │ ├── lbf_trend.py │ ├── linear_trend.py │ ├── loess_trend.py │ ├── mean_trend.py │ ├── median_trend.py │ ├── moving_average_trend.py │ ├── naive_trend.py │ ├── ransac_trend.py │ ├── svr_trend.py │ ├── theta_trend.py │ ├── trend_base_class.py │ └── zero_trend.py └── utils │ ├── __init__.py │ ├── __pycache__ │ ├── build_output.cpython-37.pyc │ ├── get_complexity.cpython-37.pyc │ ├── plotting.cpython-37.pyc │ └── trend_dampen.cpython-37.pyc │ ├── build_output.py │ ├── get_complexity.py │ ├── hyperoptimizer.py │ ├── plotting.py │ └── trend_dampen.py ├── docs ├── Makefile ├── README.md ├── ThymeBoost.exogenous_models.rst ├── ThymeBoost.fit_components.rst ├── ThymeBoost.fitter.rst ├── ThymeBoost.rst ├── ThymeBoost.seasonality_models.rst ├── ThymeBoost.trend_models.rst ├── ThymeBoost.utils.rst ├── _static │ └── tb_logo (2).png ├── conf.py ├── index.rst ├── make.bat ├── modules.rst ├── read_me.rst └── setup.rst ├── setup.py └── static ├── complicated_components.png ├── complicated_output_bad.png ├── complicated_time_series.png ├── ensemble_output.png ├── n_rounds1.png ├── optimized_ensemble.png ├── optimizer_output.png ├── tb_complicated_output.png ├── tb_components_1.png ├── tb_components_2.png ├── tb_logo.png ├── tb_output_1.png ├── tb_output_2.png ├── thymeboost_flow.png ├── thymeboost_logo.png ├── time_series.png └── time_series_2.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | _build/ 12 | Thymeboost.egg-info/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2021 Tyler Blume 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. -------------------------------------------------------------------------------- /ThymeBoost/Datasets/AirPassengers.csv: -------------------------------------------------------------------------------- 1 | Month,#Passengers 2 | 1949-01,112 3 | 1949-02,118 4 | 1949-03,132 5 | 1949-04,129 6 | 1949-05,121 7 | 1949-06,135 8 | 1949-07,148 9 | 1949-08,148 10 | 1949-09,136 11 | 1949-10,119 12 | 1949-11,104 13 | 1949-12,118 14 | 1950-01,115 15 | 1950-02,126 16 | 1950-03,141 17 | 1950-04,135 18 | 1950-05,125 19 | 1950-06,149 20 | 1950-07,170 21 | 1950-08,170 22 | 1950-09,158 23 | 1950-10,133 24 | 1950-11,114 25 | 1950-12,140 26 | 1951-01,145 27 | 1951-02,150 28 | 1951-03,178 29 | 1951-04,163 30 | 1951-05,172 31 | 1951-06,178 32 | 1951-07,199 33 | 1951-08,199 34 | 1951-09,184 35 | 1951-10,162 36 | 1951-11,146 37 | 1951-12,166 38 | 1952-01,171 39 | 1952-02,180 40 | 1952-03,193 41 | 1952-04,181 42 | 1952-05,183 43 | 1952-06,218 44 | 1952-07,230 45 | 1952-08,242 46 | 1952-09,209 47 | 1952-10,191 48 | 1952-11,172 49 | 1952-12,194 50 | 1953-01,196 51 | 1953-02,196 52 | 1953-03,236 53 | 1953-04,235 54 | 1953-05,229 55 | 1953-06,243 56 | 1953-07,264 57 | 1953-08,272 58 | 1953-09,237 59 | 1953-10,211 60 | 1953-11,180 61 | 1953-12,201 62 | 1954-01,204 63 | 1954-02,188 64 | 1954-03,235 65 | 1954-04,227 66 | 1954-05,234 67 | 1954-06,264 68 | 1954-07,302 69 | 1954-08,293 70 | 1954-09,259 71 | 1954-10,229 72 | 1954-11,203 73 | 1954-12,229 74 | 1955-01,242 75 | 1955-02,233 76 | 1955-03,267 77 | 1955-04,269 78 | 1955-05,270 79 | 1955-06,315 80 | 1955-07,364 81 | 1955-08,347 82 | 1955-09,312 83 | 1955-10,274 84 | 1955-11,237 85 | 1955-12,278 86 | 1956-01,284 87 | 1956-02,277 88 | 1956-03,317 89 | 1956-04,313 90 | 1956-05,318 91 | 1956-06,374 92 | 1956-07,413 93 | 1956-08,405 94 | 1956-09,355 95 | 1956-10,306 96 | 1956-11,271 97 | 1956-12,306 98 | 1957-01,315 99 | 1957-02,301 100 | 1957-03,356 101 | 1957-04,348 102 | 1957-05,355 103 | 1957-06,422 104 | 1957-07,465 105 | 1957-08,467 106 | 1957-09,404 107 | 1957-10,347 108 | 1957-11,305 109 | 1957-12,336 110 | 1958-01,340 111 | 1958-02,318 112 | 1958-03,362 113 | 1958-04,348 114 | 1958-05,363 115 | 1958-06,435 116 | 1958-07,491 117 | 1958-08,505 118 | 1958-09,404 119 | 1958-10,359 120 | 1958-11,310 121 | 1958-12,337 122 | 1959-01,360 123 | 1959-02,342 124 | 1959-03,406 125 | 1959-04,396 126 | 1959-05,420 127 | 1959-06,472 128 | 1959-07,548 129 | 1959-08,559 130 | 1959-09,463 131 | 1959-10,407 132 | 1959-11,362 133 | 1959-12,405 134 | 1960-01,417 135 | 1960-02,391 136 | 1960-03,419 137 | 1960-04,461 138 | 1960-05,472 139 | 1960-06,535 140 | 1960-07,622 141 | 1960-08,606 142 | 1960-09,508 143 | 1960-10,461 144 | 1960-11,390 145 | 1960-12,432 146 | -------------------------------------------------------------------------------- /ThymeBoost/Datasets/Sales_and_Marketing.csv: -------------------------------------------------------------------------------- 1 | Time Period,Sales,Marketing Expense 2 | 2011-01-01,397,486.64 3 | 2011-02-01,400,501.8 4 | 2011-03-01,498,437.09 5 | 2011-04-01,536,565.16 6 | 2011-05-01,596,744.15 7 | 2011-06-01,591,548.74 8 | 2011-07-01,651,650.21 9 | 2011-08-01,654,777.51 10 | 2011-09-01,509,547.11 11 | 2011-10-01,437,382.81 12 | 2011-11-01,406,551.56 13 | 2011-12-01,470,401.69 14 | 2012-01-01,428,370.97 15 | 2012-02-01,423,318.39 16 | 2012-03-01,507,477.39 17 | 2012-04-01,536,418.66 18 | 2012-05-01,610,429.68 19 | 2012-06-01,609,713.24 20 | 2012-07-01,687,658.22 21 | 2012-08-01,707,800.52 22 | 2012-09-01,509,640.45 23 | 2012-10-01,452,606.49 24 | 2012-11-01,412,426.88 25 | 2012-12-01,472,513.48 26 | 2013-01-01,454,300.29 27 | 2013-02-01,455,330.84 28 | 2013-03-01,568,444.04 29 | 2013-04-01,610,628.82 30 | 2013-05-01,706,620.36 31 | 2013-06-01,661,682.6 32 | 2013-07-01,767,684.64 33 | 2013-08-01,783,748.47 34 | 2013-09-01,583,668.46 35 | 2013-10-01,513,499.31 36 | 2013-11-01,481,401.92 37 | 2013-12-01,567,605.06 38 | 2014-01-01,525,429.73 39 | 2014-02-01,520,602.86 40 | 2014-03-01,587,596.15 41 | 2014-04-01,710,619.39 42 | 2014-05-01,793,758.31 43 | 2014-06-01,749,980.16 44 | 2014-07-01,871,905.1 45 | 2014-08-01,848,784.62 46 | 2014-09-01,640,718.98 47 | 2014-10-01,581,570.3 48 | 2014-11-01,519,527.6 49 | 2014-12-01,605,559.75 50 | -------------------------------------------------------------------------------- /ThymeBoost/Datasets/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | -------------------------------------------------------------------------------- /ThymeBoost/Datasets/examples.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pandas as pd 3 | from pathlib import Path 4 | 5 | 6 | 7 | 8 | def get_data(dataset_name): 9 | file_path = str(Path.cwd().resolve()) 10 | df = pd.read_csv(fr"{file_path}\ThymeBoost\Datasets\{dataset_name}.csv") 11 | return df 12 | -------------------------------------------------------------------------------- /ThymeBoost/Forecaster.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This will become the autoforecaster which will utilize autofit and then fit a regression to combine forecasts 4 | """ 5 | 6 | import numpy as np 7 | import pandas as pd 8 | from ThymeBoost import ThymeBoost as tb 9 | 10 | 11 | class AutoForecast(tb.ThymeBoost): 12 | def __init__(self): 13 | super().__init__ 14 | self.verbose=0 15 | 16 | def forecast(self, 17 | y, 18 | seasonal_period, 19 | forecast_horizon, 20 | ): 21 | output = self.autofit(y.values, 22 | seasonal_period=seasonal_period, 23 | optimization_type='grid_search', 24 | optimization_strategy='holdout', 25 | optimization_steps=3, 26 | lag=50, 27 | optimization_metric='mse', 28 | test_set='all', 29 | ) 30 | test_results = self.optimizer.opt_results 31 | model = [] 32 | predictions = [] 33 | actuals = [] 34 | for key, val in test_results.items(): 35 | for key_2, val_2 in val.items(): 36 | n_preds = len(val_2['predictions']) 37 | model.append([str(val_2['params'])]*n_preds) 38 | predictions.append(val_2['predictions']) 39 | actuals.append(val_2['actuals']) 40 | model = [item for sublist in model for item in sublist] 41 | actuals = pd.concat(actuals) 42 | predictions = pd.concat(predictions) 43 | smart_ensemble = actuals.to_frame() 44 | smart_ensemble.columns = ['actuals'] 45 | smart_ensemble['predictions'] = predictions 46 | smart_ensemble['model'] = model 47 | pivot_smart_ensemble = smart_ensemble.pivot( 48 | values='predictions', 49 | columns=['model']) 50 | pivot_smart_ensemble['actuals'] = smart_ensemble['actuals'].values[:len(pivot_smart_ensemble)] 51 | y_ = pivot_smart_ensemble['actuals'] 52 | X = pivot_smart_ensemble.drop('actuals', axis=1).values 53 | import statsmodels.api as sm 54 | ols = sm.OLS(y_, X) 55 | fitted = ols.fit() 56 | fitted.summary() 57 | predicted = fitted.predict(X) 58 | output = self.ensemble(y.values, 59 | trend_estimator=['linear', 'croston'], 60 | fit_type=['global'], 61 | seasonal_estimator=['fourier'], 62 | alpha=[.2], 63 | seasonal_period=[0, 24], 64 | additive=[True, False],) 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /ThymeBoost/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/CostFunctions.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/CostFunctions.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/FitExogenous.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/FitExogenous.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/FitSeasonality.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/FitSeasonality.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/FitTrend.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/FitTrend.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/FitTrendTest.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/FitTrendTest.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/SplitProposals.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/SplitProposals.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/ThymeBoost.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/ThymeBoost.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/ThymeBoostOptimizer.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/ThymeBoostOptimizer.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/_booster.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/_booster.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/booster.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/booster.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/cost_functions.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/cost_functions.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/ensemble.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/ensemble.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/fit_seasonality_test.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/fit_seasonality_test.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/optimizer.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/optimizer.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/param_iterator.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/param_iterator.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/predict.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/predict.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/predict_functions.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/predict_functions.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/skylasso.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/skylasso.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/__pycache__/split_proposals.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/__pycache__/split_proposals.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/cost_functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import numpy as np 4 | from scipy.stats import entropy 5 | 6 | 7 | def get_split_cost(time_series, split1, split2, split_cost): 8 | """ 9 | Calculate the cost for the given split point using binary segmentation. 10 | 11 | Parameters 12 | ---------- 13 | time_series : np.array 14 | The input time series. 15 | split1 : np.array 16 | The first split of the binary segmentation. 17 | split2 : np.array 18 | The second split of the binary segmentation.. 19 | split_cost : str 20 | The metric to use for calculating the cost. 21 | 22 | Raises 23 | ------ 24 | ValueError 25 | If the cost metric provided does not exist this error will be raised. 26 | 27 | Returns 28 | ------- 29 | cost : float 30 | The cost for the given split. 31 | 32 | """ 33 | if split_cost == 'mse': 34 | cost = np.mean((time_series - np.append(split1, split2))**2) 35 | elif split_cost == 'mae': 36 | cost = np.mean(np.abs(time_series - np.append(split1, split2))) 37 | elif split_cost == 'entropy': 38 | residuals = time_series - np.append(split1, split2) 39 | entropy_safe_residuals = residuals + min(residuals) + 1 40 | cost = entropy(entropy_safe_residuals) 41 | else: 42 | raise ValueError('That split cost is not recognized') 43 | return cost 44 | 45 | 46 | def calc_cost(time_series, prediction, c, regularization, global_cost): 47 | n = len(time_series) 48 | if global_cost == 'maic': 49 | cost = 2*(c**regularization) + \ 50 | n*np.log(np.sum((time_series - prediction)**2)/n) 51 | elif global_cost == 'maicc': 52 | cost = (2*c**2 + 2*c)/max(1, (n-c-1)) + 2*(c**regularization) + \ 53 | n*np.log(np.sum((time_series - prediction)**2)/n) 54 | elif global_cost == 'mbic': 55 | cost = n*np.log(np.sum((time_series - prediction)**2)/n) + \ 56 | (c**regularization) * np.log(n) 57 | elif global_cost == 'mse': 58 | cost = np.mean((time_series - prediction)**2) 59 | else: 60 | raise ValueError('That global cost is not recognized') 61 | if np.isinf(cost): 62 | cost = 0 63 | return cost 64 | 65 | 66 | def calc_smape(actuals, predicted): 67 | return 100 * (2 * np.sum(np.abs(predicted - actuals)) / np.sum((np.abs(actuals) + np.abs(predicted)))) 68 | 69 | 70 | def calc_mape(actuals, predicted, epsilon=.00000001): 71 | return np.sum(np.abs(predicted - actuals)) / (epsilon + np.sum((np.abs(actuals)))) 72 | 73 | 74 | def calc_mse(actuals, predicted): 75 | return np.mean((predicted - actuals)**2) 76 | 77 | 78 | def calc_mae(actuals, predicted): 79 | return np.mean(np.abs(predicted - actuals)) 80 | 81 | 82 | def calc_entropy(actuals, predicted): 83 | residuals = predicted - actuals 84 | #Don't remember what this is doing 85 | entropy_safe_residuals = residuals + min(residuals) + 1 86 | return entropy(entropy_safe_residuals) 87 | -------------------------------------------------------------------------------- /ThymeBoost/ensemble.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import traceback 4 | import itertools as it 5 | import pandas as pd 6 | from tqdm import tqdm 7 | from ThymeBoost.param_iterator import ParamIterator 8 | 9 | 10 | class Ensemble(ParamIterator): 11 | __framework__ = 'ensemble' 12 | 13 | def __init__(self, 14 | model_object, 15 | y, 16 | verbose=0, 17 | **kwargs 18 | ): 19 | self.model_object = model_object 20 | self.y = y 21 | self.verbose = verbose 22 | self.kwargs = kwargs 23 | self.settings_keys = self.kwargs.keys() 24 | 25 | def get_space(self): 26 | ensemble_space = list(it.product(*self.kwargs.values())) 27 | run_settings = [] 28 | for params in ensemble_space: 29 | run_settings.append(dict(zip(self.settings_keys, params))) 30 | cleaned_space = self.sanitize_params(run_settings) 31 | return cleaned_space 32 | 33 | def ensemble_fit(self): 34 | parameters = self.get_space() 35 | ensemble_parameters = [] 36 | if self.verbose: 37 | param_iters = tqdm(parameters) 38 | else: 39 | param_iters = parameters 40 | self.outputs = [] 41 | self.params = [] 42 | for run_settings in param_iters: 43 | y_copy = self.y.copy(deep=True) 44 | try: 45 | output = self.model_object.fit(y_copy, **run_settings) 46 | #key = ','.join(map(str, run_settings.values())) 47 | self.outputs.append(output) 48 | self.params.append(run_settings) 49 | ensemble_parameters.append(self.model_object.booster_obj) 50 | except Exception as e: 51 | print(f'{e} Error running settings: {run_settings}') 52 | traceback.print_exc() 53 | output = pd.concat(self.outputs) 54 | output = output.groupby(by=output.index).mean() 55 | return output, ensemble_parameters 56 | -------------------------------------------------------------------------------- /ThymeBoost/exogenous_models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | -------------------------------------------------------------------------------- /ThymeBoost/exogenous_models/__pycache__/decision_tree_exogenous.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/exogenous_models/__pycache__/decision_tree_exogenous.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/exogenous_models/__pycache__/exogenous_base_class.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/exogenous_models/__pycache__/exogenous_base_class.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/exogenous_models/__pycache__/glm_exogenous.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/exogenous_models/__pycache__/glm_exogenous.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/exogenous_models/__pycache__/ols_exogenous.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/exogenous_models/__pycache__/ols_exogenous.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/exogenous_models/decision_tree_exogenous.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import numpy as np 4 | import pandas as pd 5 | from sklearn.tree import DecisionTreeRegressor 6 | from ThymeBoost.exogenous_models.exogenous_base_class import ExogenousBaseModel 7 | 8 | 9 | class DecisionTree(ExogenousBaseModel): 10 | model = 'decision_tree' 11 | 12 | def __init__(self): 13 | self.model_obj = None 14 | self.fitted = None 15 | 16 | def fit(self, y, X, **kwargs): 17 | tree_depth = kwargs['tree_depth'] 18 | exo_model = DecisionTreeRegressor(max_depth=tree_depth) 19 | self.model_obj = exo_model.fit(X, y) 20 | self.fitted = self.model_obj.predict(X) 21 | #exo_impact = (exo_model.params, fitted_model.cov_params()) 22 | return self.fitted 23 | 24 | def predict(self, future_exogenous): 25 | if isinstance(future_exogenous, pd.DataFrame): 26 | future_exogenous = future_exogenous.to_numpy() 27 | return self.model_obj.predict(future_exogenous) 28 | -------------------------------------------------------------------------------- /ThymeBoost/exogenous_models/exogenous_base_class.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from abc import ABC, abstractmethod 4 | import numpy as np 5 | import pandas as pd 6 | 7 | 8 | class ExogenousBaseModel(ABC): 9 | """ 10 | Exogenous Abstract Base Class. 11 | """ 12 | model = None 13 | 14 | @abstractmethod 15 | def __init__(self): 16 | self.fitted = None 17 | pass 18 | 19 | def __str__(cls): 20 | return f'{cls.model} model' 21 | 22 | @abstractmethod 23 | def fit(self, y, **kwargs): 24 | """ 25 | Fit the exogenous component in the boosting loop. 26 | 27 | Parameters 28 | ---------- 29 | time_series : TYPE 30 | DESCRIPTION. 31 | **kwargs : TYPE 32 | DESCRIPTION. 33 | 34 | Returns 35 | ------- 36 | None. 37 | 38 | """ 39 | pass 40 | 41 | @abstractmethod 42 | def predict(self, future_exogenous, forecast_horizon): 43 | pass 44 | 45 | def __add__(self, exo_object): 46 | """ 47 | Add two exo obj together, useful for ensembling or just quick updating of exo components. 48 | 49 | Parameters 50 | ---------- 51 | exo_object : TYPE 52 | DESCRIPTION. 53 | 54 | Returns 55 | ------- 56 | TYPE 57 | DESCRIPTION. 58 | 59 | """ 60 | return self.fitted + exo_object.fitted 61 | 62 | def __mul__(self, exo_object): 63 | return self.fitted * exo_object.fitted 64 | 65 | def __div__(self, exo_object): 66 | return self.fitted / exo_object.fitted 67 | 68 | def __sub__(self, exo_object): 69 | return self.fitted - exo_object.fitted 70 | 71 | def append(self, exo_object): 72 | return np.append(self.fitted, exo_object.fitted) 73 | 74 | def to_series(self, array): 75 | return pd.Series(array) 76 | -------------------------------------------------------------------------------- /ThymeBoost/exogenous_models/glm_exogenous.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import statsmodels.api as sm 4 | import pandas as pd 5 | from ThymeBoost.exogenous_models.exogenous_base_class import ExogenousBaseModel 6 | 7 | class GLM(ExogenousBaseModel): 8 | model = 'glm' 9 | 10 | def __init__(self): 11 | self.model_obj = None 12 | self.fitted = None 13 | 14 | def fit(self, y, X, **kwargs): 15 | exo_model = sm.GLM(y, X) 16 | self.model_obj = exo_model.fit() 17 | self.fitted = self.model_obj.predict(X) 18 | #exo_impact = (exo_model.params, fitted_model.cov_params()) 19 | return self.fitted 20 | 21 | def predict(self, future_exogenous): 22 | if isinstance(future_exogenous, pd.DataFrame): 23 | future_exogenous = future_exogenous.to_numpy() 24 | return self.model_obj.predict(future_exogenous) -------------------------------------------------------------------------------- /ThymeBoost/exogenous_models/ols_exogenous.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import statsmodels.api as sm 3 | import numpy as np 4 | import pandas as pd 5 | from ThymeBoost.exogenous_models.exogenous_base_class import ExogenousBaseModel 6 | 7 | 8 | class OLS(ExogenousBaseModel): 9 | model = 'ols' 10 | 11 | def __init__(self): 12 | self.model_obj = None 13 | self.fitted = None 14 | 15 | def fit(self, y, X, **kwargs): 16 | exo_model = sm.OLS(y, X) 17 | self.model_obj = exo_model.fit() 18 | self.fitted = self.model_obj.predict(X) 19 | #exo_impact = (exo_model.params, fitted_model.cov_params()) 20 | return self.fitted 21 | 22 | def predict(self, future_exogenous): 23 | if isinstance(future_exogenous, pd.DataFrame): 24 | future_exogenous = future_exogenous.to_numpy() 25 | return self.model_obj.predict(future_exogenous) 26 | -------------------------------------------------------------------------------- /ThymeBoost/fit_components/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | -------------------------------------------------------------------------------- /ThymeBoost/fit_components/__pycache__/fit_exogenous.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/fit_components/__pycache__/fit_exogenous.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/fit_components/__pycache__/fit_seasonality.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/fit_components/__pycache__/fit_seasonality.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/fit_components/__pycache__/fit_seasonality_test.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/fit_components/__pycache__/fit_seasonality_test.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/fit_components/__pycache__/fit_trend.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/fit_components/__pycache__/fit_trend.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/fit_components/__pycache__/fit_trend_test.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/fit_components/__pycache__/fit_trend_test.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/fit_components/fit_exogenous.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | import pandas as pd 4 | from ThymeBoost.exogenous_models import (ols_exogenous, 5 | decision_tree_exogenous, 6 | glm_exogenous) 7 | 8 | 9 | class FitExogenous: 10 | def __init__(self, 11 | exo_estimator='ols', 12 | exogenous_lr=1, 13 | **kwargs): 14 | self.exo_estimator = exo_estimator 15 | self.exogenous_lr = exogenous_lr 16 | self.kwargs = kwargs 17 | return 18 | 19 | def set_estimator(self, trend_estimator): 20 | if trend_estimator == 'ols': 21 | fit_obj = ols_exogenous.OLS 22 | elif trend_estimator == 'glm': 23 | fit_obj = glm_exogenous.GLM 24 | elif trend_estimator == 'decision_tree': 25 | fit_obj = decision_tree_exogenous.DecisionTree 26 | else: 27 | raise NotImplementedError('That Exo estimation is not availale yet, add it to the road map!') 28 | return fit_obj 29 | 30 | def fit_exogenous_component(self, time_residual, exogenous): 31 | self.model_obj = self.set_estimator(self.exo_estimator)() 32 | if isinstance(exogenous, pd.DataFrame): 33 | exogenous = exogenous.to_numpy() 34 | exo_fitted = self.model_obj.fit(time_residual, exogenous, **self.kwargs) 35 | return self.exogenous_lr*np.array(exo_fitted) 36 | -------------------------------------------------------------------------------- /ThymeBoost/fit_components/fit_seasonality.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | from ThymeBoost.seasonality_models import (classic_seasonality, fourier_seasonality, 4 | naive_seasonality) 5 | 6 | 7 | class FitSeasonality: 8 | """Approximates the seasonal component 9 | """ 10 | 11 | def __init__(self, seasonal_estimator, 12 | seasonal_period, 13 | seasonality_lr, 14 | seasonality_weights, 15 | additive, 16 | normalize_seasonality, 17 | **kwargs): 18 | self.seasonal_estimator = seasonal_estimator 19 | self.seasonal_period = seasonal_period 20 | self.seasonality_lr = seasonality_lr 21 | self.additive = additive 22 | self.seasonality_weights = seasonality_weights 23 | self.normalize_seasonality = normalize_seasonality 24 | self.kwargs = kwargs 25 | 26 | @staticmethod 27 | def set_estimator(seasonal_estimator): 28 | if seasonal_estimator == 'fourier': 29 | seasonal_obj = fourier_seasonality.FourierSeasonalityModel 30 | elif seasonal_estimator == 'classic': 31 | seasonal_obj = classic_seasonality.ClassicSeasonalityModel 32 | elif seasonal_estimator == 'naive': 33 | seasonal_obj = naive_seasonality.NaiveSeasonalityModel 34 | else: 35 | raise NotImplementedError('That seasonal estimation is not availale yet, add it to the road map!') 36 | return seasonal_obj 37 | 38 | def fit_seasonal_component(self, detrended): 39 | data_len = len(detrended) 40 | if not self.seasonal_period: 41 | seasonality = np.zeros(data_len) 42 | self.model_params = None 43 | self.model_obj = None 44 | else: 45 | seasonal_class = FitSeasonality.set_estimator(self.seasonal_estimator) 46 | self.model_obj = seasonal_class(self.seasonal_period, 47 | self.normalize_seasonality, 48 | self.seasonality_weights) 49 | seasonality = self.model_obj.fit(detrended, 50 | seasonality_lr=self.seasonality_lr, 51 | **self.kwargs) 52 | self.model_params = self.model_obj.model_params 53 | return seasonality 54 | -------------------------------------------------------------------------------- /ThymeBoost/fit_components/fit_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | from scipy.signal import savgol_filter 4 | from ThymeBoost.cost_functions import get_split_cost 5 | from ThymeBoost.split_proposals import SplitProposals 6 | from ThymeBoost.trend_models import (linear_trend, mean_trend, median_trend, 7 | loess_trend, ransac_trend, ewm_trend, 8 | ets_trend, arima_trend, moving_average_trend, 9 | zero_trend, svr_trend, naive_trend, croston_trend, 10 | fast_arima_trend, fast_ets_trend, fast_ses_trend, 11 | lbf_trend, fast_theta_trend, fast_ces_trend, 12 | fast_imapa_trend, decision_tree_trend) 13 | 14 | 15 | class FitTrend: 16 | """Approximates the trend component 17 | 18 | Parameters 19 | ---------- 20 | poly : int 21 | Polynomial expansion for linear models. 22 | 23 | trend_estimator : str 24 | The estimator to use to approximate trend. 25 | 26 | fit_type : str 27 | Whether a 'global' or 'local' fit is used. This parameter 28 | should be set to 'global' for loess and ewm. 29 | 30 | given_splits : list 31 | Splits to use when using fit_type='local'. 32 | 33 | exclude_splits : list 34 | exclude these index when considering splits for 35 | fit_type='local'. Must be idx not datetimeindex if 36 | using a Pandas Series. 37 | 38 | min_sample_pct : float 39 | Percentage of samples required to consider a split. 40 | Must be 00. 52 | 53 | split_cost : str 54 | What cost function to use when selecting the split, selections are 55 | 'mse' or 'mae'. 56 | 57 | trend_lr : float 58 | Applies a learning rate in accordance to standard gradient boosting 59 | such that the trend = trend * trend_lr at each iteration. 60 | 61 | forecast_horizon : int 62 | Number of steps to take in forecasting out-of-sample. 63 | 64 | window_size : int 65 | How many samples are taken into account for sliding window methods 66 | such as loess and ewm. 67 | 68 | smoothed : boolean 69 | Whether to smooth the resulting trend or not, by default not too 70 | much smoothing is applied just enough to smooth the kinks. 71 | 72 | RETURNS 73 | ---------- 74 | numpy array : the trend component 75 | numpy array : the predicted trend component 76 | """ 77 | 78 | def __init__(self, 79 | trend_estimator, 80 | fit_type, 81 | given_splits, 82 | exclude_splits, 83 | min_sample_pct, 84 | n_split_proposals, 85 | approximate_splits, 86 | split_cost, 87 | trend_lr, 88 | time_series_index, 89 | smoothed, 90 | connectivity_constraint, 91 | split_strategy, 92 | **kwargs 93 | ): 94 | self.trend_estimator = trend_estimator.lower() 95 | self.fit_type = fit_type 96 | self.given_splits = given_splits 97 | self.exclude_splits = exclude_splits 98 | self.min_sample_pct = min_sample_pct 99 | self.n_split_proposals = n_split_proposals 100 | self.approximate_splits = approximate_splits 101 | self.split_cost = split_cost 102 | self.trend_lr = trend_lr 103 | self.time_series_index = time_series_index 104 | self.smoothed = smoothed 105 | self.connectivity_constraint = connectivity_constraint 106 | self.split = None 107 | self.split_strategy = split_strategy 108 | self.kwargs = kwargs 109 | 110 | @staticmethod 111 | def set_estimator(trend_estimator): 112 | """ 113 | 114 | Parameters 115 | ---------- 116 | trend_estimator : TYPE 117 | DESCRIPTION. 118 | 119 | Raises 120 | ------ 121 | NotImplementedError 122 | DESCRIPTION. 123 | 124 | Returns 125 | ------- 126 | fit_obj : TYPE 127 | DESCRIPTION. 128 | 129 | """ 130 | if trend_estimator == 'mean': 131 | fit_obj = mean_trend.MeanModel 132 | elif trend_estimator == 'median': 133 | fit_obj = median_trend.MedianModel 134 | elif trend_estimator == 'linear': 135 | fit_obj = linear_trend.LinearModel 136 | elif trend_estimator in ('ses', 'des', 'damped_des'): 137 | fit_obj = ets_trend.EtsModel 138 | elif trend_estimator == 'arima': 139 | fit_obj = arima_trend.ArimaModel 140 | elif trend_estimator == 'loess': 141 | fit_obj = loess_trend.LoessModel 142 | elif trend_estimator == 'ewm': 143 | fit_obj = ewm_trend.EwmModel 144 | elif trend_estimator == 'ransac': 145 | fit_obj = ransac_trend.RansacModel 146 | elif trend_estimator == 'moving_average': 147 | fit_obj = moving_average_trend.MovingAverageModel 148 | elif trend_estimator == 'zero': 149 | fit_obj = zero_trend.ZeroModel 150 | elif trend_estimator == 'svr': 151 | fit_obj = svr_trend.SvrModel 152 | elif trend_estimator == 'naive': 153 | fit_obj = naive_trend.NaiveModel 154 | elif trend_estimator == 'croston': 155 | fit_obj = croston_trend.CrostonModel 156 | elif trend_estimator == 'fast_arima': 157 | fit_obj = fast_arima_trend.FastArimaModel 158 | elif trend_estimator == 'fast_ets': 159 | fit_obj = fast_ets_trend.FastETSModel 160 | elif trend_estimator == 'lbf': 161 | fit_obj = lbf_trend.LbfModel 162 | elif trend_estimator == 'fast_theta': 163 | fit_obj = fast_theta_trend.FastThetaModel 164 | elif trend_estimator == 'fast_ses': 165 | fit_obj = fast_ses_trend.FastSESModel 166 | elif trend_estimator == 'fast_ces': 167 | fit_obj = fast_ces_trend.FastCESModel 168 | elif trend_estimator == 'fast_imapa': 169 | fit_obj = fast_imapa_trend.FastIMAPAModel 170 | elif trend_estimator == 'decision_tree': 171 | fit_obj = decision_tree_trend.DecisionTreeModel 172 | else: 173 | raise NotImplementedError('That trend estimation is not available yet, add it to the road map!') 174 | return fit_obj 175 | 176 | def fit_trend_component(self, time_series): 177 | fit_obj = self.set_estimator(self.trend_estimator) 178 | if self.fit_type == 'local': 179 | split = None 180 | proposals = SplitProposals(given_splits=self.given_splits, 181 | exclude_splits=self.exclude_splits, 182 | min_sample_pct=self.min_sample_pct, 183 | n_split_proposals=self.n_split_proposals, 184 | split_strategy=self.split_strategy, 185 | approximate_splits=self.approximate_splits) 186 | proposals = proposals.get_split_proposals(time_series) 187 | for index, i in enumerate(proposals): 188 | split_1_obj = fit_obj() 189 | fitted_1_split = split_1_obj.fit(time_series[:i], 190 | fit_constant=True, 191 | bias=0, 192 | model=self.trend_estimator, 193 | **self.kwargs) 194 | split_2_obj = fit_obj() 195 | fitted_2_split = split_2_obj.fit(time_series[i:], 196 | bias=float(fitted_1_split[-1]), 197 | fit_constant=(not self.connectivity_constraint), 198 | model=self.trend_estimator, 199 | **self.kwargs) 200 | iteration_cost = get_split_cost(time_series, 201 | fitted_1_split, 202 | fitted_2_split, 203 | self.split_cost) 204 | if index == 0: 205 | cost = iteration_cost 206 | if iteration_cost <= cost: 207 | split = self.time_series_index[i] 208 | cost = iteration_cost 209 | fitted = split_1_obj.append(split_2_obj) 210 | self.model_obj = split_2_obj 211 | self.model_params = split_2_obj.model_params 212 | self.split = split 213 | if self.split is None: 214 | raise ValueError('No split found, series length my be too small or some error occurred') 215 | if self.smoothed: 216 | fitted = savgol_filter(fitted, self.kwargs['window_size'], self.kwargs['poly']) 217 | elif self.fit_type == 'global': 218 | global_obj = fit_obj() 219 | fitted = global_obj.fit(time_series, 220 | bias=0, 221 | fit_constant=True, 222 | model=self.trend_estimator, 223 | **self.kwargs) 224 | self.split = None 225 | self.model_params = global_obj.model_params 226 | self.model_obj = global_obj 227 | else: 228 | raise NotImplementedError('Trend estimation must be local or global') 229 | return fitted * self.trend_lr 230 | -------------------------------------------------------------------------------- /ThymeBoost/fitter/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | -------------------------------------------------------------------------------- /ThymeBoost/fitter/__pycache__/booster.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/fitter/__pycache__/booster.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/fitter/__pycache__/decompose.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/fitter/__pycache__/decompose.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/fitter/booster.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import numpy as np 5 | import pandas as pd 6 | import copy 7 | from ThymeBoost.utils.get_complexity import get_complexity 8 | from ThymeBoost.cost_functions import calc_cost 9 | from ThymeBoost.fitter.decompose import Decompose 10 | 11 | 12 | class booster(Decompose): 13 | 14 | def __init__(self, 15 | time_series, 16 | given_splits, 17 | verbose, 18 | n_split_proposals, 19 | approximate_splits, 20 | exclude_splits, 21 | cost_penalty, 22 | normalize_seasonality, 23 | regularization, 24 | n_rounds, 25 | smoothed_trend, 26 | additive, 27 | split_strategy, 28 | **kwargs): 29 | time_series = pd.Series(time_series).copy(deep=True) 30 | self.time_series_index = time_series.index 31 | self.time_series = time_series.values 32 | self.boosted_data = self.time_series 33 | self.kwargs = kwargs 34 | self.boosting_params = copy.deepcopy(self.kwargs) 35 | self.verbose = verbose 36 | self.n_split_proposals = n_split_proposals 37 | self.approximate_splits = approximate_splits 38 | self.exclude_splits = exclude_splits 39 | self.given_splits = given_splits 40 | self.cost_penalty = cost_penalty 41 | self.normalize_seasonality = normalize_seasonality 42 | self.regularization = regularization 43 | self.n_rounds = n_rounds 44 | self.smoothed_trend = smoothed_trend 45 | self.additive = additive 46 | self.split_strategy = split_strategy 47 | 48 | def __add__(self, booster_obj): 49 | self.i += booster_obj.i 50 | self.trend_objs += booster_obj.trend_objs 51 | self.seasonal_objs += booster_obj.seasonal_objs 52 | self.exo_objs += booster_obj.exo_objs 53 | self.trend_pred_params += booster_obj.trend_pred_params 54 | self.seasonal_pred_params += booster_obj.seasonal_pred_params 55 | self.exo_pred_params += booster_obj.exo_pred_params 56 | self.trends += booster_obj.trends 57 | self.trend_strengths += booster_obj.trend_strengths 58 | self.seasonalities += booster_obj.seasonalities 59 | self.errors += booster_obj.errors 60 | self.fitted_exogenous += booster_obj.fitted_exogenous 61 | self.additive = booster_obj.additive 62 | return self 63 | 64 | def initialize_booster_values(self): 65 | self.split = None 66 | self.i = -1 67 | self.trend_objs = [] 68 | self.seasonal_objs = [] 69 | self.exo_objs = [] 70 | self.trend_pred_params = [] 71 | self.seasonal_pred_params = [] 72 | self.exo_pred_params = [] 73 | self.trends = [] 74 | self.seasonalities = [] 75 | self.errors = [] 76 | self.fitted_exogenous = [] 77 | self.exo_class = None 78 | self.trend_strengths = [] 79 | 80 | def update_params(self, 81 | total_trend, 82 | total_seasonal, 83 | total_exo): 84 | self.seasonal_pred_params.append(self.seasonal_obj.model_params) 85 | self.seasonal_objs.append(self.seasonal_obj) 86 | self.trend_pred_params.append(self.trend_obj.model_params) 87 | self.trend_objs.append(self.trend_obj) 88 | self.exo_objs.append(self.exo_class) 89 | self.trend_strengths.append(self.trend_strength) 90 | self.total_trend = total_trend 91 | self.total_seasonalities = total_seasonal 92 | if self.boosting_params['exogenous'] is None: 93 | self.total_fitted_exogenous = None 94 | else: 95 | self.total_fitted_exogenous = total_exo 96 | 97 | @staticmethod 98 | def calc_trend_strength(resids, deseasonalized): 99 | return max(0, 1-(np.var(resids)/np.var(deseasonalized))) 100 | 101 | def boosting_log(self, round_cost): 102 | #quick printing 103 | #TODO replace with logging 104 | print(f'''{"*"*10} Round {self.i+1} {"*"*10}''') 105 | print(f'''Using Split: {self.split}''') 106 | if self.i == 0: 107 | print(f'''Fitting initial trend globally with trend model:''') 108 | else: 109 | print(f'''Fitting {self.trend_objs[-1].fit_type} with trend model:''') 110 | print(f'''{str(self.trend_objs[-1].model_obj)}''') 111 | print(f'''seasonal model:''') 112 | print(f'''{str(self.seasonal_objs[-1].model_obj)}''') 113 | if self.exo_class is not None: 114 | print(f'''exogenous model:''') 115 | print(f'''{str(self.exo_objs[-1].model_obj)}''') 116 | print(f'''cost: {round_cost}''') 117 | 118 | def boost(self): 119 | self.initialize_booster_values() 120 | __boost = True 121 | while __boost: 122 | self.i += 1 123 | if self.i == self.n_rounds: 124 | break 125 | round_results = self.additive_boost_round(self.i) 126 | current_prediction, total_trend, total_seasonal, total_exo = round_results 127 | resids = self.time_series - current_prediction 128 | self.trend_strength = booster.calc_trend_strength(resids, 129 | resids + total_trend) 130 | self.c = get_complexity(self.i, 131 | self.boosting_params['poly'], 132 | self.boosting_params['fit_type'], 133 | self.boosting_params['trend_estimator'], 134 | self.boosting_params['arima_order'], 135 | self.boosting_params['window_size'], 136 | self.time_series, 137 | self.boosting_params['fourier_order'], 138 | self.boosting_params['seasonal_period'], 139 | self.boosting_params['exogenous']) 140 | round_cost = calc_cost(self.time_series, 141 | current_prediction, 142 | self.c, 143 | self.regularization, 144 | self.boosting_params['global_cost']) 145 | if self.i == 0: 146 | self.cost = round_cost 147 | if (round_cost <= self.cost and self.n_rounds == -1) or self.i < self.n_rounds: 148 | if self.cost > 0: 149 | self.cost = round_cost - self.cost_penalty*round_cost 150 | else: 151 | EPS = .000000000000001 152 | self.cost = round_cost + self.cost_penalty*round_cost - EPS 153 | self.update_params(total_trend, 154 | total_seasonal, 155 | total_exo) 156 | if self.verbose: 157 | self.boosting_log(round_cost) 158 | else: 159 | assert self.i > 0, 'Boosting terminated before beginning' 160 | __boost = False 161 | if self.verbose: 162 | print(f'{"="*30}') 163 | print(f'Boosting Terminated \nUsing round {self.i}') 164 | break 165 | return self.total_trend, self.total_seasonalities, self.total_fitted_exogenous 166 | 167 | 168 | -------------------------------------------------------------------------------- /ThymeBoost/fitter/decompose.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import copy 4 | import pandas as pd 5 | import numpy as np 6 | from ThymeBoost.fit_components.fit_trend import FitTrend 7 | from ThymeBoost.fit_components.fit_seasonality import FitSeasonality 8 | from ThymeBoost.fit_components.fit_exogenous import FitExogenous 9 | 10 | 11 | class Decompose: 12 | 13 | def __init__(self, 14 | time_series, 15 | given_splits, 16 | verbose, 17 | n_split_proposals, 18 | approximate_splits, 19 | exclude_splits, 20 | cost_penalty, 21 | normalize_seasonality, 22 | regularization, 23 | n_rounds, 24 | smoothed_trend, 25 | additive, 26 | split_strategy, 27 | **kwargs): 28 | time_series = pd.Series(time_series) 29 | self.time_series_index = time_series.index 30 | self.time_series = time_series.values 31 | self.boosted_data = self.time_series 32 | self.kwargs = kwargs 33 | self.boosting_params = copy.deepcopy(self.kwargs) 34 | self.verbose = verbose 35 | self.n_split_proposals = n_split_proposals 36 | self.approximate_splits = approximate_splits 37 | self.exclude_splits = exclude_splits 38 | self.given_splits = given_splits 39 | self.cost_penalty = cost_penalty 40 | self.normalize_seasonality = normalize_seasonality 41 | self.regularization = regularization 42 | self.n_rounds = n_rounds 43 | self.smoothed_trend = smoothed_trend 44 | self.additive = additive 45 | self.split_strategy = split_strategy 46 | 47 | def update_iterated_features(self): 48 | self.boosting_params = {k: next(v) for k, v in self.kwargs.items()} 49 | 50 | def multiplicative_fit(self): 51 | raise ValueError('Multiplicative Seasonality is not enabled!') 52 | 53 | def get_init_trend_component(self, time_series): 54 | self.trend_obj = FitTrend(trend_estimator=next(self.boosting_params['init_trend']), 55 | fit_type='global', 56 | given_splits=self.given_splits, 57 | exclude_splits=self.exclude_splits, 58 | min_sample_pct=.01, 59 | poly=1, 60 | trend_weights=None, 61 | l2=None, 62 | n_split_proposals=self.n_split_proposals, 63 | approximate_splits=self.approximate_splits, 64 | split_cost='mse', 65 | trend_lr=1, 66 | time_series_index=self.time_series_index, 67 | smoothed=False, 68 | connectivity_constraint=True, 69 | split_strategy=self.split_strategy 70 | ) 71 | trend = self.trend_obj.fit_trend_component(time_series) 72 | self.trends.append(trend) 73 | self.split = self.trend_obj.split 74 | return trend 75 | 76 | def get_trend_component(self, time_series): 77 | self.trend_obj = FitTrend(given_splits=self.given_splits, 78 | exclude_splits=self.exclude_splits, 79 | approximate_splits=self.approximate_splits, 80 | time_series_index=self.time_series_index, 81 | smoothed=self.smoothed_trend, 82 | n_split_proposals=self.n_split_proposals, 83 | additive=self.additive, 84 | split_strategy=self.split_strategy, 85 | **self.boosting_params) 86 | trend = self.trend_obj.fit_trend_component(time_series) 87 | self.trends.append(trend) 88 | self.split = self.trend_obj.split 89 | return trend 90 | 91 | def get_seasonal_component(self, detrended): 92 | self.seasonal_obj = FitSeasonality(normalize_seasonality=self.normalize_seasonality, 93 | additive=self.additive, 94 | **self.boosting_params) 95 | seasonality = self.seasonal_obj.fit_seasonal_component(detrended) 96 | self.seasonalities.append(seasonality) 97 | return seasonality 98 | 99 | def get_exogenous_component(self, residual): 100 | self.exo_class = FitExogenous(self.boosting_params['exogenous_estimator'], 101 | **self.boosting_params) 102 | exo_fit = self.exo_class.fit_exogenous_component(self.boosted_data, 103 | self.boosting_params['exogenous'], 104 | ) 105 | self.fitted_exogenous.append(exo_fit) 106 | self.boosted_data = self.boosted_data - exo_fit 107 | return exo_fit 108 | 109 | def additive_boost_round(self, round_number): 110 | if round_number == 0: 111 | trend = self.get_init_trend_component(self.boosted_data) 112 | else: 113 | trend = self.get_trend_component(self.boosted_data) 114 | self.update_iterated_features() 115 | detrended = self.boosted_data - trend 116 | self.boosted_data = detrended 117 | seasonality = self.get_seasonal_component(detrended) 118 | self.boosted_data -= seasonality 119 | if self.boosting_params['exogenous'] is not None: 120 | self.get_exogenous_component(self.boosted_data) 121 | #self.errors.append(np.mean(np.abs(self.boosted_data))) 122 | total_trend = np.sum(self.trends, axis=0) 123 | total_seasonalities = np.sum(self.seasonalities, axis=0) 124 | total_exo = np.sum(self.fitted_exogenous, axis=0) 125 | current_prediction = (total_trend + 126 | total_seasonalities + 127 | total_exo) 128 | return current_prediction, total_trend, total_seasonalities, total_exo 129 | -------------------------------------------------------------------------------- /ThymeBoost/optimizer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | #TODO: fix exogenous looping. 3 | import types 4 | import traceback 5 | import itertools as it 6 | import pandas as pd 7 | import copy 8 | from tqdm import tqdm 9 | import numpy as np 10 | from ThymeBoost.cost_functions import calc_smape, calc_mape, calc_mse, calc_mae 11 | from ThymeBoost.param_iterator import ParamIterator 12 | import ThymeBoost 13 | 14 | 15 | class Optimizer(ParamIterator): 16 | __framework__ = 'optimizer' 17 | 18 | def __init__(self, 19 | model_object, 20 | y, 21 | optimization_type, 22 | optimization_strategy, 23 | optimization_steps, 24 | lag, 25 | optimization_metric, 26 | test_set, 27 | verbose, 28 | **kwargs 29 | ): 30 | self.verbose = verbose 31 | self.optimization_type = optimization_type 32 | self.optimization_strategy = optimization_strategy 33 | self.lag = lag 34 | self.optimization_metric = optimization_metric 35 | if self.optimization_strategy == 'holdout': 36 | optimization_steps = 1 37 | self.optimization_steps = optimization_steps 38 | self.y = y 39 | self.model_object = model_object 40 | self.test_set = test_set 41 | self.search_space = {'trend_estimator': ['mean', 'median', 'linear'], 42 | 'fit_type': ['local', 'global'], 43 | 'seasonal_period': [None] 44 | } 45 | self.search_space.update(kwargs) 46 | self.search_keys = self.search_space.keys() 47 | 48 | def set_optimization_metric(self): 49 | if self.optimization_metric == 'smape': 50 | self.optimization_metric = calc_smape 51 | if self.optimization_metric == 'mape': 52 | self.optimization_metric = calc_mape 53 | if self.optimization_metric == 'mse': 54 | self.optimization_metric = calc_mse 55 | if self.optimization_metric == 'mae': 56 | self.optimization_metric = calc_mae 57 | return 58 | 59 | def get_search_space(self): 60 | thymeboost_search_space = list(it.product(*self.search_space.values())) 61 | run_settings = [] 62 | for params in thymeboost_search_space: 63 | run_settings.append(dict(zip(self.search_keys, params))) 64 | cleaned_space = self.sanitize_params(run_settings) 65 | return cleaned_space 66 | 67 | @staticmethod 68 | def combiner_check(param_dict, wrap_values=True): 69 | ensemble_dict = {} 70 | if any(isinstance(v, types.FunctionType) 71 | for v in list(param_dict.values())): 72 | ensemble = True 73 | for k, v in param_dict.items(): 74 | if isinstance(v, types.FunctionType): 75 | ensemble_dict[k] = v 76 | param_dict[k] = v() 77 | else: 78 | if wrap_values: 79 | param_dict[k] = [v] 80 | else: 81 | ensemble = False 82 | return ensemble, ensemble_dict 83 | 84 | @staticmethod 85 | def exo_check(param_dict, ensemble): 86 | exo = False 87 | if ensemble: 88 | if param_dict['exogenous'][0] is not None: 89 | exo = True 90 | else: 91 | if param_dict['exogenous'] is not None: 92 | exo = True 93 | return exo 94 | 95 | def fit(self): 96 | #This needs to be refactored 97 | self.parameters = self.get_search_space() 98 | self.set_optimization_metric() 99 | results = {} 100 | for num_steps in range(1, self.optimization_steps + 1): 101 | y_copy = self.y.copy(deep=True) 102 | if self.optimization_strategy == 'cv': 103 | test_y = y_copy[-self.lag * num_steps + 1:] 104 | train_y = y_copy[:-self.lag * num_steps + 1] 105 | test_y = test_y[:self.lag] 106 | else: 107 | test_y = y_copy[-self.lag - num_steps + 1:] 108 | train_y = y_copy[:-self.lag - num_steps + 1] 109 | test_y = test_y[:self.lag] 110 | results[str(num_steps)] = {} 111 | if self.verbose: 112 | param_iters = tqdm(self.parameters) 113 | else: 114 | param_iters = self.parameters 115 | for settings in param_iters: 116 | try: 117 | run_settings = copy.deepcopy(settings) 118 | ensemble, ensemble_dict = Optimizer.combiner_check(run_settings) 119 | exo = Optimizer.exo_check(run_settings, ensemble) 120 | if exo: 121 | if ensemble: 122 | X_test = run_settings['exogenous'][0].loc[test_y.index] 123 | X_train = run_settings['exogenous'][0].iloc[:len(train_y), :] 124 | params = copy.deepcopy(run_settings) 125 | run_settings['exogenous'] = [X_train] 126 | output = self.model_object.ensemble(train_y, 127 | **run_settings) 128 | else: 129 | X_test = run_settings['exogenous'].loc[test_y.index] 130 | X_train = run_settings['exogenous'].iloc[:len(train_y), :] 131 | params = copy.deepcopy(run_settings) 132 | run_settings['exogenous'] = X_train 133 | output = self.model_object.fit(train_y, 134 | **run_settings) 135 | predicted_output = self.model_object.predict(output, 136 | self.lag, 137 | future_exogenous=X_test) 138 | run_settings.pop('exogenous') 139 | else: 140 | if ensemble: 141 | output = self.model_object.ensemble(train_y, 142 | **run_settings) 143 | else: 144 | output = self.model_object.fit(train_y, **run_settings) 145 | predicted_output = self.model_object.predict(output, 146 | self.lag) 147 | 148 | params = copy.deepcopy(run_settings) 149 | predicted = predicted_output['predictions'] 150 | if self.test_set == 'all': 151 | test_error = self.optimization_metric(actuals=test_y, 152 | predicted=predicted) 153 | elif self.test_set == 'last': 154 | test_error = self.optimization_metric(actuals=test_y.iloc[-1], 155 | predicted=predicted.iloc[-1]) 156 | key = ','.join(map(str, run_settings.values())) 157 | results[str(num_steps)][key] = {} 158 | results[str(num_steps)][key]['error'] = test_error 159 | params.update(ensemble_dict) 160 | results[str(num_steps)][key]['params'] = params 161 | results[str(num_steps)][key]['predictions'] = predicted 162 | results[str(num_steps)][key]['actuals'] = test_y 163 | except Exception as e: 164 | results[str(num_steps)][','.join(map(str, run_settings))] = np.inf 165 | if self.verbose: 166 | print(f'{e} Error running settings: {run_settings}') 167 | traceback.print_exc() 168 | return results 169 | 170 | def optimize(self): 171 | self.opt_results = self.fit() 172 | average_result = {} 173 | for key in self.opt_results['1'].keys(): 174 | summation = 0 175 | for step in self.opt_results.keys(): 176 | summation += self.opt_results[step][key]['error'] 177 | average_result[key] = summation / len(self.opt_results.keys()) 178 | average_result = pd.Series(average_result) 179 | average_result = average_result.sort_values() 180 | best_setting = average_result.index[0] 181 | self.run_settings = self.opt_results['1'][best_setting]['params'] 182 | self.cv_predictions = [] 183 | for k, v in self.opt_results.items(): 184 | self.cv_predictions.append(self.opt_results[k][best_setting]['predictions']) 185 | ensemble, _ = Optimizer.combiner_check(self.run_settings, wrap_values=False) 186 | if ensemble: 187 | output = self.model_object.ensemble(self.y, **self.run_settings) 188 | else: 189 | output = self.model_object.fit(self.y, **self.run_settings) 190 | if self.verbose: 191 | print(f'Optimal model configuration: {self.run_settings}') 192 | print(f'Params ensembled: {ensemble}') 193 | return output 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /ThymeBoost/param_iterator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | A base class which is inherited by both ensemble and optimize classes. 4 | Used to clean large parameter lists of illegal combinations 5 | """ 6 | import numpy as np 7 | 8 | 9 | class ParamIterator: 10 | """ 11 | The ensemble/optimizer base class 12 | """ 13 | 14 | def __init__(self): 15 | pass 16 | 17 | def param_check(self, params): 18 | """ 19 | Given a dict of params, check for illegal combinations 20 | 21 | Parameters 22 | ---------- 23 | params : dict 24 | A dictionary of params for one single thymeboost model. 25 | 26 | Returns 27 | ------- 28 | params : dict 29 | A dictionary with illegal values nullified. 30 | 31 | """ 32 | v = list(params.values()) 33 | k = list(params.keys()) 34 | exo = False 35 | if 'exogenous' in k: 36 | exogenous = params['exogenous'] 37 | params.pop('exogenous', None) 38 | v = list(params.values()) 39 | k = list(params.keys()) 40 | else: 41 | exogenous = None 42 | if 'ewm' not in v and 'ewm_alpha' in k: 43 | params['ewm_alpha'] = None 44 | if ('ses' not in v and 'des' not in v and 'damped_des' not in v and 'croston' not in v) and \ 45 | ('alpha' in k): 46 | params['alpha'] = None 47 | if ('des' not in v and 'damped_des' not in v) and \ 48 | ('beta' in k): 49 | params['beta'] = None 50 | if 'linear' not in v and 'trend_weights' in k: 51 | params['trend_weights'] = None 52 | if 'linear' not in v and 'l2' in k: 53 | params['l2'] = None 54 | if ('linear' not in v and 'ransac' not in v and 'loess' not in v) and 'poly' in k: 55 | params['poly'] = None 56 | # if 'loess' not in v and 'window_size' in k and 'moving_average' not in v and 'window_size' in k: 57 | # params['window_size'] = None 58 | if 'fourier' not in v and 'fourier_order' in k: 59 | params['fourier_order'] = None 60 | if 'arima' not in str(v) and 'arima_order' in k: 61 | params['arima_order'] = None 62 | if 'decision_tree' not in v and 'tree_depth' in k: 63 | params['tree_depth'] = None 64 | # if 'local' in v and ('loess' in v or 'ewm' in v or 'ses' in v or 'des' 65 | # in v or 'damped_des' in v or 'arima' in v): 66 | # params['fit_type'] = 'global' 67 | params['exogenous'] = exogenous 68 | return params 69 | 70 | 71 | 72 | def sanitize_params(self, param_list): 73 | """ 74 | Iterate through param dicts to sanitize illegal combinations. 75 | 76 | Parameters 77 | ---------- 78 | param_list : list 79 | A List of param dicts. 80 | 81 | Returns 82 | ------- 83 | list 84 | List of cleaned param dicts. 85 | 86 | """ 87 | cleaned = [self.param_check(i) for i in param_list] 88 | #drop duplicate settings breaks with arrays from seasonality_weights 89 | #return [i for n, i in enumerate(cleaned) if i not in cleaned[n + 1:]] 90 | return cleaned 91 | -------------------------------------------------------------------------------- /ThymeBoost/predict_functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import numpy as np 4 | import pandas as pd 5 | import matplotlib.pyplot as plt 6 | from ThymeBoost.utils import trend_dampen 7 | 8 | 9 | def predict_trend(booster_obj, 10 | boosting_round, 11 | forecast_horizon, 12 | trend_penalty, 13 | online_learning): 14 | """ 15 | Predict the trend component using the booster 16 | 17 | Parameters 18 | ---------- 19 | boosting_round : int 20 | The round to reference when getting model params. 21 | forecast_horizon : int 22 | Number of periods to forecast. 23 | 24 | Returns 25 | ------- 26 | trend_round : np.array 27 | That boosting round's predicted trend component. 28 | 29 | """ 30 | trend_param = booster_obj.trend_pred_params[boosting_round] 31 | trend_model = booster_obj.trend_objs[boosting_round].model_obj 32 | trend_round = trend_model.predict(forecast_horizon, trend_param) 33 | if trend_penalty: 34 | avg_slope = np.mean(np.gradient(trend_round)) 35 | if avg_slope != 0: 36 | penalty = booster_obj.trend_strengths[boosting_round] 37 | trend_round = trend_dampen.trend_dampen(1-penalty, trend_round) 38 | if online_learning: 39 | trend_model._online_steps += forecast_horizon 40 | return trend_round 41 | 42 | 43 | def predict_seasonality(booster_obj, boosting_round, forecast_horizon): 44 | """ 45 | Predict the seasonality component using the booster. 46 | 47 | Parameters 48 | ---------- 49 | boosting_round : int 50 | The round to reference when getting model params. 51 | forecast_horizon : int 52 | Number of periods to forecast. 53 | 54 | Returns 55 | ------- 56 | seas_round : np.array 57 | That boosting round's predicted seasonal component. 58 | 59 | """ 60 | seas_param = booster_obj.seasonal_pred_params[boosting_round] 61 | seas_model = booster_obj.seasonal_objs[boosting_round].model_obj 62 | if seas_model is None: 63 | seas_round = np.zeros(forecast_horizon) 64 | else: 65 | seas_round = seas_model.predict(forecast_horizon, seas_param) 66 | return seas_round 67 | 68 | 69 | def predict_exogenous(booster_obj, 70 | future_exo, 71 | boosting_round, 72 | forecast_horizon): 73 | """ 74 | Predict the exogenous component using the booster. 75 | 76 | Parameters 77 | ---------- 78 | boosting_round : int 79 | The round to reference when getting model params. 80 | forecast_horizon : int 81 | Number of periods to forecast. 82 | 83 | Returns 84 | ------- 85 | seas_round : np.array 86 | That boosting round's predicted seasonal component. 87 | 88 | """ 89 | if future_exo is None: 90 | exo_round = np.zeros(forecast_horizon) 91 | else: 92 | exo_model = booster_obj.exo_objs[boosting_round].model_obj 93 | exo_round = exo_model.predict(future_exo) 94 | exo_round = exo_round * booster_obj.exo_class.exogenous_lr 95 | return exo_round 96 | 97 | 98 | def predict_rounds(booster_obj, 99 | forecast_horizon, 100 | trend_penalty, 101 | future_exo=None, 102 | online_learning=False): 103 | """ 104 | Predict all the rounds from a booster 105 | 106 | Parameters 107 | ---------- 108 | fitted_output : pd.DataFrame 109 | Output from fit method. 110 | forecast_horizon : int 111 | Number of periods to forecast. 112 | 113 | Returns 114 | ------- 115 | trend_predictions : np.array 116 | Trend component. 117 | seasonal_predictions : np.array 118 | seasonal component. 119 | predictions : np.array 120 | Predictions. 121 | 122 | """ 123 | trend_predictions = np.zeros(forecast_horizon) 124 | seasonal_predictions = np.zeros(forecast_horizon) 125 | exo_predictions = np.zeros(forecast_horizon) 126 | for boosting_round in range(booster_obj.i): 127 | trend_predictions += predict_trend(booster_obj, 128 | boosting_round, 129 | forecast_horizon, 130 | trend_penalty, 131 | online_learning) 132 | seasonal_predictions += predict_seasonality(booster_obj, 133 | boosting_round, 134 | forecast_horizon) 135 | exo_predictions += predict_exogenous(booster_obj, 136 | future_exo, 137 | boosting_round, 138 | forecast_horizon) 139 | predictions = (trend_predictions + 140 | seasonal_predictions + 141 | exo_predictions) 142 | return trend_predictions, seasonal_predictions, exo_predictions, predictions 143 | -------------------------------------------------------------------------------- /ThymeBoost/seasonality_models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | -------------------------------------------------------------------------------- /ThymeBoost/seasonality_models/__pycache__/SeasonalityBaseModel.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/seasonality_models/__pycache__/SeasonalityBaseModel.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/seasonality_models/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/seasonality_models/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/seasonality_models/__pycache__/classic_seasonality.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/seasonality_models/__pycache__/classic_seasonality.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/seasonality_models/__pycache__/fourier_seasonality.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/seasonality_models/__pycache__/fourier_seasonality.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/seasonality_models/__pycache__/seasonality_base_class.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/seasonality_models/__pycache__/seasonality_base_class.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/seasonality_models/classic_seasonality.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | from ThymeBoost.seasonality_models.seasonality_base_class import SeasonalityBaseModel 4 | 5 | 6 | class ClassicSeasonalityModel(SeasonalityBaseModel): 7 | """ 8 | Seasonality for naive decomposition method. 9 | """ 10 | model = 'classic' 11 | 12 | def __init__(self, seasonal_period, normalize_seasonality, seasonality_weights): 13 | self.seasonal_period = seasonal_period 14 | self.normalize_seasonality = normalize_seasonality 15 | self.seasonality_weights = seasonality_weights 16 | self.seasonality = None 17 | return 18 | 19 | def __str__(self): 20 | return f'{self.model}({self.seasonality_weights is not None})' 21 | 22 | def fit(self, y, **kwargs): 23 | """ 24 | Fit the seasonal component for naive method in the boosting loop. 25 | 26 | Parameters 27 | ---------- 28 | y : TYPE 29 | DESCRIPTION. 30 | **kwargs : TYPE 31 | DESCRIPTION. 32 | 33 | Returns 34 | ------- 35 | None. 36 | 37 | """ 38 | if self.seasonality_weights is not None: 39 | y = y * self.seasonality_weights 40 | avg_seas = [np.mean(y[i::self.seasonal_period], axis=0) for i in range(self.seasonal_period)] 41 | avg_seas = np.array(avg_seas) 42 | self.seasonality = np.resize(avg_seas, len(y)) 43 | # If normalize_seasonality we call normalize function from base class 44 | if self.normalize_seasonality: 45 | self.seasonality = self.normalize() 46 | self.seasonality = self.seasonality * kwargs['seasonality_lr'] 47 | single_season = self.seasonality[:self.seasonal_period] 48 | future_seasonality = np.resize(single_season, len(y) + self.seasonal_period) 49 | self.model_params = future_seasonality[-self.seasonal_period:] 50 | return self.seasonality 51 | 52 | def predict(self, forecast_horizon, model_params): 53 | return np.resize(model_params, forecast_horizon) 54 | -------------------------------------------------------------------------------- /ThymeBoost/seasonality_models/fourier_seasonality.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | from ThymeBoost.seasonality_models.seasonality_base_class import SeasonalityBaseModel 4 | 5 | 6 | class FourierSeasonalityModel(SeasonalityBaseModel): 7 | """ 8 | Seasonality for naive decomposition method. 9 | """ 10 | model = 'fourier' 11 | 12 | def __init__(self, 13 | seasonal_period, 14 | normalize_seasonality, 15 | seasonality_weights): 16 | self.seasonal_period = seasonal_period 17 | self.normalize_seasonality = normalize_seasonality 18 | self.seasonality_weights = seasonality_weights 19 | self.seasonality = None 20 | self.model_params = None 21 | return 22 | 23 | def __str__(self): 24 | return f'{self.model}({self.kwargs["fourier_order"]}, {self.seasonality_weights is not None})' 25 | 26 | def handle_seasonal_weights(self, y): 27 | if self.seasonality_weights is None: 28 | seasonality_weights = None 29 | elif isinstance(self.seasonality_weights, list): 30 | if self.seasonal_weights[0] is None: 31 | seasonality_weights = None 32 | elif self.seasonality_weights == 'regularize': 33 | seasonality_weights = 1/(0.0001 + y**2) 34 | elif self.seasonality_weights == 'explode': 35 | seasonality_weights = (y**2) 36 | elif callable(self.seasonality_weights): 37 | seasonality_weights = self.seasonality_weights(y) 38 | else: 39 | seasonality_weights = np.array(self.seasonality_weights).reshape(-1,) 40 | seasonality_weights = seasonality_weights[:len(y)] 41 | return seasonality_weights 42 | 43 | def get_fourier_series(self, t, fourier_order): 44 | x = 2 * np.pi * (np.arange(1, fourier_order + 1) / 45 | self.seasonal_period) 46 | x = x * t[:, None] 47 | fourier_series = np.concatenate((np.cos(x), np.sin(x)), axis=1) 48 | return fourier_series 49 | 50 | def fit(self, y, **kwargs): 51 | """ 52 | Fit the seasonal component for fourier basis function method in the boosting loop. 53 | 54 | Parameters 55 | ---------- 56 | y : TYPE 57 | DESCRIPTION. 58 | **kwargs : TYPE 59 | DESCRIPTION. 60 | 61 | Returns 62 | ------- 63 | None. 64 | 65 | """ 66 | self.kwargs = kwargs 67 | fourier_order = kwargs['fourier_order'] 68 | seasonality_weights = self.handle_seasonal_weights(y) 69 | X = self.get_fourier_series(np.arange(len(y)), fourier_order) 70 | if seasonality_weights is not None: 71 | weighted_X_T = X.T @ np.diag(seasonality_weights) 72 | beta = np.linalg.pinv(weighted_X_T.dot(X)).dot(weighted_X_T.dot(y)) 73 | else: 74 | beta = np.linalg.pinv(X.T.dot(X)).dot(X.T.dot(y)) 75 | self.seasonality = X @ beta 76 | # If normalize_seasonality we call normalize function from base class 77 | if self.normalize_seasonality: 78 | self.seasonality = self.normalize() 79 | self.seasonality = self.seasonality * kwargs['seasonality_lr'] 80 | single_season = self.seasonality[:self.seasonal_period] 81 | future_seasonality = np.resize(single_season, len(y) + self.seasonal_period) 82 | self.model_params = future_seasonality[-self.seasonal_period:] 83 | return self.seasonality 84 | 85 | def predict(self, forecast_horizon, model_params): 86 | return np.resize(model_params, forecast_horizon) 87 | -------------------------------------------------------------------------------- /ThymeBoost/seasonality_models/naive_seasonality.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import numpy as np 4 | from ThymeBoost.seasonality_models.seasonality_base_class import SeasonalityBaseModel 5 | 6 | 7 | class NaiveSeasonalityModel(SeasonalityBaseModel): 8 | """ 9 | Seasonality for naive decomposition method. 10 | """ 11 | model = 'naive' 12 | 13 | def __init__(self, seasonal_period, normalize_seasonality, seasonality_weights): 14 | self.seasonal_period = seasonal_period 15 | self.normalize_seasonality = normalize_seasonality 16 | self.seasonality_weights = seasonality_weights 17 | self.seasonality = None 18 | return 19 | 20 | def __str__(self): 21 | return f'{self.model}({self.seasonality_weights is not None})' 22 | 23 | def fit(self, y, **kwargs): 24 | """ 25 | Fit the seasonal component for naive method in the boosting loop. 26 | 27 | Parameters 28 | ---------- 29 | y : TYPE 30 | DESCRIPTION. 31 | **kwargs : TYPE 32 | DESCRIPTION. 33 | 34 | Returns 35 | ------- 36 | None. 37 | 38 | """ 39 | self.seasonality = y 40 | if self.normalize_seasonality: 41 | self.seasonality = self.normalize() 42 | init_seasonality = self.seasonality[:self.seasonal_period] 43 | last_seasonality = self.seasonality[-self.seasonal_period:] 44 | self.seasonality = np.append(init_seasonality, self.seasonality[:-self.seasonal_period]) 45 | self.seasonality = self.seasonality * kwargs['seasonality_lr'] 46 | self.model_params = last_seasonality 47 | future_seasonality = np.resize(last_seasonality, len(y) + self.seasonal_period) 48 | self.model_params = future_seasonality[-self.seasonal_period:] 49 | return self.seasonality 50 | 51 | def predict(self, forecast_horizon, model_params): 52 | return np.resize(model_params, forecast_horizon) 53 | 54 | -------------------------------------------------------------------------------- /ThymeBoost/seasonality_models/seasonality_base_class.py: -------------------------------------------------------------------------------- 1 | 2 | from abc import ABC, abstractmethod 3 | import numpy as np 4 | import pandas as pd 5 | 6 | class SeasonalityBaseModel(ABC): 7 | """ 8 | Seasonality Abstract Base Class. 9 | """ 10 | model = None 11 | 12 | @abstractmethod 13 | def __init__(self): 14 | self.seasonality = None 15 | pass 16 | 17 | def __str__(cls): 18 | return f'{cls.model} model' 19 | 20 | @abstractmethod 21 | def fit(self, y, **kwargs): 22 | """ 23 | Fit the seasonal component in the boosting loop. 24 | 25 | Parameters 26 | ---------- 27 | time_series : TYPE 28 | DESCRIPTION. 29 | **kwargs : TYPE 30 | DESCRIPTION. 31 | 32 | Returns 33 | ------- 34 | None. 35 | 36 | """ 37 | pass 38 | 39 | @abstractmethod 40 | def predict(self, forecast_horizon): 41 | pass 42 | 43 | def __add__(self, seas_obj): 44 | """ 45 | Add two seasonal obj together, useful for ensembling or just quick updating of seasonal components. 46 | 47 | Parameters 48 | ---------- 49 | trend_obj : TYPE 50 | DESCRIPTION. 51 | 52 | Returns 53 | ------- 54 | TYPE 55 | DESCRIPTION. 56 | 57 | """ 58 | return self.fitted + seas_obj.fitted 59 | 60 | def __mul__(self, seas_obj): 61 | return self.fitted * seas_obj.fitted 62 | 63 | def __div__(self, seas_obj): 64 | return self.fitted / seas_obj.fitted 65 | 66 | def __sub__(self, seas_obj): 67 | return self.fitted - seas_obj.fitted 68 | 69 | def append(self, seas_obj): 70 | return np.append(self.fitted, seas_obj.fitted) 71 | 72 | def to_series(self, array): 73 | return pd.Series(array) 74 | 75 | def normalize(self): 76 | """Enforce average seasonlaity of 0 for 'add' seasonality and 1 for 'mult' seasonality""" 77 | self.seasonality -= np.mean(self.seasonality) 78 | return self.seasonality 79 | -------------------------------------------------------------------------------- /ThymeBoost/split_proposals.py: -------------------------------------------------------------------------------- 1 | """ 2 | A class to propose splits according to the most and least interesting points 3 | based on the gradient. 4 | """ 5 | import numpy as np 6 | import pandas as pd 7 | import scipy.stats as stats 8 | 9 | 10 | class SplitProposals: 11 | """ 12 | Generate splits to try when fit_type = 'local'. 13 | 14 | Parameters 15 | ---------- 16 | given_splits : list 17 | Splits to use when using fit_type='local'. 18 | 19 | exclude_splits : list 20 | exclude these index when considering splits for 21 | fit_type='local'. Must be idx not datetimeindex if 22 | using a Pandas Series. 23 | 24 | min_sample_pct : float 25 | Percentage of samples required to consider a split. 26 | Must be 0 min_split_idx] 91 | proposals = [i for i in proposals if i < len(self.time_series) - min_split_idx] 92 | proposals = [i for i in proposals if i not in self.exclude_splits] 93 | if not proposals: 94 | proposals = list(range(min_split_idx, len(self.time_series) - min_split_idx)) 95 | return proposals 96 | 97 | def get_split_proposals(self, time_series): 98 | """ 99 | Get proposals based on simple gradient strategy or histograms. 100 | 101 | Parameters 102 | ---------- 103 | time_series : np.array 104 | The time series. 105 | 106 | Returns 107 | ------- 108 | list 109 | A list of split indices to try. 110 | 111 | """ 112 | self.time_series = time_series 113 | if self.approximate_splits: 114 | return self.get_approximate_split_proposals() 115 | elif self.given_splits: 116 | return self.given_splits 117 | else: 118 | min_idx = int(max(5, len(self.time_series) * self.min_sample_pct)) 119 | return list(range(min_idx, len(self.time_series) - min_idx)) 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /ThymeBoost/tests/unitTests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | import pandas as pd 4 | import numpy as np 5 | from ThymeBoost.trend_models import (linear_trend, mean_trend, median_trend, 6 | loess_trend, ransac_trend, ewm_trend, 7 | ets_trend, arima_trend, moving_average_trend, 8 | zero_trend, svr_trend, naive_trend) 9 | def testing_data(): 10 | seasonality = ((np.cos(np.arange(1, 101))*10 + 50)) 11 | np.random.seed(100) 12 | true = np.linspace(-1, 1, 100) 13 | noise = np.random.normal(0, 1, 100) 14 | y = true + seasonality# + noise 15 | return y 16 | 17 | class BaseModelTest(): 18 | """Allows self without overriding unitTest __init__""" 19 | def setUp(self): 20 | self.model_obj = None 21 | 22 | def set_model_obj(self, child_model_obj): 23 | self.model_obj = child_model_obj 24 | self._params = {'arima_order': 'auto', 25 | 'model': 'ses', 26 | 'bias': 0, 27 | 'arima_trend': None, 28 | 'alpha': None, 29 | 'poly': 1, 30 | 'fit_constant': True, 31 | 'l2': 0, 32 | 'trend_weights': None, 33 | 'ewm_alpha': .5, 34 | 'window_size': 13, 35 | 'ransac_trials': 20, 36 | 'ransac_min_samples': 5} 37 | 38 | def test_fitted_series(self): 39 | y = testing_data() 40 | fitted_values = self.model_obj.fit(y, **self._params) 41 | self.assertTrue(isinstance(fitted_values, np.ndarray)) 42 | 43 | def test_predicted_series(self): 44 | y = testing_data() 45 | self.model_obj.fit(y, **self._params) 46 | predictions = self.model_obj.predict(24, self.model_obj.model_params) 47 | self.assertTrue(isinstance(predictions, np.ndarray)) 48 | 49 | def test_fitted_null(self): 50 | y = testing_data() 51 | fitted_values = self.model_obj.fit(y, **self._params) 52 | self.assertFalse(pd.Series(fitted_values).isnull().values.any()) 53 | 54 | def test_prediction_null(self): 55 | y = testing_data() 56 | self.model_obj.fit(y, **self._params) 57 | predictions = self.model_obj.predict(24, self.model_obj.model_params) 58 | self.assertFalse(pd.Series(predictions).isnull().values.any()) 59 | 60 | 61 | class ArimaTest(BaseModelTest, unittest.TestCase): 62 | def setUp(self): 63 | self.set_model_obj(arima_trend.ArimaModel()) 64 | 65 | class MeanTest(BaseModelTest, unittest.TestCase): 66 | def setUp(self): 67 | self.set_model_obj(mean_trend.MeanModel()) 68 | 69 | class MedianTest(BaseModelTest, unittest.TestCase): 70 | def setUp(self): 71 | self.set_model_obj(median_trend.MedianModel()) 72 | 73 | class MovingAverageTest(BaseModelTest, unittest.TestCase): 74 | def setUp(self): 75 | self.set_model_obj(moving_average_trend.MovingAverageModel()) 76 | 77 | class ZeroTest(BaseModelTest, unittest.TestCase): 78 | def setUp(self): 79 | self.set_model_obj(zero_trend.ZeroModel()) 80 | 81 | class EwmTest(BaseModelTest, unittest.TestCase): 82 | def setUp(self): 83 | self.set_model_obj(ewm_trend.EwmModel()) 84 | 85 | class EtsTest(BaseModelTest, unittest.TestCase): 86 | def setUp(self): 87 | self.set_model_obj(ets_trend.EtsModel()) 88 | 89 | class LinearTest(BaseModelTest, unittest.TestCase): 90 | def setUp(self): 91 | self.set_model_obj(linear_trend.LinearModel()) 92 | 93 | class LoessTest(BaseModelTest, unittest.TestCase): 94 | def setUp(self): 95 | self.set_model_obj(loess_trend.LoessModel()) 96 | 97 | class SvrTest(BaseModelTest, unittest.TestCase): 98 | def setUp(self): 99 | self.set_model_obj(svr_trend.SvrModel()) 100 | 101 | class RansacTest(BaseModelTest, unittest.TestCase): 102 | def setUp(self): 103 | self.set_model_obj(ransac_trend.RansacModel()) 104 | 105 | class NaiveTest(BaseModelTest, unittest.TestCase): 106 | def setUp(self): 107 | self.set_model_obj(naive_trend.NaiveModel()) 108 | 109 | 110 | 111 | 112 | if __name__ == '__main__': 113 | unittest.main() 114 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/ArimaModel.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/ArimaModel.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/EtsModel.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/EtsModel.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/EwmModel.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/EwmModel.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/LinearModel.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/LinearModel.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/LoessModel.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/LoessModel.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/MeanModel.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/MeanModel.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/MedianModel.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/MedianModel.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/RansacModel.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/RansacModel.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/TrendBaseModel.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/TrendBaseModel.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/arima_trend.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/arima_trend.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/ets_trend.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/ets_trend.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/ewm_trend.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/ewm_trend.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/linear_trend.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/linear_trend.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/loess_trend.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/loess_trend.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/mean_trend.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/mean_trend.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/median_trend.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/median_trend.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/ransac_trend.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/ransac_trend.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/__pycache__/trend_base_class.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/trend_models/__pycache__/trend_base_class.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/trend_models/arima_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | import statsmodels as sm 4 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 5 | from pmdarima.arima import auto_arima 6 | 7 | class ArimaModel(TrendBaseModel): 8 | """ARIMA Model from Statsmodels""" 9 | model = 'arima' 10 | 11 | def __init__(self): 12 | self.model_params = None 13 | self.fitted = None 14 | self._online_steps = 0 15 | 16 | def __str__(self): 17 | return f'{self.model}({self.kwargs["arima_order"]})' 18 | 19 | def fit(self, y, **kwargs): 20 | """ 21 | Fit the trend component in the boosting loop for a arima model. 22 | 23 | Parameters 24 | ---------- 25 | time_series : TYPE 26 | DESCRIPTION. 27 | **kwargs : TYPE 28 | DESCRIPTION. 29 | 30 | Returns 31 | ------- 32 | None. 33 | 34 | """ 35 | self.kwargs = kwargs 36 | self.order = kwargs['arima_order'] 37 | self.arima_trend = kwargs['arima_trend'] 38 | bias = kwargs['bias'] 39 | if self.order == 'auto': 40 | ar_model = auto_arima(y, 41 | seasonal=False, 42 | error_action='warn', 43 | trace=False, 44 | supress_warnings=True, 45 | stepwise=True, 46 | random_state=20, 47 | n_fits=50) 48 | self.fitted = ar_model.predict_in_sample() 49 | else: 50 | ar_model = sm.tsa.arima.model.ARIMA(y - bias, 51 | order=self.order, 52 | trend=self.arima_trend).fit() 53 | self.fitted = ar_model.predict(start=0, end=len(y) - 1) + bias 54 | self.model_params = (ar_model, bias, len(y)) 55 | return self.fitted 56 | 57 | def predict(self, forecast_horizon, model_params): 58 | last_point = model_params[2] + forecast_horizon 59 | if self.order == 'auto': 60 | prediction = model_params[0].predict(n_periods=forecast_horizon) 61 | else: 62 | prediction = model_params[0].predict(start=model_params[2] + 1, end=last_point) + \ 63 | model_params[1] 64 | return prediction 65 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/croston_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | import pandas as pd 4 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 5 | 6 | 7 | class CrostonModel(TrendBaseModel): 8 | model = 'croston' 9 | 10 | def __init__(self): 11 | self.model_params = None 12 | self.fitted = None 13 | self._online_steps = 0 14 | 15 | def __str__(self): 16 | return f'{self.model}()' 17 | 18 | def fit(self, y, **kwargs): 19 | """ 20 | Fit the trend component in the boosting loop for the croston model. Stolen from Sktime: 21 | https://www.sktime.org/en/v0.8.0/api_reference/auto_generated/sktime.forecasting.croston.Croston.html 22 | Thank you Sktime! 23 | 24 | Parameters 25 | ---------- 26 | time_series : TYPE 27 | DESCRIPTION. 28 | **kwargs : TYPE 29 | DESCRIPTION. 30 | 31 | Returns 32 | ------- 33 | fitted values. 34 | 35 | """ 36 | self.kwargs = kwargs 37 | bias = kwargs['bias'] 38 | # y -= bias 39 | n_timepoints = len(y) # Historical period: i.e the input array's length 40 | smoothing = kwargs['alpha'] 41 | if smoothing is None: 42 | smoothing = .5 43 | 44 | # Fit the parameters: level(q), periodicity(a) and forecast(f) 45 | q, a, f = np.full((3, n_timepoints + 1), np.nan) 46 | p = 1 # periods since last demand observation 47 | 48 | # Initialization: 49 | first_occurrence = np.argmax(y[:n_timepoints] > 0) 50 | q[0] = y[first_occurrence] 51 | a[0] = 1 + first_occurrence 52 | f[0] = q[0] / a[0] 53 | 54 | # Create t+1 forecasts: 55 | for t in range(0, n_timepoints): 56 | if y[t] > 0: 57 | q[t + 1] = smoothing * y[t] + (1 - smoothing) * q[t] 58 | a[t + 1] = smoothing * p + (1 - smoothing) * a[t] 59 | f[t + 1] = q[t + 1] / a[t + 1] 60 | p = 1 61 | else: 62 | q[t + 1] = q[t] 63 | a[t + 1] = a[t] 64 | f[t + 1] = f[t] 65 | p += 1 66 | self.fitted = f[1:] 67 | last_fitted_values = self.fitted[-1] 68 | self.model_params = last_fitted_values 69 | return self.fitted #+ bias 70 | 71 | def predict(self, forecast_horizon, model_params): 72 | return np.tile(model_params, forecast_horizon) 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/decision_tree_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | import numpy as np 4 | import pandas as pd 5 | from sklearn.tree import DecisionTreeRegressor 6 | 7 | class DecisionTreeModel(TrendBaseModel): 8 | model = 'decision_tree' 9 | 10 | def __init__(self): 11 | self.model_params = None 12 | self.fitted = None 13 | self._online_steps = 0 14 | 15 | def __str__(self): 16 | return f'{self.model}()' 17 | 18 | def fit(self, y, **kwargs): 19 | """ 20 | Fit the trend component in the boosting loop for an optimized theta model. 21 | 22 | Parameters 23 | ---------- 24 | time_series : TYPE 25 | DESCRIPTION. 26 | **kwargs : TYPE 27 | DESCRIPTION. 28 | 29 | Returns 30 | ------- 31 | None. 32 | 33 | """ 34 | self.kwargs = kwargs 35 | bias = kwargs['bias'] 36 | tree_depth = kwargs['tree_depth'] 37 | # y -= bias 38 | X = np.array(range(len(y))).reshape((-1, 1)) 39 | tree_model = DecisionTreeRegressor(max_depth=tree_depth) 40 | self.model_obj = tree_model.fit(X, y) 41 | self.fitted = self.model_obj.predict(X)# + bias 42 | last_fitted_values = self.fitted[-1] 43 | self.model_params = last_fitted_values 44 | return self.fitted 45 | 46 | def predict(self, forecast_horizon, model_params): 47 | return np.tile(model_params, forecast_horizon) 48 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/ets_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | import numpy as np 4 | from statsmodels.tsa.api import SimpleExpSmoothing, Holt 5 | 6 | class EtsModel(TrendBaseModel): 7 | """Several ETS methods from Statsmodels including: 8 | 'ses': Simple Exponential Smoother 9 | 'des': Double Exponential Smoother 10 | 'damped_des': Damped Double Exponential Smoother 11 | These are to be passed as the 'trend_estimator' parameter in the ThymeBoost fit method. 12 | If alpha or beta are not given then it will follow Statsmodels optimization. 13 | For more info: https://www.statsmodels.org/stable/examples/notebooks/generated/exponential_smoothing.html 14 | """ 15 | model = 'ets' 16 | 17 | def __init__(self): 18 | self.model_params = None 19 | self.fitted = None 20 | self._online_steps = 0 21 | 22 | def __str__(self): 23 | return f'{self.model}()' 24 | 25 | def simple_exponential_smoothing(self, y, bias, alpha): 26 | smoother = SimpleExpSmoothing(y - bias) 27 | fit_model = smoother.fit(smoothing_level=alpha) 28 | fitted = fit_model.fittedvalues 29 | self.model_params = (fit_model, bias, len(y)) 30 | return fitted 31 | 32 | def double_exponential_smoothing(self, y, bias, alpha, beta): 33 | smoother = Holt(y - bias) 34 | fit_model = smoother.fit(smoothing_level=alpha, smoothing_trend=beta) 35 | fitted = fit_model.fittedvalues 36 | self.model_params = (fit_model, bias, len(y)) 37 | return fitted 38 | 39 | def damped_double_exponential_smoothing(self, y, bias, alpha, beta): 40 | smoother = Holt(y - bias, damped_trend=True) 41 | fit_model = smoother.fit(smoothing_level=alpha, 42 | smoothing_trend=beta) 43 | fitted = fit_model.fittedvalues 44 | self.model_params = (fit_model, bias, len(y)) 45 | return fitted 46 | 47 | def fit(self, y, **kwargs): 48 | """ 49 | Fit the trend component in the boosting loop for a ets model. 50 | 51 | Parameters 52 | ---------- 53 | time_series : np.ndarray 54 | DESCRIPTION. 55 | **kwargs : 56 | Key 1: 'alpha': The alpha parameter for the level smoothing. If not given then this will be optimized 57 | Key 2: 'beta': The beta parameter for the trend smoothing. If not given then this will be optimized 58 | 59 | 60 | Returns 61 | ------- 62 | Fitted array. 63 | 64 | """ 65 | self.model = kwargs['model'] 66 | bias = kwargs['bias'] 67 | if self.model == 'ses': 68 | self.fitted = self.simple_exponential_smoothing(y, bias, kwargs['alpha']) 69 | elif self.model == 'des': 70 | self.fitted = self.double_exponential_smoothing(y, bias, kwargs['alpha'], kwargs['beta']) 71 | elif self.model == 'damped_des': 72 | self.fitted = self.damped_double_exponential_smoothing(y, bias, kwargs['alpha'], kwargs['beta']) 73 | else: 74 | raise ValueError('That model type is not implemented!') 75 | return self.fitted 76 | 77 | def predict(self, forecast_horizon, model_params): 78 | _start = model_params[2] 79 | _end = _start + forecast_horizon - 1 80 | prediction = model_params[0].predict(start=_start, end=_end) + model_params[1] 81 | return prediction 82 | 83 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/ewm_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | import numpy as np 4 | import pandas as pd 5 | 6 | class EwmModel(TrendBaseModel): 7 | """ 8 | The ewm method utilizes a Pandas ewm method: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ewm.html. 9 | Fitting this with a 'local' fit_type parameter is not advised. 10 | """ 11 | model = 'ewm' 12 | 13 | def __init__(self): 14 | self.model_params = None 15 | self.fitted = None 16 | self._online_steps = 0 17 | 18 | def __str__(self): 19 | return f'{self.model}({self.kwargs["ewm_alpha"]})' 20 | 21 | def fit(self, y, **kwargs): 22 | """ 23 | Fit the trend component in the boosting loop for a ewm model using the 'ewm_alpha' parameter. 24 | 25 | Parameters 26 | ---------- 27 | time_series : np.ndarray 28 | DESCRIPTION. 29 | **kwargs : 30 | The key 'ewm_alpha' is passed to this method from the ThymeBoost fit method. 31 | 32 | Returns 33 | ------- 34 | Fitted array. 35 | 36 | """ 37 | self.kwargs = kwargs 38 | alpha = kwargs['ewm_alpha'] 39 | bias = kwargs['bias'] 40 | y = pd.Series(y - bias) 41 | self.fitted = np.array(y.ewm(alpha=alpha).mean()) + bias 42 | last_fitted_values = self.fitted[-1] 43 | self.model_params = last_fitted_values 44 | return self.fitted 45 | 46 | def predict(self, forecast_horizon, model_params): 47 | return np.tile(model_params, forecast_horizon) 48 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/fast_arima_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 4 | 5 | 6 | class FastArimaModel(TrendBaseModel): 7 | """Fast ARIMA Model from Statsforecast""" 8 | model = 'fast_arima' 9 | 10 | def __init__(self): 11 | self.model_params = None 12 | self.fitted = None 13 | self._online_steps = 0 14 | 15 | def __str__(self): 16 | return f'{self.model}({self.kwargs["arima_order"]})' 17 | 18 | def fit(self, y, **kwargs): 19 | """ 20 | Fit the trend component in the boosting loop for a fast arima model. 21 | 22 | Parameters 23 | ---------- 24 | time_series : TYPE 25 | DESCRIPTION. 26 | **kwargs : TYPE 27 | DESCRIPTION. 28 | 29 | Returns 30 | ------- 31 | None. 32 | 33 | """ 34 | try: 35 | from statsforecast.models import AutoARIMA 36 | except Exception: 37 | raise ValueError('Using Fast implementations requires an optional dependency Statsforecast: pip install statsforecast') 38 | self.kwargs = kwargs 39 | bias = kwargs['bias'] 40 | ar_model = AutoARIMA(season_length=0).fit(y - bias) 41 | self.fitted = ar_model.predict_in_sample()['fitted'] 42 | self.model_params = (ar_model, bias) 43 | return self.fitted 44 | 45 | def predict(self, forecast_horizon, model_params): 46 | prediction = model_params[0].predict(h=forecast_horizon)['mean'] + \ 47 | model_params[1] 48 | return prediction 49 | #%% 50 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/fast_ces_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | 4 | 5 | class FastCESModel(TrendBaseModel): 6 | """Fast ETS Model from Statsforecast""" 7 | model = 'fast_ces' 8 | 9 | def __init__(self): 10 | self.model_params = None 11 | self.fitted = None 12 | self._online_steps = 0 13 | 14 | def __str__(self): 15 | return f'{self.model}({self.kwargs["arima_order"]})' 16 | 17 | def fit(self, y, **kwargs): 18 | """ 19 | Fit the trend component in the boosting loop for a fast ets model. 20 | 21 | Parameters 22 | ---------- 23 | time_series : TYPE 24 | DESCRIPTION. 25 | **kwargs : TYPE 26 | DESCRIPTION. 27 | 28 | Returns 29 | ------- 30 | None. 31 | 32 | """ 33 | try: 34 | from statsforecast.models import AutoCES 35 | except Exception: 36 | raise ValueError('Using Fast implementations requires an optional dependency Statsforecast: pip install statsforecast') 37 | self.kwargs = kwargs 38 | bias = kwargs['bias'] 39 | ets_model = AutoCES().fit(y - bias) 40 | self.fitted = ets_model.predict_in_sample()['fitted'] 41 | self.model_params = (ets_model, bias) 42 | return self.fitted 43 | 44 | def predict(self, forecast_horizon, model_params): 45 | prediction = model_params[0].predict(h=forecast_horizon)['mean'] + \ 46 | model_params[1] 47 | return prediction 48 | #%% 49 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/fast_ets_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | 4 | 5 | class FastETSModel(TrendBaseModel): 6 | """Fast ETS Model from Statsforecast""" 7 | model = 'fast_ets' 8 | 9 | def __init__(self): 10 | self.model_params = None 11 | self.fitted = None 12 | self._online_steps = 0 13 | 14 | def __str__(self): 15 | return f'{self.model}({self.kwargs["arima_order"]})' 16 | 17 | def fit(self, y, **kwargs): 18 | """ 19 | Fit the trend component in the boosting loop for a fast ets model. 20 | 21 | Parameters 22 | ---------- 23 | time_series : TYPE 24 | DESCRIPTION. 25 | **kwargs : TYPE 26 | DESCRIPTION. 27 | 28 | Returns 29 | ------- 30 | None. 31 | 32 | """ 33 | try: 34 | from statsforecast.models import AutoETS 35 | except Exception: 36 | raise ValueError('Using Fast implementations requires an optional dependency Statsforecast: pip install statsforecast') 37 | self.kwargs = kwargs 38 | bias = kwargs['bias'] 39 | ets_model = AutoETS().fit(y - bias) 40 | self.fitted = ets_model.predict_in_sample()['fitted'] 41 | self.model_params = (ets_model, bias) 42 | return self.fitted 43 | 44 | def predict(self, forecast_horizon, model_params): 45 | prediction = model_params[0].predict(h=forecast_horizon)['mean'] + \ 46 | model_params[1] 47 | return prediction 48 | #%% 49 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/fast_imapa_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | 4 | 5 | class FastIMAPAModel(TrendBaseModel): 6 | """Fast ETS Model from Statsforecast""" 7 | model = 'fast_imapa' 8 | 9 | def __init__(self): 10 | self.model_params = None 11 | self.fitted = None 12 | self._online_steps = 0 13 | 14 | def __str__(self): 15 | return f'{self.model}({self.kwargs["arima_order"]})' 16 | 17 | def fit(self, y, **kwargs): 18 | """ 19 | Fit the trend component in the boosting loop for a fast ets model. 20 | 21 | Parameters 22 | ---------- 23 | time_series : TYPE 24 | DESCRIPTION. 25 | **kwargs : TYPE 26 | DESCRIPTION. 27 | 28 | Returns 29 | ------- 30 | None. 31 | 32 | """ 33 | try: 34 | from statsforecast.models import IMAPA 35 | except Exception: 36 | raise ValueError('Using Fast implementations requires an optional dependency Statsforecast: pip install statsforecast') 37 | self.kwargs = kwargs 38 | bias = kwargs['bias'] 39 | ets_model = IMAPA().fit(y - bias) 40 | self.fitted = ets_model.predict_in_sample()['fitted'] 41 | self.model_params = (ets_model, bias) 42 | return self.fitted 43 | 44 | def predict(self, forecast_horizon, model_params): 45 | prediction = model_params[0].predict(h=forecast_horizon)['mean'] + \ 46 | model_params[1] 47 | return prediction 48 | #%% 49 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/fast_ses_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 4 | 5 | 6 | class FastSESModel(TrendBaseModel): 7 | """Fast simple exponential smoother Model from Statsforecast""" 8 | model = 'fast_ses' 9 | 10 | def __init__(self): 11 | self.model_params = None 12 | self.fitted = None 13 | self._online_steps = 0 14 | 15 | def __str__(self): 16 | return f'{self.model}(optimized)' 17 | 18 | def fit(self, y, **kwargs): 19 | """ 20 | Fit the trend component in the boosting loop for a fast ses model. 21 | 22 | Parameters 23 | ---------- 24 | time_series : TYPE 25 | DESCRIPTION. 26 | **kwargs : TYPE 27 | DESCRIPTION. 28 | 29 | Returns 30 | ------- 31 | None. 32 | 33 | """ 34 | try: 35 | from statsforecast.models import SimpleExponentialSmoothingOptimized 36 | except Exception: 37 | raise ValueError('Using Fast implementations requires an optional dependency Statsforecast: pip install statsforecast') 38 | self.kwargs = kwargs 39 | bias = kwargs['bias'] 40 | ets_model = SimpleExponentialSmoothingOptimized().fit(y - bias) 41 | self.fitted = ets_model.predict_in_sample()['fitted'] 42 | self.model_params = (ets_model, bias) 43 | return self.fitted 44 | 45 | def predict(self, forecast_horizon, model_params): 46 | prediction = model_params[0].predict(h=forecast_horizon)['mean'] + \ 47 | model_params[1] 48 | return prediction 49 | #%% 50 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/fast_theta_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | 4 | 5 | class FastThetaModel(TrendBaseModel): 6 | """Fast Theta Model from Statsforecast""" 7 | model = 'fast_theta' 8 | 9 | def __init__(self): 10 | self.model_params = None 11 | self.fitted = None 12 | self._online_steps = 0 13 | 14 | def __str__(self): 15 | return f'{self.model}({self.kwargs["arima_order"]})' 16 | 17 | def fit(self, y, **kwargs): 18 | """ 19 | Fit the trend component in the boosting loop for a fast ets model. 20 | 21 | Parameters 22 | ---------- 23 | time_series : TYPE 24 | DESCRIPTION. 25 | **kwargs : TYPE 26 | DESCRIPTION. 27 | 28 | Returns 29 | ------- 30 | None. 31 | 32 | """ 33 | try: 34 | from statsforecast.models import AutoTheta 35 | except Exception: 36 | raise ValueError('Using Fast implementations requires an optional dependency Statsforecast: pip install statsforecast') 37 | self.kwargs = kwargs 38 | bias = kwargs['bias'] 39 | ets_model = AutoTheta(season_length=1, 40 | decomposition_type="additive").fit(y - bias) 41 | self.fitted = ets_model.predict_in_sample()["fitted"] 42 | self.model_params = (ets_model, bias) 43 | return self.fitted 44 | 45 | def predict(self, forecast_horizon, model_params): 46 | prediction = model_params[0].predict(h=forecast_horizon)['mean'] + \ 47 | model_params[1] 48 | return prediction 49 | #%% 50 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/lbf_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 4 | import numpy as np 5 | import pandas as pd 6 | from sklearn import linear_model 7 | 8 | 9 | class LinearBasisFunction: 10 | 11 | def __init__(self, 12 | n_changepoints, 13 | decay=None, 14 | weighted=True, 15 | basis_difference=False): 16 | self.n_changepoints = n_changepoints 17 | self.decay = decay 18 | self.weighted = weighted 19 | self.basis_difference = basis_difference 20 | 21 | def get_basis(self, y): 22 | y = y.copy() 23 | y -= y[0] 24 | mean_y = np.mean(y) 25 | self.length = len(y) 26 | n_changepoints = self.n_changepoints 27 | array_splits = np.array_split(np.array(y),n_changepoints + 1)[:-1] 28 | if self.weighted: 29 | initial_point = y[0] 30 | final_point = y[-1] 31 | else: 32 | initial_point = 0 33 | final_point = 0 34 | changepoints = np.zeros(shape=(len(y), n_changepoints+1)) 35 | len_splits = 0 36 | for i in range(n_changepoints): 37 | len_splits += len(array_splits[i]) 38 | if self.weighted: 39 | # moving_point = array_splits[i][-1] 40 | moving_point = np.mean(array_splits[i]) 41 | else: 42 | moving_point = 1 43 | left_basis = np.linspace(initial_point, 44 | moving_point, 45 | len_splits) 46 | end_point = self.add_decay(moving_point, final_point, mean_y) 47 | right_basis = np.linspace(moving_point, 48 | end_point, 49 | len(y) - len_splits + 1) 50 | changepoints[:, i] = np.append(left_basis, right_basis[1:]) 51 | changepoints[:, i+1] = np.arange(0, len(y)) 52 | if self.basis_difference and self.n_changepoints > 1: 53 | r,c = np.triu_indices(changepoints.shape[1],1) 54 | changepoints = changepoints[:,r] - changepoints[:,c] 55 | return changepoints 56 | 57 | def add_decay(self, moving_point, final_point, mean_point): 58 | if self.decay is None: 59 | return final_point 60 | else: 61 | if self.decay == 'auto': 62 | dd = max(.001, min(.99, moving_point**2 / (mean_point**2))) 63 | return moving_point - ((moving_point - final_point) * (1 - dd)) 64 | else: 65 | return moving_point - ((moving_point - final_point) * (1 - self.decay)) 66 | 67 | def get_future_basis(self, basis_functions, forecast_horizon): 68 | n_components = np.shape(basis_functions)[1] 69 | slopes = np.gradient(basis_functions)[0][-1, :] 70 | future_basis = np.array(np.arange(0, forecast_horizon + 1)) 71 | future_basis += len(basis_functions) 72 | future_basis = np.transpose([future_basis] * n_components) 73 | future_basis = future_basis * slopes 74 | future_basis = future_basis + (basis_functions[-1, :] - future_basis[0, :]) 75 | return future_basis[1:, :] 76 | 77 | class LbfModel(TrendBaseModel): 78 | """ 79 | Fitting this with a 'local' fit_type parameter is not advised. 80 | """ 81 | model = 'lbf' 82 | 83 | def __init__(self): 84 | self.model_params = None 85 | self.fitted = None 86 | self._online_steps = 0 87 | 88 | def __str__(self): 89 | return f'{self.model}({self.kwargs["n_changepoints"]})' 90 | 91 | def fit(self, y, **kwargs): 92 | """ 93 | Fit the trend component in the boosting loop for a ewm model using the 'ewm_alpha' parameter. 94 | 95 | Parameters 96 | ---------- 97 | time_series : np.ndarray 98 | DESCRIPTION. 99 | **kwargs : 100 | The key 'ewm_alpha' is passed to this method from the ThymeBoost fit method. 101 | 102 | Returns 103 | ------- 104 | Fitted array. 105 | 106 | """ 107 | self.kwargs = kwargs 108 | n_changepoints = kwargs['n_changepoints'] 109 | bias = kwargs['bias'] 110 | weight = kwargs['trend_weights'] 111 | alpha = kwargs['alpha'] 112 | if n_changepoints < 1: 113 | n_changepoints = int(n_changepoints * len(y)) 114 | if alpha is None: 115 | alpha = .001 116 | lbf = LinearBasisFunction(n_changepoints, 117 | decay='auto') 118 | X = lbf.get_basis(y) 119 | y = pd.Series(y) # - bias) 120 | clf = linear_model.Lasso(alpha=alpha) 121 | clf.fit(X, y, sample_weight=weight) 122 | self.fitted = clf.predict(X)# + bias 123 | self.model_params = [lbf, X, clf] 124 | return self.fitted 125 | 126 | def predict(self, forecast_horizon, model_params): 127 | clf = model_params[2] 128 | lbf = model_params[0] 129 | future_X = lbf.get_future_basis(model_params[1], 130 | forecast_horizon) 131 | return clf.predict(future_X) 132 | 133 | #%% 134 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/linear_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | import numpy as np 4 | from sklearn.preprocessing import PolynomialFeatures 5 | 6 | class LinearModel(TrendBaseModel): 7 | model = 'linear' 8 | 9 | def __init__(self): 10 | self.model_params = None 11 | self.fitted = None 12 | self._online_steps = 0 13 | 14 | def __str__(self): 15 | return f'{self.model}({self.kwargs["poly"], self.kwargs["l2"]})' 16 | 17 | def get_polynomial_expansion(self, X, poly): 18 | """ 19 | Polynomial expansion for curvey lines! 20 | poly == 2 => trend = b1*x1 + b2*x1^2 21 | 22 | Parameters 23 | ---------- 24 | X : np.array 25 | Input X matrix. 26 | poly : int 27 | Order of the expansion. 28 | 29 | Returns 30 | ------- 31 | np.array 32 | X matrix with expansion. 33 | 34 | """ 35 | return PolynomialFeatures(degree=poly, include_bias=False).fit(X).transform(X) 36 | 37 | def add_constant(self, X): 38 | """ 39 | Add constant to X matrix. Used to allow intercept changes in the split. 40 | But main purpose is to allow left split to have intercept but constrain right split for connectivity. 41 | 42 | Parameters 43 | ---------- 44 | X : np.array 45 | Input X matrix. 46 | 47 | Returns 48 | ------- 49 | np.array 50 | X matrix with constant term. 51 | 52 | """ 53 | return np.append(X, np.asarray(np.ones(len(X))).reshape(len(X), 1), axis = 1) 54 | 55 | def ridge_regression(self, X, y, l2): 56 | """ 57 | Equation to derive coefficients with a ridge constrain: l2, may not be super useful but here it is. 58 | 59 | Parameters 60 | ---------- 61 | X : np.array 62 | Input X matrix. 63 | y : np.array 64 | Input time series. 65 | l2 : float 66 | Ridge constraint, obviously scale dependent so beware! 67 | Returns 68 | ------- 69 | np.array 70 | Our ridge beta coefficients to get predictions. 71 | 72 | """ 73 | return np.linalg.pinv(X.T.dot(X) + l2*np.eye(X.shape[1])).dot(X.T.dot(y)) 74 | 75 | def wls(self, X, y, weight): 76 | """ 77 | Simple WLS where weighting is based on previous error. If True, then our take on a IRLS scheme in the boosting loop. 78 | If iterable then apply those weights assuming these are sample weights. 79 | ToDo: Does IRLS like this even work ok? 80 | 81 | Parameters 82 | ---------- 83 | X : np.array 84 | Input X matrix. 85 | y : np.array 86 | Input time series. 87 | weight : boolean/np.array 88 | if True then apply IRLS weighting scheme, else apply sample weights. 89 | Returns 90 | ------- 91 | np.array 92 | Our beta coefficients to get predictions. 93 | 94 | """ 95 | if isinstance(weight, bool) and weight: 96 | #since we are boosting y is our error term from last iteration, so this works right? 97 | weight = np.diag(1/(y.reshape(-1,)**2)) 98 | weighted_X_T = X.T @ weight 99 | return np.linalg.pinv(weighted_X_T.dot(X)).dot(weighted_X_T.dot(y)) 100 | 101 | def ols(self, X, y): 102 | """ 103 | Simple OLS with normal equation. Obviously we have a singluar matrix so we use pinv. 104 | ToDo: Look to implement faster equations for simple trend lines to speed up comp time. 105 | 106 | Parameters 107 | ---------- 108 | X : np.array 109 | Input X matrix. 110 | y : np.array 111 | Input time series. 112 | 113 | Returns 114 | ------- 115 | np.array 116 | Our beta coefficients to get predictions. 117 | 118 | """ 119 | return np.linalg.pinv(X.T.dot(X)).dot(X.T.dot(y)) 120 | 121 | def fit(self, y, **kwargs): 122 | """ 123 | Fit the trend component in the boosting loop for a collection of linear models. 124 | 125 | Parameters 126 | ---------- 127 | time_series : TYPE 128 | DESCRIPTION. 129 | **kwargs : TYPE 130 | DESCRIPTION. 131 | 132 | Returns 133 | ------- 134 | None. 135 | 136 | """ 137 | self.kwargs = kwargs 138 | bias = kwargs['bias'] 139 | poly = kwargs['poly'] 140 | fit_constant = kwargs['fit_constant'] 141 | weight = kwargs['trend_weights'] 142 | l2 = kwargs['l2'] 143 | if bias and weight is not None: 144 | weight = weight[-len(y):] 145 | elif weight is not None: 146 | weight = weight[:len(y)] 147 | y = y - bias 148 | y = (y).reshape((-1, 1)) 149 | X = np.array(list(range(len(y))), ndmin=1).reshape((-1, 1)) 150 | if poly > 1: 151 | X = self.get_polynomial_expansion(X, poly) 152 | if fit_constant: 153 | X = self.add_constant(X) 154 | if l2: 155 | beta = self.ridge_regression(X, y, l2) 156 | elif weight is not None: 157 | beta = self.wls(X, y, weight) 158 | else: 159 | beta = self.ols(X, y) 160 | self.fitted = X.dot(beta) + bias 161 | slope = self.fitted[-1] - self.fitted[-2] 162 | self.model_params = (slope, self.fitted[-1]) 163 | return self.fitted.reshape(-1, ) 164 | 165 | def predict(self, forecast_horizon, model_params): 166 | last_fitted_value = model_params[1] 167 | slope = model_params[0] 168 | predicted = np.arange(1, self._online_steps + forecast_horizon + \ 169 | 1) * slope + last_fitted_value 170 | return predicted[-forecast_horizon:].reshape(-1, ) 171 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/loess_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | import numpy as np 4 | from scipy.signal import savgol_filter 5 | 6 | 7 | class LoessModel(TrendBaseModel): 8 | model = 'loess' 9 | 10 | def __init__(self): 11 | self.model_params = None 12 | self.fitted = None 13 | self._online_steps = 0 14 | 15 | def __str__(self): 16 | return f'{self.model}({self.kwargs["poly"], self.kwargs["window_size"]})' 17 | 18 | def fit(self, y, **kwargs): 19 | """ 20 | Fit the trend component in the boosting loop for a ewm model using alpha. 21 | 22 | Parameters 23 | ---------- 24 | time_series : TYPE 25 | DESCRIPTION. 26 | **kwargs : TYPE 27 | DESCRIPTION. 28 | 29 | Returns 30 | ------- 31 | None. 32 | 33 | """ 34 | self.kwargs = kwargs 35 | window_size = kwargs['window_size'] 36 | bias = kwargs['bias'] 37 | poly = kwargs['poly'] 38 | self.fitted = savgol_filter(y - bias, window_size, poly) + bias 39 | slope = self.fitted[-1] - self.fitted[-2] 40 | last_value = self.fitted[-1] 41 | self.model_params = (slope, last_value) 42 | return self.fitted 43 | 44 | def predict(self, forecast_horizon, model_params): 45 | last_fitted_value = model_params[1] 46 | slope = model_params[0] 47 | predicted = np.arange(1, forecast_horizon + 1) * slope + last_fitted_value 48 | return predicted 49 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/mean_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | import numpy as np 4 | 5 | 6 | class MeanModel(TrendBaseModel): 7 | model = 'mean' 8 | 9 | def __init__(self): 10 | self.model_params = None 11 | self.fitted = None 12 | self._online_steps = 0 13 | 14 | def __str__(self): 15 | return f'{self.model}()' 16 | 17 | def fit(self, y, **kwargs): 18 | """ 19 | Fit the trend component in the boosting loop for a mean model. 20 | 21 | Parameters 22 | ---------- 23 | time_series : TYPE 24 | DESCRIPTION. 25 | **kwargs : TYPE 26 | DESCRIPTION. 27 | 28 | Returns 29 | ------- 30 | None. 31 | 32 | """ 33 | mean_est = np.mean(y) 34 | self.model_params = mean_est 35 | self.fitted = np.tile(mean_est, len(y)) 36 | return self.fitted 37 | 38 | def predict(self, forecast_horizon, model_params): 39 | return np.tile(model_params, forecast_horizon) 40 | 41 | 42 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/median_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | import numpy as np 4 | 5 | 6 | class MedianModel(TrendBaseModel): 7 | model = 'median' 8 | 9 | def __init__(self): 10 | self.model_params = None 11 | self.fitted = None 12 | self._online_steps = 0 13 | 14 | def __str__(self): 15 | return f'{self.model}()' 16 | 17 | def fit(self, y, **kwargs): 18 | """ 19 | Fit the trend component in the boosting loop for a mean model. 20 | 21 | Parameters 22 | ---------- 23 | time_series : TYPE 24 | DESCRIPTION. 25 | **kwargs : TYPE 26 | DESCRIPTION. 27 | 28 | Returns 29 | ------- 30 | None. 31 | 32 | """ 33 | median_est = np.median(y) 34 | self.model_params = median_est 35 | self.fitted = np.tile(median_est, len(y)) 36 | return self.fitted 37 | 38 | def predict(self, forecast_horizon, model_params): 39 | return np.tile(model_params, forecast_horizon) 40 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/moving_average_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | import numpy as np 4 | import pandas as pd 5 | 6 | class MovingAverageModel(TrendBaseModel): 7 | """A simple moving average utilizing the rolling functionality of pandas. 8 | The only argument to pass from ThymeBoost's fit is the 'window_size' parameter. 9 | """ 10 | model = 'moving_average' 11 | 12 | def __init__(self): 13 | self.model_params = None 14 | self.fitted = None 15 | self._online_steps = 0 16 | 17 | def __str__(self): 18 | return f'{self.model}({self.kwargs["window_size"]})' 19 | 20 | def fit(self, y, **kwargs): 21 | """ 22 | Fit the trend component in the boosting loop for a ewm model using alpha. 23 | 24 | Parameters 25 | ---------- 26 | time_series : TYPE 27 | DESCRIPTION. 28 | **kwargs : 29 | Key 1: window_size: the size of the window used to average while rolling through series. 30 | 31 | Returns 32 | ------- 33 | None. 34 | 35 | """ 36 | self.kwargs = kwargs 37 | window = kwargs['window_size'] 38 | bias = kwargs['bias'] 39 | y = pd.Series(y - bias) 40 | self.fitted = np.array(y.rolling(window).mean().fillna(method='backfill')) + bias 41 | last_fitted_values = self.fitted[-1] 42 | self.model_params = last_fitted_values 43 | return self.fitted 44 | 45 | def predict(self, forecast_horizon, model_params): 46 | return np.tile(model_params, forecast_horizon) 47 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/naive_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | import numpy as np 4 | 5 | 6 | class NaiveModel(TrendBaseModel): 7 | model = 'naive' 8 | 9 | def __init__(self): 10 | self.model_params = None 11 | self.fitted = None 12 | self._online_steps = 0 13 | 14 | def __str__(self): 15 | return f'{self.model}()' 16 | 17 | def fit(self, y, **kwargs): 18 | """ 19 | Fit the trend component in the boosting loop for a naive/random-walk model AKA the last value. 20 | 21 | Parameters 22 | ---------- 23 | time_series : TYPE 24 | DESCRIPTION. 25 | **kwargs : TYPE 26 | DESCRIPTION. 27 | 28 | Returns 29 | ------- 30 | None. 31 | 32 | """ 33 | self.model_params = y[-1] 34 | y = np.append(np.array(y[0]), y) 35 | y = y[:-1] 36 | self.fitted = y 37 | return self.fitted 38 | 39 | def predict(self, forecast_horizon, model_params): 40 | return np.tile(model_params, forecast_horizon) 41 | 42 | 43 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/ransac_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | import numpy as np 4 | from sklearn.preprocessing import PolynomialFeatures 5 | from sklearn.linear_model import RANSACRegressor 6 | from sklearn.linear_model import LinearRegression 7 | 8 | class RansacModel(TrendBaseModel): 9 | """Uses sklearn's RANSACRegressor method to build a robust regression. 10 | The parameters that can be passed for this trend are: 11 | ransac_min_samples 12 | ransac_trials 13 | poly 14 | fit_constant 15 | For more info: https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.RANSACRegressor.html 16 | """ 17 | model = 'ransac' 18 | 19 | def __init__(self): 20 | self.model_params = None 21 | self.fitted = None 22 | self._online_steps = 0 23 | 24 | def __str__(self): 25 | return f'{self.model}({self.kwargs["poly"]})' 26 | 27 | def get_polynomial_expansion(self, X, poly): 28 | """ 29 | Polynomial expansion for curvey lines! 30 | poly == 2 => trend = b1*x1 + b2*x1^2 31 | 32 | Parameters 33 | ---------- 34 | X : np.array 35 | Input X matrix. 36 | poly : int 37 | Order of the expansion. 38 | 39 | Returns 40 | ------- 41 | np.array 42 | X matrix with expansion. 43 | """ 44 | return PolynomialFeatures(degree=poly, include_bias=False).fit(X).transform(X) 45 | 46 | def add_constant(self, X): 47 | """ 48 | Add constant to X matrix. Used to allow intercept changes in the split. 49 | But main purpose is to allow left split to have intercept but constrain right split for connectivity. 50 | 51 | Parameters 52 | ---------- 53 | X : np.array 54 | Input X matrix. 55 | 56 | Returns 57 | ------- 58 | np.array 59 | X matrix with constant term. 60 | 61 | """ 62 | return np.append(X, np.asarray(np.ones(len(X))).reshape(len(X), 1), axis = 1) 63 | 64 | def fit(self, y, **kwargs): 65 | """ 66 | Fit the trend component in the boosting loop for a collection of linear models. 67 | 68 | Parameters 69 | ---------- 70 | time_series : TYPE 71 | DESCRIPTION. 72 | **kwargs : TYPE 73 | DESCRIPTION. 74 | 75 | Returns 76 | ------- 77 | None. 78 | 79 | """ 80 | self.kwargs = kwargs 81 | bias = kwargs['bias'] 82 | poly = kwargs['poly'] 83 | fit_constant = kwargs['fit_constant'] 84 | trials = kwargs['ransac_trials'] 85 | min_samples = kwargs['ransac_min_samples'] 86 | y = y - bias 87 | y = (y).reshape((-1, 1)) 88 | X = np.array(list(range(len(y))), ndmin=1).reshape((-1, 1)) 89 | if poly > 1: 90 | X = self.get_polynomial_expansion(X, poly) 91 | if fit_constant: 92 | X = self.add_constant(X) 93 | model = RANSACRegressor(LinearRegression(fit_intercept=False), 94 | min_samples=min_samples, 95 | max_trials=trials, 96 | random_state= 32) 97 | model.fit(X, y) 98 | self.fitted = model.predict(X) + bias 99 | slope = self.fitted[-1] - self.fitted[-2] 100 | last_value = self.fitted[-1] 101 | self.model_params = (slope, last_value) 102 | return self.fitted.reshape(-1, ) 103 | 104 | def predict(self, forecast_horizon, model_params): 105 | last_fitted_value = model_params[1] 106 | slope = model_params[0] 107 | predicted = np.arange(1, forecast_horizon + 1) * slope + last_fitted_value 108 | return predicted 109 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/svr_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | import numpy as np 4 | from sklearn.svm import SVR 5 | from sklearn.model_selection import GridSearchCV 6 | 7 | class SvrModel(TrendBaseModel): 8 | model = 'svr' 9 | 10 | def __init__(self): 11 | self.model_params = None 12 | self.fitted = None 13 | self._online_steps = 0 14 | 15 | def __str__(self): 16 | return f'{self.model}()' 17 | 18 | 19 | 20 | def add_constant(self, X): 21 | """ 22 | Add constant to X matrix. Used to allow intercept changes in the split. 23 | But main purpose is to allow left split to have intercept but constrain right split for connectivity. 24 | 25 | Parameters 26 | ---------- 27 | X : np.array 28 | Input X matrix. 29 | 30 | Returns 31 | ------- 32 | np.array 33 | X matrix with constant term. 34 | 35 | """ 36 | return np.append(X, np.asarray(np.ones(len(X))).reshape(len(X), 1), axis = 1) 37 | 38 | def fit(self, y, **kwargs): 39 | """ 40 | Fit the trend component in the boosting loop for a SVR. 41 | 42 | Parameters 43 | ---------- 44 | time_series : TYPE 45 | DESCRIPTION. 46 | **kwargs : TYPE 47 | DESCRIPTION. 48 | 49 | Returns 50 | ------- 51 | None. 52 | 53 | """ 54 | self.kwargs = kwargs 55 | bias = kwargs['bias'] 56 | fit_constant = kwargs['fit_constant'] 57 | y = y - bias 58 | y = (y).reshape((-1, 1)) 59 | X = np.array(list(range(len(y))), ndmin=1).reshape((-1, 1)) 60 | if fit_constant: 61 | X = self.add_constant(X) 62 | # from Sklearn example 63 | # TODO: generalize 64 | svr = GridSearchCV( 65 | SVR(kernel="rbf", gamma=0.1), 66 | param_grid={"C": [1e0, 1e1, 1e2, 1e3], "gamma": np.logspace(-2, 2, 5)}, 67 | ) 68 | svr.fit(X, y) 69 | self.fitted = svr.predict(X) + bias 70 | slope = self.fitted[-1] - self.fitted[-2] 71 | last_value = self.fitted[-1] 72 | self.model_params = (slope, last_value) 73 | return self.fitted 74 | 75 | def predict(self, forecast_horizon, model_params): 76 | last_fitted_value = model_params[1] 77 | slope = model_params[0] 78 | predicted = np.arange(1, forecast_horizon + 1) * slope + last_fitted_value 79 | return predicted 80 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/theta_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | import numpy as np 4 | import pandas as pd 5 | from statsmodels.tsa.forecasting.theta import ThetaModel 6 | 7 | class ThetaModel(TrendBaseModel): 8 | model = 'theta' 9 | 10 | def __init__(self): 11 | self.model_params = None 12 | self.fitted = None 13 | self._online_steps = 0 14 | 15 | def __str__(self): 16 | return f'{self.model}()' 17 | 18 | def fit(self, y, **kwargs): 19 | """ 20 | Fit the trend component in the boosting loop for an optimized theta model. 21 | 22 | Parameters 23 | ---------- 24 | time_series : TYPE 25 | DESCRIPTION. 26 | **kwargs : TYPE 27 | DESCRIPTION. 28 | 29 | Returns 30 | ------- 31 | None. 32 | 33 | """ 34 | self.kwargs = kwargs 35 | bias = kwargs['bias'] 36 | y -= bias 37 | theta_model = ThetaModel(y, method="additive", period=1) + bias 38 | fitted = theta_model.fit() 39 | self.fitted = theta_model 40 | last_fitted_values = self.fitted[-1] 41 | self.model_params = last_fitted_values 42 | return self.fitted 43 | 44 | def predict(self, forecast_horizon, model_params): 45 | return np.tile(model_params, forecast_horizon) 46 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/trend_base_class.py: -------------------------------------------------------------------------------- 1 | 2 | from abc import ABC, abstractmethod 3 | import numpy as np 4 | import pandas as pd 5 | from ThymeBoost.cost_functions import get_split_cost 6 | 7 | class TrendBaseModel(ABC): 8 | 9 | @abstractmethod 10 | def __init__(self): 11 | pass 12 | 13 | @abstractmethod 14 | def fit(self, y, **kwargs): 15 | """ 16 | Fit the trend component in the boosting loop. 17 | 18 | Parameters 19 | ---------- 20 | time_series : TYPE 21 | DESCRIPTION. 22 | **kwargs : TYPE 23 | DESCRIPTION. 24 | 25 | Returns 26 | ------- 27 | None. 28 | 29 | """ 30 | pass 31 | 32 | @abstractmethod 33 | def predict(self, forecast_horizon): 34 | pass 35 | 36 | def __add__(self, trend_obj): 37 | """ 38 | Add two trend obj together, useful for ensembling or just quick updating of trend components. 39 | 40 | Parameters 41 | ---------- 42 | trend_obj : TYPE 43 | DESCRIPTION. 44 | 45 | Returns 46 | ------- 47 | TYPE 48 | DESCRIPTION. 49 | 50 | """ 51 | return self.fitted + trend_obj.fitted 52 | 53 | def __mul__(self, trend_obj): 54 | return self.fitted * trend_obj.fitted 55 | 56 | def __div__(self, trend_obj): 57 | return self.fitted / trend_obj.fitted 58 | 59 | def __sub__(self, trend_obj): 60 | return self.fitted - trend_obj.fitted 61 | 62 | def append(self, trend_obj): 63 | return np.append(self.fitted, trend_obj.fitted) 64 | 65 | def to_series(self, array): 66 | return pd.Series(array) 67 | 68 | def _split_cost(self, time_series, cost_function: str, trend_obj=None): 69 | return get_split_cost(time_series, 70 | self.fitted, 71 | trend_obj.fitted, 72 | cost_function) 73 | 74 | def __str__(cls): 75 | return f'{cls.model} model' 76 | 77 | def get_frequency(self, index): 78 | if len(index) > 2: 79 | freq = pd.infer_freq(index) 80 | elif len(index) == 2: 81 | time_delta = (index[-1] - index[0]).days 82 | if time_delta > 2 and time_delta < 20: 83 | freq = 'W' 84 | elif time_delta < 2: 85 | freq = 'D' 86 | elif time_delta > 20 and time_delta < 40: 87 | freq = 'M' 88 | elif time_delta > 300: 89 | freq = 'Y' 90 | else: 91 | #safe setting to month 92 | freq = 'M' 93 | return freq 94 | -------------------------------------------------------------------------------- /ThymeBoost/trend_models/zero_trend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from ThymeBoost.trend_models.trend_base_class import TrendBaseModel 3 | import numpy as np 4 | import pandas as pd 5 | 6 | class ZeroModel(TrendBaseModel): 7 | """A utility trend method. If you do not want to center or detrend your data before approximating seasonality then it is useful. 8 | """ 9 | model = 'zero' 10 | 11 | def __init__(self): 12 | self.model_params = None 13 | self.fitted = None 14 | self._online_steps = 0 15 | 16 | def __str__(self): 17 | return f'{self.model}()' 18 | 19 | def fit(self, y, **kwargs): 20 | """ 21 | A utility trend that simply returns 0 22 | 23 | Parameters 24 | ---------- 25 | time_series : TYPE 26 | DESCRIPTION. 27 | **kwargs : TYPE 28 | DESCRIPTION. 29 | 30 | Returns 31 | ------- 32 | None. 33 | 34 | """ 35 | self.fitted = np.zeros(len(y)) 36 | self.model_params = 0 37 | return self.fitted 38 | 39 | def predict(self, forecast_horizon, model_params): 40 | return np.zeros(forecast_horizon) -------------------------------------------------------------------------------- /ThymeBoost/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | -------------------------------------------------------------------------------- /ThymeBoost/utils/__pycache__/build_output.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/utils/__pycache__/build_output.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/utils/__pycache__/get_complexity.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/utils/__pycache__/get_complexity.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/utils/__pycache__/plotting.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/utils/__pycache__/plotting.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/utils/__pycache__/trend_dampen.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/ThymeBoost/utils/__pycache__/trend_dampen.cpython-37.pyc -------------------------------------------------------------------------------- /ThymeBoost/utils/build_output.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pandas as pd 4 | import numpy as np 5 | from scipy import stats 6 | from ThymeBoost.utils.trend_dampen import trend_dampen 7 | 8 | 9 | class BuildOutput: 10 | 11 | def __init__(self, 12 | time_series, 13 | time_series_index, 14 | scaler_obj, 15 | c): 16 | self.time_series = time_series 17 | self.time_series_index = time_series_index 18 | self.scaler_obj = scaler_obj 19 | self.c = c 20 | 21 | def handle_future_index(self, forecast_horizon): 22 | if isinstance(self.time_series_index, pd.DatetimeIndex): 23 | last_date = self.time_series_index[-1] 24 | freq = pd.infer_freq(self.time_series_index) 25 | future_index = pd.date_range(last_date, 26 | periods=forecast_horizon + 1, 27 | freq=freq)[1:] 28 | else: 29 | future_index = np.arange(len(self.time_series_index) + forecast_horizon) 30 | future_index = future_index[-forecast_horizon:] 31 | return future_index 32 | 33 | @staticmethod 34 | def get_fitted_intervals(y, fitted): 35 | """ 36 | A interval calculation based on linear regression, only useful for non-smoother/state space/local models. 37 | TODO: Look for generalized approach, possibly using rolling predicted residuals? 38 | 39 | Parameters 40 | ---------- 41 | y : TYPE 42 | DESCRIPTION. 43 | fitted : TYPE 44 | DESCRIPTION. 45 | c : TYPE 46 | DESCRIPTION. 47 | 48 | Returns 49 | ------- 50 | upper : TYPE 51 | DESCRIPTION. 52 | lower : TYPE 53 | DESCRIPTION. 54 | """ 55 | n = len(y) 56 | # denom = n * np.sum((y - np.mean(y))**2) 57 | sd_error = np.sqrt((1 / max(1, (n - 2))) * np.sum((y - np.mean(fitted))**2)) 58 | # sd_error = np.sqrt(top / denom) 59 | # sd_error = np.std(y - fitted) 60 | t_stat = stats.t.ppf(.9, len(y)) 61 | upper = fitted + t_stat*sd_error 62 | lower = fitted - t_stat*sd_error 63 | return upper, lower 64 | 65 | @staticmethod 66 | def get_predicted_intervals(y, fitted, predicted, c, uncertainty): 67 | """ 68 | A interval calculation based on linear regression with forecast penalty, 69 | only semi-useful for non-smoother/state space/local models. 70 | TODO: Look for generalized approach, possibly using rolling predicted residuals? 71 | 72 | Parameters 73 | ---------- 74 | y : TYPE 75 | DESCRIPTION. 76 | fitted : TYPE 77 | DESCRIPTION. 78 | c : TYPE 79 | DESCRIPTION. 80 | 81 | Returns 82 | ------- 83 | upper : TYPE 84 | DESCRIPTION. 85 | lower : TYPE 86 | DESCRIPTION. 87 | 88 | """ 89 | n = len(y) 90 | # denom = n * np.sum((y - np.mean(y))**2) 91 | sd_error = np.sqrt((1 / max(1, (n - 2))) * np.sum((y - np.mean(fitted))**2)) 92 | # sd_error = np.sqrt(top / denom) 93 | # sd_error = np.std(y - fitted) 94 | t_stat = stats.t.ppf(.9, len(y)) 95 | len_frac = len(predicted)/len(fitted) 96 | if uncertainty: 97 | interval_uncertainty_param = np.linspace(1.0, 98 | 1.0 + 3*len_frac, 99 | len(predicted)) 100 | else: 101 | interval_uncertainty_param = np.ones(len(predicted)) 102 | upper = predicted + t_stat*sd_error*interval_uncertainty_param 103 | lower = predicted - t_stat*sd_error*interval_uncertainty_param 104 | return upper, lower 105 | 106 | def build_fitted_df(self, 107 | trend, 108 | seasonality, 109 | exogenous): 110 | time_series = self.scaler_obj(pd.Series(self.time_series)) 111 | output = pd.DataFrame(time_series.values, 112 | index=self.time_series_index, 113 | columns=['y']) 114 | yhat = trend + seasonality 115 | if exogenous is not None: 116 | yhat += exogenous 117 | upper_fitted, lower_fitted = self.get_fitted_intervals(self.scaler_obj(self.time_series), 118 | self.scaler_obj(yhat)) 119 | if exogenous is not None: 120 | output['exogenous'] = exogenous 121 | # output['Exogenous Summary'] = self.get_boosted_exo_results(exo_impact) 122 | # self.exo_impact = exo_impact 123 | output['yhat'] = self.scaler_obj(yhat) 124 | output['yhat_upper'] = upper_fitted 125 | output['yhat_lower'] = lower_fitted 126 | output['seasonality'] = self.scaler_obj(seasonality) 127 | output['trend'] = self.scaler_obj(trend) 128 | return output 129 | 130 | def build_predicted_df(self, 131 | fitted_output, 132 | forecast_horizon, 133 | trend, 134 | seasonality, 135 | exogenous, 136 | predictions, 137 | trend_cap_target, 138 | damp_factor, 139 | uncertainty 140 | ): 141 | if trend_cap_target is not None: 142 | predicted_trend_perc = (trend[-1] - trend[0]) / trend[0] 143 | trend_change = trend_cap_target / predicted_trend_perc 144 | damp_factor = max(0, 1 - trend_change) 145 | if damp_factor is not None: 146 | trend = trend_dampen(damp_factor, 147 | trend).values 148 | future_index = self.handle_future_index(forecast_horizon) 149 | bounds = self.get_predicted_intervals(self.scaler_obj(self.time_series), 150 | fitted_output['yhat'], 151 | self.scaler_obj(predictions), 152 | c=self.c, 153 | uncertainty=uncertainty) 154 | predicted_output = pd.DataFrame(self.scaler_obj(predictions), 155 | index=future_index, 156 | columns=['predictions']) 157 | upper_prediction, lower_prediction = bounds 158 | predicted_output['predicted_trend'] = self.scaler_obj(trend) 159 | predicted_output['predicted_seasonality'] = self.scaler_obj(seasonality) 160 | if exogenous is not None: 161 | predicted_output['predicted_exogenous'] = self.scaler_obj(exogenous) 162 | predicted_output['predicted_upper'] = upper_prediction 163 | predicted_output['predicted_lower'] = lower_prediction 164 | return predicted_output 165 | -------------------------------------------------------------------------------- /ThymeBoost/utils/get_complexity.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import numpy as np 4 | 5 | 6 | def get_complexity(boosting_round, 7 | poly, 8 | fit_type, 9 | trend_estimator, 10 | arima_order, 11 | window_size, 12 | time_series, 13 | fourier_order, 14 | seasonal_period, 15 | exogenous): 16 | #Get a measure of complexity: number of splits + any extra variables 17 | if fit_type == 'global' and trend_estimator == 'linear': 18 | c = 1 19 | elif fit_type == 'global' and trend_estimator == 'loess': 20 | c = int(len(time_series) / window_size) 21 | elif fit_type == 'global' and trend_estimator == 'ar': 22 | c = np.sum(arima_order) 23 | else: 24 | if seasonal_period != 0 and not None: 25 | c = boosting_round + fourier_order 26 | else: 27 | c = boosting_round 28 | if trend_estimator == 'linear' and fit_type == 'local': 29 | c = poly + fourier_order + boosting_round 30 | if exogenous is not None: 31 | input_shape = np.shape(exogenous) 32 | if len(input_shape) == 1: 33 | c += 1 34 | else: 35 | c += input_shape[1] 36 | return c 37 | -------------------------------------------------------------------------------- /ThymeBoost/utils/hyperoptimizer.py: -------------------------------------------------------------------------------- 1 | # #-*- coding: utf-8 -*- 2 | 3 | # from sklearn.model_selection import TimeSeriesSplit 4 | # from sklearn.metrics import mean_squared_error 5 | # import time 6 | # from hyperopt import space_eval 7 | # from sklearn.model_selection import cross_val_score 8 | # from hyperopt import fmin, tpe, hp, STATUS_OK, Trials 9 | # from hyperopt.pyll import scope 10 | # from hyperopt.fmin import fmin 11 | # import numpy as np 12 | # import pandas as pd 13 | # from ThymeBoost import ThymeBoost as tb 14 | 15 | 16 | # class Optimize: 17 | # def __init__(self, 18 | # y, 19 | # seasonal_period=0, 20 | # n_folds=3, 21 | # test_size=None): 22 | # self.y = y 23 | # self.seasonal_period = seasonal_period 24 | # self.n_folds = n_folds 25 | # self.test_size = test_size 26 | 27 | # def logic_layer(self): 28 | # time_series = pd.Series(self.y).copy(deep=True) 29 | # if isinstance(self.seasonal_period, list): 30 | # generator_seasonality = True 31 | # else: 32 | # generator_seasonality = False 33 | # if generator_seasonality: 34 | # max_seasonal_pulse = self.seasonal_period[0] 35 | # else: 36 | # max_seasonal_pulse = self.seasonal_period 37 | # if not generator_seasonality: 38 | # self.seasonal_period = [self.seasonal_period] 39 | # if self.seasonal_period[0]: 40 | # self.seasonal_period = [0, self.seasonal_period, self.seasonal_period.append(0)] 41 | 42 | # _contains_zero = not (time_series > 0).all() 43 | # if _contains_zero: 44 | # self.additive = [True] 45 | # else: 46 | # self.additive = [True, False] 47 | 48 | # if len(time_series) > 2.5 * max_seasonal_pulse and max_seasonal_pulse: 49 | # seasonal_sample_weights = [] 50 | # weight = 1 51 | # for i in range(len(time_series)): 52 | # if (i) % max_seasonal_pulse == 0: 53 | # weight += 1 54 | # seasonal_sample_weights.append(weight) 55 | # self.seasonal_sample_weights = [None, 56 | # np.array(seasonal_sample_weights)] 57 | 58 | # def get_space(self): 59 | # self.space = { 60 | # 'trend_estimator': hp.choice('trends', [{'trend_estimator': 'linear', 61 | # 'poly': hp.choice('exp', [1, 2]), 62 | # 'arima_order': 'auto', 63 | # 'fit_type': hp.choice('cp1', 64 | # ['local', 'global']), 65 | # }, 66 | # {'trend_estimator': 'arima', 67 | # 'poly': 1, 68 | # 'arima_order': 'auto', 69 | # 'fit_type': 'global', 70 | # }, 71 | # {'trend_estimator': 'naive', 72 | # 'poly': 1, 73 | # 'arima_order': 'auto', 74 | # 'fit_type': 'global', 75 | # }, 76 | # {'trend_estimator': 'ses', 77 | # 'poly': 1, 78 | # 'arima_order': 'auto', 79 | # 'fit_type': 'global' 80 | # }, 81 | # {'trend_estimator': ['linear', 'ses'], 82 | # 'poly': 1, 83 | # 'arima_order': 'auto', 84 | # 'fit_type': hp.choice('cp2', 85 | # [['global'], ['local', 'global']]) 86 | # }, 87 | # {'trend_estimator': 'mean', 88 | # 'poly': 1, 89 | # 'arima_order': 'auto', 90 | # 'fit_type': hp.choice('cp3', 91 | # ['global', 'local']) 92 | # }]), 93 | # 'global_cost': hp.choice('gcost', ['mse', 'maicc']), 94 | # 'additive': hp.choice('add', self.additive), 95 | # 'seasonal_estimator': hp.choice('seas', ['fourier', 'naive']), 96 | # 'seasonal_period': hp.choice('seas_period', self.seasonal_period) 97 | # } 98 | # return self.space 99 | 100 | # def scorer(self, model_obj, y, metric, cv, params): 101 | # cv_splits = cv.split(y) 102 | # mses = [] 103 | # for train_index, test_index in cv_splits: 104 | # fitted = model_obj.fit(y[train_index], **params) 105 | # predicted = model_obj.predict(fitted, len(y[test_index])) 106 | # predicted = predicted['predictions'].values 107 | # mses.append(mean_squared_error(y[test_index], predicted)) 108 | # return_dict = {'loss': np.mean(mses), 109 | # 'eval_time': time.time(), 110 | # 'status': STATUS_OK} 111 | # return return_dict 112 | 113 | 114 | # def objective(self, params): 115 | # # print(params) 116 | # if isinstance(params['trend_estimator']['trend_estimator'], tuple): 117 | # params['trend_estimator']['trend_estimator'] = list(params['trend_estimator']['trend_estimator']) 118 | # if isinstance(params['seasonal_period'], tuple): 119 | # params['seasonal_period'] = list(params['seasonal_period']) 120 | # if isinstance(params['trend_estimator']['fit_type'], tuple): 121 | # params['trend_estimator']['fit_type'] = list(params['trend_estimator']['fit_type']) 122 | # params = { 123 | # 'trend_estimator': params['trend_estimator']['trend_estimator'], 124 | # 'fit_type': params['trend_estimator']['fit_type'], 125 | # 'arima_order': params['trend_estimator']['arima_order'], 126 | # 'poly': params['trend_estimator']['poly'], 127 | # 'seasonal_period': params['seasonal_period'], 128 | # 'additive': params['additive'], 129 | # 'global_cost': params['global_cost'], 130 | 131 | # } 132 | # clf = tb.ThymeBoost() 133 | # score = self.scorer(clf, 134 | # self.y, 135 | # mean_squared_error, 136 | # TimeSeriesSplit(self.n_folds, test_size=self.test_size), 137 | # params) 138 | # return score 139 | 140 | # def fit(self): 141 | # self.logic_layer() 142 | # best = fmin(fn=self.objective, 143 | # space=self.get_space(), 144 | # algo=tpe.suggest, 145 | # max_evals=100, 146 | # # early_stop_fn=fn, 147 | # verbose=False) 148 | # # print(best) 149 | # return best 150 | 151 | 152 | # if __name__ == '__main__': 153 | # opt = Optimize(example_data, 154 | # seasonal_period=12, 155 | # n_folds=3, 156 | # test_size=12) 157 | # best_params = opt.fit() -------------------------------------------------------------------------------- /ThymeBoost/utils/plotting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import matplotlib.pyplot as plt 4 | from itertools import cycle 5 | 6 | def plot_components(fitted_df, predicted_df, figsize): 7 | """Simple plot of components for convenience""" 8 | if predicted_df is not None: 9 | rename_dict = {'predictions': 'yhat', 10 | 'predicted_trend': 'trend', 11 | 'predicted_seasonality': 'seasonality', 12 | 'predicted_upper': 'yhat_upper', 13 | 'predicted_lower': 'yhat_lower', 14 | 'predicted_exogenous': 'exogenous'} 15 | predicted_df = predicted_df.rename(rename_dict, 16 | axis=1) 17 | component_df = fitted_df.append(predicted_df) 18 | else: 19 | component_df = fitted_df 20 | if 'exogenous' in fitted_df.columns: 21 | fig, ax = plt.subplots(4, figsize=figsize) 22 | ax[-2].plot(component_df['exogenous'], color='orange') 23 | ax[-2].set_title('Exogenous') 24 | ax[-2].xaxis.set_visible(False) 25 | else: 26 | fig, ax = plt.subplots(3, figsize=figsize) 27 | ax[0].plot(component_df['trend'], color='orange') 28 | ax[0].set_title('Trend') 29 | ax[0].xaxis.set_visible(False) 30 | ax[1].plot(component_df['seasonality'], color='orange') 31 | ax[1].set_title('Seasonality') 32 | ax[1].xaxis.set_visible(False) 33 | ax[-1].plot(component_df['y'], color='black') 34 | ax[-1].plot(component_df['yhat'], color='orange') 35 | ax[-1].plot(component_df['yhat_upper'], 36 | linestyle='dashed', 37 | alpha=.5, 38 | color='orange') 39 | ax[-1].plot(component_df['yhat_lower'], 40 | linestyle='dashed', 41 | alpha=.5, 42 | color='orange') 43 | ax[-1].set_title('Fitted') 44 | plt.tight_layout() 45 | plt.show() 46 | 47 | 48 | def plot_results(fitted_df, predicted_df, figsize): 49 | """Simple plot of results for convenience""" 50 | fig, ax = plt.subplots(figsize=figsize) 51 | ax.plot(fitted_df['y'], color='black') 52 | ax.plot(fitted_df['yhat'], color='orange') 53 | ax.plot(fitted_df['yhat_upper'], 54 | linestyle='dashed', 55 | alpha=.5, 56 | color='orange') 57 | ax.plot(fitted_df['yhat_lower'], 58 | linestyle='dashed', 59 | alpha=.5, 60 | color='orange') 61 | if predicted_df is not None: 62 | ax.plot(fitted_df['yhat'].tail(1).append(predicted_df['predictions']), 63 | color='red', 64 | linestyle='dashed') 65 | ax.fill_between(x=fitted_df['yhat_lower'].tail(1).append(predicted_df['predicted_lower']).index, 66 | y1=fitted_df['yhat_lower'].tail(1).append(predicted_df['predicted_lower']).values, 67 | y2=fitted_df['yhat_upper'].tail(1).append(predicted_df['predicted_upper']).values, 68 | alpha=.5, 69 | color='orange') 70 | ax.set_title('ThymeBoost Results') 71 | if 'outliers' in fitted_df.columns: 72 | outlier_df = fitted_df[fitted_df['outliers'] == True] 73 | ax.scatter(outlier_df.index, outlier_df['y'], marker='x', color='red') 74 | plt.show() 75 | 76 | def plot_optimization(fitted_df, opt_predictions, opt_type, figsize): 77 | """Simple plot of optimization results for convenience""" 78 | step_colors = ['tab:blue', 79 | 'tab:orange', 80 | 'tab:green', 81 | 'tab:red', 82 | 'tab:purple', 83 | 'tab:cyan'] 84 | step_colors = cycle(step_colors) 85 | min_opt_idx = opt_predictions[-1].index[0] 86 | fitted_y = fitted_df['yhat'].loc[:min_opt_idx] 87 | fig, ax = plt.subplots(figsize=figsize) 88 | ax.plot(fitted_df['y'], color='black') 89 | ax.plot(fitted_y, color='orange') 90 | for idx, i in enumerate(opt_predictions): 91 | section_color = next(step_colors) 92 | ax.plot(i, color=section_color, linestyle='dashed') 93 | if opt_type in ['holdout', 'cv']: 94 | ax.axvspan(i.index[0], i.index[-1], alpha=0.2, color=section_color, linestyle='dashed') 95 | ax.set_title('ThymeBoost Optimization') 96 | plt.show() 97 | 98 | # def plot_rounds(self, figsize = (6,4)): 99 | # if self.exogenous is not None: 100 | # fig, ax = plt.subplots(3, figsize = figsize) 101 | # for iteration in range(len(self.fitted_exogenous)-1): 102 | # ax[2].plot( 103 | # np.sum(self.fitted_exogenous[:iteration], axis=0), 104 | # label=iteration 105 | # ) 106 | # ax[2].set_title('Exogenous') 107 | # else: 108 | # fig, ax = plt.subplots(2, figsize = figsize) 109 | # for iteration in range(len(self.trends)-1): 110 | # ax[0].plot(np.sum(self.trends[:iteration], axis=0), label=iteration) 111 | # ax[0].set_title('Trends') 112 | # for iteration in range(len(self.seasonalities)-1): 113 | # ax[1].plot(np.sum(self.seasonalities[:iteration], axis=0), label=iteration) 114 | # ax[1].set_title('Seasonalities') 115 | # plt.legend() 116 | # plt.show() 117 | -------------------------------------------------------------------------------- /ThymeBoost/utils/trend_dampen.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import numpy as np 4 | import pandas as pd 5 | 6 | 7 | def trend_dampen(damp_fact, trend): 8 | zeroed_trend = trend - trend[0] 9 | damp_fact = 1 - damp_fact 10 | if damp_fact < 0: 11 | damp_fact = 0 12 | if damp_fact > 1: 13 | damp_fact = 1 14 | if damp_fact == 1: 15 | dampened_trend = zeroed_trend 16 | else: 17 | tau = (damp_fact * 1.15 + (1.15 * damp_fact / .85)**9) *\ 18 | (2*len(zeroed_trend)) 19 | dampened_trend = (zeroed_trend*np.exp(-pd.Series(range(1, len(zeroed_trend) + 1))/(tau))) 20 | crossing = np.where(np.diff(np.sign(np.gradient(dampened_trend))))[0] 21 | if crossing.size > 0: 22 | crossing_point = crossing[0] 23 | avg_grad = (np.mean(np.gradient(zeroed_trend))*dampened_trend) 24 | dampened_trend[crossing_point:] = dampened_trend[avg_grad.idxmax()] 25 | #TODO random fix for array/series confusion 26 | dampened_trend = pd.Series(dampened_trend) 27 | return dampened_trend.values + trend[0] 28 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # ThymeBoost 2 | 3 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/thymeboost_logo.png?raw=true "Output 1") 4 | 5 | ThymeBoost combines time series decomposition with gradient boosting to provide a flexible mix-and-match time series framework for spicy forecasting. At the most granular level are the trend/level (going forward this is just referred to as 'trend') models, seasonal models, and endogenous models. These are used to approximate the respective components at each 'boosting round' and sequential rounds are fit on residuals in usual boosting fashion. 6 | 7 | Basic flow of the algorithm: 8 | 9 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/thymeboost_flow.png?raw=true "Output 1") 10 | 11 | 12 | ## Quick Start. 13 | 14 | ``` 15 | pip install ThymeBoost 16 | ``` 17 | 18 | 19 | 20 | ## Some basic examples: 21 | Starting with a very simple example of a simple trend + seasonality + noise 22 | ``` 23 | import numpy as np 24 | import matplotlib.pyplot as plt 25 | import seaborn as sns 26 | from ThymeBoost import ThymeBoost as tb 27 | 28 | sns.set_style('darkgrid') 29 | 30 | #Here we will just create a random series with seasonality and a slight trend 31 | seasonality = ((np.cos(np.arange(1, 101))*10 + 50)) 32 | np.random.seed(100) 33 | true = np.linspace(-1, 1, 100) 34 | noise = np.random.normal(0, 1, 100) 35 | y = true + noise + seasonality 36 | plt.plot(y) 37 | plt.show() 38 | 39 | ``` 40 | 41 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/time_series.png) 42 | 43 | First we will build the ThymeBoost model object: 44 | ``` 45 | boosted_model = tb.ThymeBoost(approximate_splits=True, 46 | n_split_proposals=25, 47 | verbose=1, 48 | cost_penalty=.001) 49 | ``` 50 | The arguments passed here are also the defaults. Most importantly, we pass whether we want to use 'approximate splits' and how many splits to propose. If we pass ```approximate_splits=False``` then ThymeBoost will exhaustively try every data point to split on if we look for changepoints. If we don't care about changepoints then this is ignored. 51 | 52 | ThymeBoost uses a standard fit => predict procedure. Let's use the fit method where everything passed is converted to a itertools cycle object in ThymeBoost, this will be referred as 'generator' parameters moving forward. This might not make sense yet but is shown further in the examples! 53 | ``` 54 | output = boosted_model.fit(y, 55 | trend_estimator='linear', 56 | seasonal_estimator='fourier', 57 | seasonal_period=25, 58 | split_cost='mse', 59 | global_cost='maicc', 60 | fit_type='global') 61 | ``` 62 | We pass the input time_series and the parameters used to fit. For ThymeBoost the more specific parameters are the different cost functions controlling for each split and the global cost function which controls how many boosting rounds to do. Additionally, the ```fit_type='global'``` designates that we are NOT looking for changepoints and just fits our trend_estimator globally. 63 | 64 | With verbose ThymeBoost will print out some relevant information for us. 65 | 66 | Now that we have fitted our series we can take a look at our results 67 | ``` 68 | boosted_model.plot_results(output) 69 | ``` 70 | 71 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/tb_output_1.png?raw=true "Output 1") 72 | 73 | The fit looks correct enough, but let's take a look at the indiviudal components we fitted. 74 | ``` 75 | boosted_model.plot_components(output) 76 | ``` 77 | 78 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/tb_components_1.png?raw=true "Output 1") 79 | 80 | Alright, the decomposition looks reasonable as well but let's complicate the task by now adding a changepoint. 81 | 82 | 83 | ## Adding a changepoint 84 | ``` 85 | true = np.linspace(1, 50, 100) 86 | noise = np.random.normal(0, 1, 100) 87 | y = np.append(y, true + noise + seasonality) 88 | plt.plot(y) 89 | plt.show() 90 | ``` 91 | 92 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/time_series_2.png?raw=true "Output 1") 93 | 94 | In order to fit this we will change ```fit_type='global'``` to ```fit_type='local'```. Let's see what happens. 95 | 96 | ``` 97 | boosted_model = tb.ThymeBoost( 98 | approximate_splits=True, 99 | n_split_proposals=25, 100 | verbose=1, 101 | cost_penalty=.001, 102 | ) 103 | 104 | output = boosted_model.fit(y, 105 | trend_estimator='linear', 106 | seasonal_estimator='fourier', 107 | seasonal_period=25, 108 | split_cost='mse', 109 | global_cost='maicc', 110 | fit_type='local') 111 | predicted_output = boosted_model.predict(output, 100) 112 | ``` 113 | Here we add in the predict method which takes in the fitted results as well as the forecast horizon. You will notice that the print out now states we are fitting locally and we do an additional round of boosting. Let's plot the results and see if the new round was ThymeBoost picking up the changepoint. 114 | 115 | ``` 116 | boosted_model.plot_results(output, predicted_output) 117 | ``` 118 | 119 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/tb_output_2.png?raw=true "Output 1") 120 | 121 | 122 | Ok, cool. Looks like it worked about as expected here, we did do 1 wasted round where ThymeBoost just did a slight adjustment at split 80 but that can be fixed as you will see! 123 | 124 | Once again looking at the components: 125 | ``` 126 | boosted_model.plot_components(output) 127 | ``` 128 | 129 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/tb_components_2.png?raw=true "Output 1") 130 | 131 | 132 | There is a kink in the trend right around 100 as to be expected. 133 | 134 | Let's further complicate this series. 135 | 136 | ## Adding a large jump 137 | 138 | ``` 139 | #Pretty complicated model 140 | true = np.linspace(1, 20, 100) + 100 141 | noise = np.random.normal(0, 1, 100) 142 | y = np.append(y, true + noise + seasonality) 143 | plt.plot(y) 144 | plt.show() 145 | ``` 146 | 147 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/complicated_time_series.png?raw=true "Output 1") 148 | 149 | 150 | So here we have 3 distinct trend lines and one large shift upward. Overall, pretty nasty and automatically fitting this with any model (including ThymeBoost) can have extremely wonky results. 151 | 152 | But...let's try anyway. Here we will utilize the 'generator' variables. As mentioned before, everything passed in to the fit method is a generator variable. This basically means that we can pass a list for a parameter and that list will be cycled through at each boosting round. So if we pass this: ```trend_estimator=['mean', 'linear']``` after the initial trend estimation using the median we then use mean followed by linear then mean and linear until boosting is terminated. We can also use this to approximate a potential complex seasonality just by passing a list of what the complex seasonality can be. Let's fit with these generator variables and pay close attention to the print out as it will show you what ThymeBoost is doing at each round. 153 | 154 | ``` 155 | boosted_model = tb.ThymeBoost( 156 | approximate_splits=True, 157 | verbose=1, 158 | cost_penalty=.001, 159 | ) 160 | 161 | output = boosted_model.fit(y, 162 | trend_estimator=['mean'] + ['linear']*20, 163 | seasonal_estimator='fourier', 164 | seasonal_period=[25, 0], 165 | split_cost='mae', 166 | global_cost='maicc', 167 | fit_type='local', 168 | connectivity_constraint=True, 169 | ) 170 | 171 | predicted_output = boosted_model.predict(output, 100) 172 | 173 | ``` 174 | 175 | The log tells us what we need to know: 176 | 177 | ``` 178 | ********** Round 1 ********** 179 | Using Split: None 180 | Fitting initial trend globally with trend model: 181 | median() 182 | seasonal model: 183 | fourier(10, False) 184 | cost: 2406.7734967780552 185 | ********** Round 2 ********** 186 | Using Split: 200 187 | Fitting local with trend model: 188 | mean() 189 | seasonal model: 190 | None 191 | cost: 1613.03414289753 192 | ********** Round 3 ********** 193 | Using Split: 174 194 | Fitting local with trend model: 195 | linear((1, None)) 196 | seasonal model: 197 | fourier(10, False) 198 | cost: 1392.923553270366 199 | ********** Round 4 ********** 200 | Using Split: 274 201 | Fitting local with trend model: 202 | linear((1, None)) 203 | seasonal model: 204 | None 205 | cost: 1384.306737800115 206 | ============================== 207 | Boosting Terminated 208 | Using round 4 209 | ``` 210 | 211 | The initial round for trend is always the same (this idea is pretty core to the boosting framework) but after that we fit with mean and the next 2 rounds are fit with linear estimation. The complex seasonality works 100% as we expect, just going back and forth between the 2 periods we give it where a 0 period means no seasonality estimation occurs. 212 | 213 | Let's take a look at the results: 214 | 215 | ``` 216 | boosted_model.plot_results(output, predicted_output) 217 | ``` 218 | 219 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/complicated_output_bad.png?raw=true "Output 1") 220 | 221 | Hmmm, that looks very wonky. 222 | 223 | But since we used a mean estimator we are saying that there is a change in the overall level of the series. That's not exactly true, by appending that last series with just another trend line we essentially changed the slope and the intercept of the series. 224 | 225 | To account for this, let's relax connectivity constraints and just try linear estimators. Once again, EVERYTHING passed to the fit method is a generator variable so we will relax the connectivity constraint for the first linear fit to hopefully account for the large jump. After that we will use the constraint for 10 rounds then ThymeBoost will just cycle through the list we provide again. 226 | 227 | ``` 228 | #Without connectivity constraint 229 | boosted_model = tb.ThymeBoost( 230 | approximate_splits=True, 231 | verbose=1, 232 | cost_penalty=.001, 233 | ) 234 | 235 | output = boosted_model.fit(y, 236 | trend_estimator='linear', 237 | seasonal_estimator='fourier', 238 | seasonal_period=[25, 0], 239 | split_cost='mae', 240 | global_cost='maicc', 241 | fit_type='local', 242 | connectivity_constraint=[False] + [True]*10, 243 | ) 244 | predicted_output = boosted_model.predict(output, 100) 245 | boosted_model.plot_results(output, predicted_output) 246 | ``` 247 | 248 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/tb_complicated_output.png?raw=true "Output 1") 249 | 250 | Alright, that looks a ton better. It does have some underfitting going on in the middle which is typical since we are using binary segmentation for the changepoints. But other than that it seems reasonable. Let's take a look at the components: 251 | 252 | ``` 253 | boosted_model.plot_components(output) 254 | ``` 255 | 256 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/complicated_components.png?raw=true "Output 1") 257 | 258 | 259 | Looks like the model is catching on to the underlying process creating the data. The trend is clearly composed of three segments and has that large jump right at 200 just as we hoped to see! 260 | 261 | ## Controlling the boosting rounds 262 | 263 | We can control how many rounds and therefore the complexity of our model a couple of different ways. The most direct is by controlling the number of rounds. 264 | ``` 265 | #n_rounds=1 266 | boosted_model = tb.ThymeBoost( 267 | approximate_splits=True, 268 | verbose=1, 269 | cost_penalty=.001, 270 | n_rounds=1 271 | ) 272 | 273 | output = boosted_model.fit(y, 274 | trend_estimator='arima', 275 | arima_order=[(1, 0, 0), (1, 0, 1), (1, 1, 1)], 276 | seasonal_estimator='fourier', 277 | seasonal_period=25, 278 | split_cost='mae', 279 | global_cost='maicc', 280 | fit_type='global', 281 | ) 282 | predicted_output = boosted_model.predict(output, 100) 283 | boosted_model.plot_components(output) 284 | ``` 285 | 286 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/n_rounds1.png?raw=true "Output 1") 287 | 288 | 289 | By passing ```n_rounds=1``` we only allow ThymeBoost to do the initial trend estimation (a simple median) and one shot at approximating the seasonality. 290 | 291 | Additionally we are trying out a new ```trend_estimator``` along with the related parameter ```arima_order```. Although we didn't get to it we are passing the ```arima_order``` to go from simple to complex. 292 | 293 | Let's try forcing ThymeBoost to go through all of our provided ARIMA orders by setting ```n_rounds=4``` 294 | 295 | ``` 296 | boosted_model = tb.ThymeBoost( 297 | approximate_splits=True, 298 | verbose=1, 299 | cost_penalty=.001, 300 | n_rounds=4, 301 | regularization=1.2 302 | ) 303 | 304 | output = boosted_model.fit(y, 305 | trend_estimator='arima', 306 | arima_order=[(1, 0, 0), (1, 0, 1), (1, 1, 1)], 307 | seasonal_estimator='fourier', 308 | seasonal_period=25, 309 | split_cost='mae', 310 | global_cost='maicc', 311 | fit_type='global', 312 | ) 313 | predicted_output = boosted_model.predict(output, 100) 314 | ``` 315 | 316 | Looking at the log: 317 | 318 | ``` 319 | ********** Round 1 ********** 320 | Using Split: None 321 | Fitting initial trend globally with trend model: 322 | median() 323 | seasonal model: 324 | fourier(10, False) 325 | cost: 2406.7734967780552 326 | ********** Round 2 ********** 327 | Using Split: None 328 | Fitting global with trend model: 329 | arima((1, 0, 0)) 330 | seasonal model: 331 | fourier(10, False) 332 | cost: 988.0694403606061 333 | ********** Round 3 ********** 334 | Using Split: None 335 | Fitting global with trend model: 336 | arima((1, 0, 1)) 337 | seasonal model: 338 | fourier(10, False) 339 | cost: 991.7292716360867 340 | ********** Round 4 ********** 341 | Using Split: None 342 | Fitting global with trend model: 343 | arima((1, 1, 1)) 344 | seasonal model: 345 | fourier(10, False) 346 | cost: 1180.688829140743 347 | ``` 348 | 349 | We can see that the cost which typically controls boosting is ignored. It actually increases in round 3. An alternative for boosting complexity would be to pass a larger ```regularization``` parameter when building the model class. 350 | 351 | ## Component Regularization with a Learning Rate 352 | 353 | Another idea taken from gradient boosting is the use of a learning rate. However, we allow component-specific learning rates. The main benefit to this is that it allows us to have the same fitting procedure (always trend => seasonality => exogenous) but account for the potential different ways we want to fit. For example, let's say our series is responding to an exogenous variable that is seasonal. Since we fit for seasonality BEFORE exogenous then we could eat up that signal. However, we could simply pass a ```seasonality_lr``` (or trend_lr / exogenous_lr) which will penalize the seasonality approximation and leave the signal for the exogenous component fit. 354 | 355 | Here is a quick example, as always we could pass it as a list if we want to allow seasonality to return to normal after the first round. 356 | 357 | ``` 358 | #seasonality regularization 359 | boosted_model = tb.ThymeBoost( 360 | approximate_splits=True, 361 | verbose=1, 362 | cost_penalty=.001, 363 | n_rounds=2 364 | ) 365 | 366 | output = boosted_model.fit(y, 367 | trend_estimator='arima', 368 | arima_order=(1, 0, 1), 369 | seasonal_estimator='fourier', 370 | seasonal_period=25, 371 | split_cost='mae', 372 | global_cost='maicc', 373 | fit_type='global', 374 | seasonality_lr=.1 375 | ) 376 | predicted_output = boosted_model.predict(output, 100) 377 | ``` 378 | 379 | ## Parameter Optimization 380 | 381 | ThymeBoost has an optimizer which will try to find the 'optimal' parameter settings based on all combinations that are passed. 382 | 383 | Importantly, all parameters that are normally pass to fit must now be passed as a list. 384 | 385 | Let's take a look: 386 | 387 | ``` 388 | boosted_model = tb.ThymeBoost( 389 | approximate_splits=True, 390 | verbose=0, 391 | cost_penalty=.001, 392 | ) 393 | 394 | output = boosted_model.optimize(y, 395 | verbose=1, 396 | lag=20, 397 | optimization_steps=1, 398 | trend_estimator=['mean', 'linear', ['mean', 'linear']], 399 | seasonal_period=[0, 25], 400 | fit_type=['local', 'global']) 401 | ``` 402 | 403 | ``` 404 | 100%|██████████| 12/12 [00:00<00:00, 46.63it/s] 405 | Optimal model configuration: {'trend_estimator': 'linear', 'fit_type': 'local', 'seasonal_period': 25, 'exogenous': None} 406 | Params ensembled: False 407 | ``` 408 | 409 | 410 | First off, I disabled the verbose call in the constructor so it won't print out everything for each model. Instead, passing ```verbose=1``` to the optimize method will print a tqdm progress bar and the best model configuration. Lag refers to the number of points to holdout for our test set and optimization_steps allows you to roll through the holdout. 411 | 412 | Another important thing to note, one of the elements in the list of trend_estimators is itself a list. With optimization, all we do is try each combination of the parameters given so each element in the list provided will be passed to the normal fit method, if that element is a list then that means you are using a generator variable for that implementation. 413 | 414 | With the optimizer class we retain all other methods we have been using after fit. 415 | 416 | ``` 417 | predicted_output = boosted_model.predict(output, 100) 418 | 419 | boosted_model.plot_results(output, predicted_output) 420 | ``` 421 | 422 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/optimizer_output.png?raw=true "Output 1") 423 | 424 | So this output looks wonky around that changepoint but it recovers in time to produce a good enough forecast to do well in the holdout. 425 | 426 | ## Ensembling 427 | 428 | Instead of iterating through and choosing the best parameters we could also just ensemble them into a simple average of every parameter setting. 429 | 430 | Everything stated about the optimizer holds for ensemble as well, except now we just call the ensemble method. 431 | 432 | ``` 433 | boosted_model = tb.ThymeBoost( 434 | approximate_splits=True, 435 | verbose=0, 436 | cost_penalty=.001, 437 | ) 438 | 439 | output = boosted_model.ensemble(y, 440 | trend_estimator=['mean', 'linear', ['mean', 'linear']], 441 | seasonal_period=[0, 25], 442 | fit_type=['local', 'global']) 443 | 444 | predicted_output = boosted_model.predict(output, 100) 445 | 446 | boosted_model.plot_results(output, predicted_output) 447 | ``` 448 | 449 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/ensemble_output.png?raw=true) 450 | 451 | Obviously, this output is quite wonky. Primarily because of the 'global' parameter which is pulling everything to the center of the data. However, ensembling has been shown to be quite effective in the wild. 452 | 453 | ## Optimization with Ensembling? 454 | 455 | So what if we want to try an ensemble out during optimization, is that possible? 456 | 457 | The answer is yes! 458 | 459 | But to do it we have to use a new function in our optimize method. Here is an example: 460 | 461 | ``` 462 | boosted_model = tb.ThymeBoost( 463 | approximate_splits=True, 464 | verbose=0, 465 | cost_penalty=.001, 466 | ) 467 | 468 | output = boosted_model.optimize(y, 469 | lag=10, 470 | optimization_steps=1, 471 | trend_estimator=['mean', boosted_model.combine(['ses', 'des', 'damped_des'])], 472 | seasonal_period=[0, 25], 473 | fit_type=['global']) 474 | 475 | predicted_output = boosted_model.predict(output, 100) 476 | ``` 477 | 478 | For everything we want to be treated as an ensemble while optimizing we must wrap the parameter list in the combine function as seen: ```boosted_model.combine(['ses', 'des', 'damped_des'])``` 479 | 480 | And now in the log: 481 | 482 | ``` 483 | Optimal model configuration: {'trend_estimator': ['ses', 'des', 'damped_des'], 'fit_type': ['global'], 'seasonal_period': [25], 'exogenous': [None]} 484 | Params ensembled: True 485 | ``` 486 | 487 | We see that everything returned is a list and 'Params ensembled' is now True, signifying to ThymeBoost that this is an Ensemble. 488 | 489 | Let's take a look at the outputs: 490 | ``` 491 | boosted_model.plot_results(output, predicted_output) 492 | ``` 493 | 494 | ![alt text](https://github.com/tblume1992/ThymeBoost/blob/main/static/optimized_ensemble.png?raw=true "Output 1") 495 | 496 | 497 | # ToDo 498 | 499 | The package is still under heavy development and with the large number of combinations that arise from the framework if you find any issues definitely raise them! 500 | 501 | Logging and error handling is still basic to non-existent, so it is one of our top priorities. 502 | -------------------------------------------------------------------------------- /docs/ThymeBoost.exogenous_models.rst: -------------------------------------------------------------------------------- 1 | Exogenous Models 2 | ==================================== 3 | 4 | Decision Tree 5 | ------------------------------------------------------------- 6 | 7 | .. automodule:: ThymeBoost.exogenous_models.decision_tree_exogenous 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | 13 | GLM 14 | -------------------------------------------------- 15 | 16 | .. automodule:: ThymeBoost.exogenous_models.glm_exogenous 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | 21 | OLS 22 | -------------------------------------------------- 23 | 24 | .. automodule:: ThymeBoost.exogenous_models.ols_exogenous 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | 29 | 30 | .. automodule:: ThymeBoost.exogenous_models 31 | :members: 32 | :undoc-members: 33 | :show-inheritance: 34 | -------------------------------------------------------------------------------- /docs/ThymeBoost.fit_components.rst: -------------------------------------------------------------------------------- 1 | ThymeBoost.fit\_components package 2 | ================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | ThymeBoost.fit\_components.fit\_exogenous module 8 | ------------------------------------------------ 9 | 10 | .. automodule:: ThymeBoost.fit_components.fit_exogenous 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | ThymeBoost.fit\_components.fit\_seasonality module 16 | -------------------------------------------------- 17 | 18 | .. automodule:: ThymeBoost.fit_components.fit_seasonality 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | ThymeBoost.fit\_components.fit\_trend module 24 | -------------------------------------------- 25 | 26 | .. automodule:: ThymeBoost.fit_components.fit_trend 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | Module contents 32 | --------------- 33 | 34 | .. automodule:: ThymeBoost.fit_components 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | -------------------------------------------------------------------------------- /docs/ThymeBoost.fitter.rst: -------------------------------------------------------------------------------- 1 | ThymeBoost.fitter package 2 | ========================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | ThymeBoost.fitter.booster module 8 | -------------------------------- 9 | 10 | .. automodule:: ThymeBoost.fitter.booster 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | ThymeBoost.fitter.decompose module 16 | ---------------------------------- 17 | 18 | .. automodule:: ThymeBoost.fitter.decompose 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: ThymeBoost.fitter 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /docs/ThymeBoost.rst: -------------------------------------------------------------------------------- 1 | ThymeBoost package 2 | ================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | ThymeBoost.Datasets 11 | ThymeBoost.exogenous_models 12 | ThymeBoost.fit_components 13 | ThymeBoost.fitter 14 | ThymeBoost.seasonality_models 15 | ThymeBoost.trend_models 16 | ThymeBoost.utils 17 | 18 | Submodules 19 | ---------- 20 | 21 | ThymeBoost.ThymeBoost module 22 | ---------------------------- 23 | 24 | .. automodule:: ThymeBoost.ThymeBoost 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | 29 | ThymeBoost.cost\_functions module 30 | --------------------------------- 31 | 32 | .. automodule:: ThymeBoost.cost_functions 33 | :members: 34 | :undoc-members: 35 | :show-inheritance: 36 | 37 | ThymeBoost.ensemble module 38 | -------------------------- 39 | 40 | .. automodule:: ThymeBoost.ensemble 41 | :members: 42 | :undoc-members: 43 | :show-inheritance: 44 | 45 | ThymeBoost.optimizer module 46 | --------------------------- 47 | 48 | .. automodule:: ThymeBoost.optimizer 49 | :members: 50 | :undoc-members: 51 | :show-inheritance: 52 | 53 | ThymeBoost.param\_iterator module 54 | --------------------------------- 55 | 56 | .. automodule:: ThymeBoost.param_iterator 57 | :members: 58 | :undoc-members: 59 | :show-inheritance: 60 | 61 | ThymeBoost.predict\_functions module 62 | ------------------------------------ 63 | 64 | .. automodule:: ThymeBoost.predict_functions 65 | :members: 66 | :undoc-members: 67 | :show-inheritance: 68 | 69 | ThymeBoost.split\_proposals module 70 | ---------------------------------- 71 | 72 | .. automodule:: ThymeBoost.split_proposals 73 | :members: 74 | :undoc-members: 75 | :show-inheritance: 76 | 77 | Module contents 78 | --------------- 79 | 80 | .. automodule:: ThymeBoost 81 | :members: 82 | :undoc-members: 83 | :show-inheritance: 84 | -------------------------------------------------------------------------------- /docs/ThymeBoost.seasonality_models.rst: -------------------------------------------------------------------------------- 1 | Seasonality Models 2 | ====================================== 3 | 4 | Classic 5 | ---------------------------------------------------------- 6 | 7 | .. automodule:: ThymeBoost.seasonality_models.classic_seasonality 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | Fourier 13 | ---------------------------------------------------------- 14 | 15 | .. automodule:: ThymeBoost.seasonality_models.fourier_seasonality 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | .. automodule:: ThymeBoost.seasonality_models 21 | :members: 22 | :undoc-members: 23 | :show-inheritance: 24 | -------------------------------------------------------------------------------- /docs/ThymeBoost.trend_models.rst: -------------------------------------------------------------------------------- 1 | Trend Models 2 | ================================ 3 | 4 | ARIMA 5 | -------------------------------------------- 6 | 7 | .. automodule:: ThymeBoost.trend_models.arima_trend 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | Smoothers 13 | ------------------------------------------ 14 | 15 | .. automodule:: ThymeBoost.trend_models.ets_trend 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | EWM 21 | ------------------------------------------ 22 | 23 | .. automodule:: ThymeBoost.trend_models.ewm_trend 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | Linear 29 | --------------------------------------------- 30 | 31 | .. automodule:: ThymeBoost.trend_models.linear_trend 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | LOESS 37 | -------------------------------------------- 38 | 39 | .. automodule:: ThymeBoost.trend_models.loess_trend 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | Mean 45 | ------------------------------------------- 46 | 47 | .. automodule:: ThymeBoost.trend_models.mean_trend 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | Median 53 | --------------------------------------------- 54 | 55 | .. automodule:: ThymeBoost.trend_models.median_trend 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | RANSAC 61 | --------------------------------------------- 62 | 63 | .. automodule:: ThymeBoost.trend_models.ransac_trend 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | 68 | .. automodule:: ThymeBoost.trend_models 69 | :members: 70 | :undoc-members: 71 | :show-inheritance: 72 | -------------------------------------------------------------------------------- /docs/ThymeBoost.utils.rst: -------------------------------------------------------------------------------- 1 | Utilities 2 | ======================== 3 | 4 | ThymeBoost.utils.plotting module 5 | -------------------------------- 6 | 7 | .. automodule:: ThymeBoost.utils.plotting 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | ThymeBoost.utils.trend\_dampen module 13 | ------------------------------------- 14 | 15 | .. automodule:: ThymeBoost.utils.trend_dampen 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | .. automodule:: ThymeBoost.utils 21 | :members: 22 | :undoc-members: 23 | :show-inheritance: 24 | -------------------------------------------------------------------------------- /docs/_static/tb_logo (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/docs/_static/tb_logo (2).png -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'ThymeBoost' 21 | copyright = '2021, Tyler Blume' 22 | author = 'Tyler Blume' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.1.13' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ['sphinx.ext.napoleon', 34 | #'myst_parser', 35 | 'sphinx.ext.mathjax', 36 | 'sphinx_rtd_theme', 37 | #'m2r2' 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | master_doc = 'index' 43 | html_static_path = ['_static'] 44 | 45 | html_logo = "tb_logo.png" 46 | html_theme_options = { 47 | 'logo_only': True, 48 | 'display_version': True, 49 | } 50 | 51 | # List of patterns, relative to source directory, that match files and 52 | # directories to ignore when looking for source files. 53 | # This pattern also affects html_static_path and html_extra_path. 54 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 55 | 56 | #source_suffix = [".rst", ".md"] 57 | 58 | # -- Options for HTML output ------------------------------------------------- 59 | 60 | # The theme to use for HTML and HTML Help pages. See the documentation for 61 | # a list of builtin themes. 62 | # 63 | html_theme = 'sphinx_rtd_theme' 64 | 65 | # Add any paths that contain custom static files (such as style sheets) here, 66 | # relative to this directory. They are copied after the builtin static files, 67 | # so a file named "default.css" will overwrite the builtin "default.css". 68 | html_static_path = ['_static'] 69 | 70 | html_theme_options = { 71 | 'style_nav_header_background': '#FFFFFF', 72 | } 73 | 74 | source_suffix = ['.rst', '.md'] 75 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. ThymeBoost documentation master file, created by 2 | sphinx-quickstart on Wed Oct 27 11:48:36 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to ThymeBoost's documentation! 7 | ====================================== 8 | 9 | .. include:: read_me.rst 10 | 11 | .. include:: ThymeBoost.trend_models.rst 12 | 13 | .. include:: ThymeBoost.seasonality_models.rst 14 | 15 | .. include:: ThymeBoost.exogenous_models.rst 16 | 17 | .. include:: ThymeBoost.utils.rst 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | :caption: Contents: 22 | 23 | read_me 24 | ThymeBoost.trend_models 25 | ThymeBoost.seasonality_models 26 | ThymeBoost.exogenous_models 27 | ThymeBoost.utils 28 | 29 | 30 | Indices and tables 31 | ================== 32 | 33 | * :ref:`genindex` 34 | * :ref:`modindex` 35 | * :ref:`search` 36 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | ThymeBoost 2 | ========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | ThymeBoost 8 | setup 9 | -------------------------------------------------------------------------------- /docs/read_me.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | =========== 3 | 4 | https://github.com/tblume1992/ThymeBoost 5 | 6 | .. mdinclude:: README.md -------------------------------------------------------------------------------- /docs/setup.rst: -------------------------------------------------------------------------------- 1 | setup module 2 | ============ 3 | 4 | .. automodule:: setup 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import setuptools 3 | 4 | with open("README.md", "r") as fh: 5 | long_description = fh.read() 6 | 7 | setuptools.setup( 8 | name="ThymeBoost", 9 | version="0.1.16", 10 | author="Tyler Blume", 11 | url="https://github.com/tblume1992/ThymeBoost", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | description = "Gradient boosted time series decomposition for forecasting.", 15 | author_email = 'tblume@mail.USF.edu', 16 | keywords = ['forecasting', 'time series', 'seasonality', 'trend'], 17 | install_requires=[ 18 | 'numpy', 19 | 'pandas', 20 | 'statsmodels', 21 | 'scikit-learn', 22 | 'scipy', 23 | 'more-itertools', 24 | 'matplotlib', 25 | 'tqdm', 26 | 'pmdarima' 27 | ], 28 | packages=setuptools.find_packages(), 29 | classifiers=[ 30 | "Programming Language :: Python :: 3", 31 | "License :: OSI Approved :: MIT License", 32 | "Operating System :: OS Independent", 33 | ], 34 | ) 35 | 36 | 37 | -------------------------------------------------------------------------------- /static/complicated_components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/complicated_components.png -------------------------------------------------------------------------------- /static/complicated_output_bad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/complicated_output_bad.png -------------------------------------------------------------------------------- /static/complicated_time_series.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/complicated_time_series.png -------------------------------------------------------------------------------- /static/ensemble_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/ensemble_output.png -------------------------------------------------------------------------------- /static/n_rounds1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/n_rounds1.png -------------------------------------------------------------------------------- /static/optimized_ensemble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/optimized_ensemble.png -------------------------------------------------------------------------------- /static/optimizer_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/optimizer_output.png -------------------------------------------------------------------------------- /static/tb_complicated_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/tb_complicated_output.png -------------------------------------------------------------------------------- /static/tb_components_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/tb_components_1.png -------------------------------------------------------------------------------- /static/tb_components_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/tb_components_2.png -------------------------------------------------------------------------------- /static/tb_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/tb_logo.png -------------------------------------------------------------------------------- /static/tb_output_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/tb_output_1.png -------------------------------------------------------------------------------- /static/tb_output_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/tb_output_2.png -------------------------------------------------------------------------------- /static/thymeboost_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/thymeboost_flow.png -------------------------------------------------------------------------------- /static/thymeboost_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/thymeboost_logo.png -------------------------------------------------------------------------------- /static/time_series.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/time_series.png -------------------------------------------------------------------------------- /static/time_series_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tblume1992/ThymeBoost/d4f04b1b14949f584814393cc1ae26f5a029a3e6/static/time_series_2.png --------------------------------------------------------------------------------