diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..02ff02f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/.github/" + schedule: + interval: "weekly" + groups: + all-actions: + patterns: [ "*" ] + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..e52e784 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,36 @@ +name: Python package + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11"] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' # caching pip dependencies + cache-dependency-path: | + **/requirements*.txt + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi + if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi + - name: Sort imports with isort + run: isort . --check --color --diff + - name: Code format with black + run: black . --check --color --diff diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a19790 --- /dev/null +++ b/.gitignore @@ -0,0 +1,174 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..765be16 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + exclude_types: [python] + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files +- repo: https://github.com/PyCQA/isort + rev: 6.0.1 + hooks: + - id: isort +- repo: https://github.com/psf/black + rev: 25.1.0 + hooks: + - id: black diff --git a/README.md b/README.md index 218ddf7..f55fa7e 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,90 @@ -# pyarimafft library - -A Python Library which efficiently combines LOESS cleaning, Fast Fourier Transform Extracted key Cyclicities and ARIMA -to produce meaningful and explainable time series forecasts. - - -## Installation -pip install pyarimafft - - - -## Usage - - -endog = np.array(vector) - -model_obj = pyarimafft.model(forecast_horizon=12) - -model_obj.outlier_clean(endog=endog,window_size=10,outlier_threshold=0.8,peak_clean=False,trough_clean=False,both_sides_clean=True) - -model_obj.extract_key_seasonalities(power_quantile=0.90,time_period=d) - -model_obj.reconstruct_seasonal_features(mode='seperate') - -## It is possible to add one exogenous vector at a time - -model_obj.add_exog(exog1) - -model_obj.add_exog(exog2) - -## Call the auto_arima function - -model_obj.auto_arima(p=None,d=None,q=None,max_p=3,max_q=3,max_d=1,auto_fit=True) - -## Attributes which you can extract - -model_obj.endog - -model_obj.trend - -model_obj.outlier_cleaned - -model_obj.seasonal_component - -model_obj.isolated_components - -model_obj.isolated_seasonality - -model_obj.forecast - -model_obj.seasonal_feature_train - -model_obj.seasonal_feature_future - -model_obj.time_train - -model_obj.time_future - -model_obj.forecast_horizon - -model_obj.forecast - -model_obj.optimal_order - -''' - - - - - +# pyarimafft library + +[![PyPI Latest Release](https://img.shields.io/pypi/v/pyarimafft.svg)](https://pypi.org/project/pyarimafft/) +[![PyPI downloads](https://static.pepy.tech/badge/pyarimafft)](https://pepy.tech/project/pyarimafft) +[![License](https://img.shields.io/github/license/shashboy/pyarimafft)](https://github.com/shashboy/pyarimafft/blob/main/LICENSE) + +A Python Library which efficiently combines LOESS cleaning, Fast Fourier Transform Extracted key Cyclicities and ARIMA +to produce meaningful and explainable time series forecasts. + +## Installation + +```sh +pip install pyarimafft +``` + +## Usage + +```py +import numpy as np + +import pyarimafft + +endog = np.array(vector) + +model_obj = pyarimafft.model(forecast_horizon=12) + +model_obj.outlier_clean( + endog=endog, + window_size=10, + outlier_threshold=0.8, + peak_clean=False, + trough_clean=False, + both_sides_clean=True, +) + +model_obj.extract_key_seasonalities(power_quantile=0.90, time_period=d) + +model_obj.reconstruct_seasonal_features(mode="seperate") + +## It is possible to add one exogenous vector at a time + +model_obj.add_exog(exog1) + +model_obj.add_exog(exog2) + +## Call the auto_arima function + +model_obj.auto_arima(p=None, d=None, q=None, max_p=3, max_q=3, max_d=1, auto_fit=True) + +## Attributes which you can extract + +model_obj.endog + +model_obj.trend + +model_obj.outlier_cleaned + +model_obj.seasonal_component + +model_obj.isolated_components + +model_obj.isolated_seasonality + +model_obj.forecast + +model_obj.seasonal_feature_train + +model_obj.seasonal_feature_future + +model_obj.time_train + +model_obj.time_future + +model_obj.forecast_horizon + +model_obj.forecast + +model_obj.optimal_order +``` + +## Contribution + +Please add Issues or submit Pull Requests! + +For local development, install optional testing dependencies and pre-commit hooks using + +```sh +pip install pyarimafft[dev] +pre-commit install +``` diff --git a/demo.py b/demo.py new file mode 100644 index 0000000..b602594 --- /dev/null +++ b/demo.py @@ -0,0 +1,60 @@ +import numpy as np + +import pyarimafft + +endog = np.array(vector) + +model_obj = pyarimafft.model(forecast_horizon=12) + +model_obj.outlier_clean( + endog=endog, + window_size=10, + outlier_threshold=0.8, + peak_clean=False, + trough_clean=False, + both_sides_clean=True, +) + +model_obj.extract_key_seasonalities(power_quantile=0.90, time_period=d) + +model_obj.reconstruct_seasonal_features(mode="seperate") + +## It is possible to add one exogenous vector at a time + +model_obj.add_exog(exog1) + +model_obj.add_exog(exog2) + +## Call the auto_arima function + +model_obj.auto_arima(p=None, d=None, q=None, max_p=3, max_q=3, max_d=1, auto_fit=True) + +## Attributes which you can extract + +model_obj.endog + +model_obj.trend + +model_obj.outlier_cleaned + +model_obj.seasonal_component + +model_obj.isolated_components + +model_obj.isolated_seasonality + +model_obj.forecast + +model_obj.seasonal_feature_train + +model_obj.seasonal_feature_future + +model_obj.time_train + +model_obj.time_future + +model_obj.forecast_horizon + +model_obj.forecast + +model_obj.optimal_order diff --git a/pyarimafft/__init__.py b/pyarimafft/__init__.py new file mode 100644 index 0000000..a5b1a93 --- /dev/null +++ b/pyarimafft/__init__.py @@ -0,0 +1,437 @@ +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import statsmodels.api as sm +from statsmodels.tsa.arima.model import ARIMA +from statsmodels.tsa.holtwinters import ExponentialSmoothing + +from .constants import VERSION + +__version__ = VERSION + + +def outlier_label(err2, threshold): + if err2 >= threshold: + return 1 + else: + return 0 + + +def get_complex_conjugate(x): + return x.conjugate() + + +get_complex_conjugate_vec = np.vectorize(get_complex_conjugate) +outlier_label_vec = np.vectorize(outlier_label) + + +class model: + def __init__(self, forecast_horizon): + self.endog = None + self.trend = None + self.outlier_cleaned = None + self.seasonal_component = None + self.isolated_components = None + self.isolated_seasonality = None + self.forecast = None + self.seasonal_feature_train = None + self.seasonal_feature_future = None + self.neg_index = None + self.time_train = None + self.time_future = None + if forecast_horizon == 0: + forecast_horizon = None + self.forecast_horizon = forecast_horizon + self.forecast = None + self.optimal_order = None + + def outlier_clean( + self, + endog, + window_size, + outlier_threshold=0.8, + peak_clean=None, + trough_clean=None, + both_sides_clean=True, + ): + lowess_frac = window_size / len(endog) + endog_bool = isinstance(endog, np.ndarray) + if endog_bool == False: + endog = np.array(endog) + self.endog = np.array(endog) + model_smooth = sm.nonparametric.lowess( + endog, list(range(len(endog))), frac=lowess_frac + )[ + :, 1 + ] ####trend on actuals. + error_2 = (endog - model_smooth) ** 2 ####error squared + median_error_2 = np.quantile(error_2, 0.5) ####median error benchmark + error_mark = np.quantile(error_2, outlier_threshold) + benchmark_volatility = error_mark**0.5 + seasonal_component_temp = endog - model_smooth + label = outlier_label_vec(error_2, error_mark) + cleaned = np.array(endog) + if both_sides_clean == True or (peak_clean == True and trough_clean == True): + count = 0 + trough_clean = None + peak_clean = None + while count < endog.shape[0]: + if seasonal_component_temp[count] < 0 and label[count] == 1: + cleaned[count] = model_smooth[count] - benchmark_volatility + elif seasonal_component_temp[count] > 0 and label[count] == 1: + cleaned[count] = model_smooth[count] + benchmark_volatility + else: + pass + count = count + 1 + elif peak_clean == True and trough_clean == False and both_sides_clean == False: + count = 0 + trough_clean = None + both_sides_clean = None + while count < endog.shape[0]: + if seasonal_component_temp[count] > 0 and label[count] == 1: + cleaned[count] = model_smooth[count] + benchmark_volatility + else: + pass + count = count + 1 + elif peak_clean == False and trough_clean == True and both_sides_clean == False: + count = 0 + trough_clean = None + both_sides_clean = None + while count < endog.shape[0]: + if seasonal_component_temp[count] < 0 and label[count] == 1: + cleaned[count] = model_smooth[count] - benchmark_volatility + else: + pass + count = count + 1 + model_smooth = sm.nonparametric.lowess( + cleaned, list(range(len(endog))), frac=lowess_frac + )[:, 1] + seasonal_component = cleaned - model_smooth + self.trend = model_smooth + plt.plot(endog, label="endog") + plt.plot(model_smooth, label="trend") + plt.plot(cleaned, label="outlier cleaned") + plt.legend() + plt.show() + self.outlier_cleaned = cleaned + self.seasonal_component = seasonal_component + # return self.trend,self.outlier_cleaned,self.seasonal_component + + def extract_key_seasonalities(self, power_quantile, time_period=0.01): + cn = np.fft.fft(self.seasonal_component) + freq = np.fft.fftfreq(len(self.seasonal_component), d=time_period) + neg_index = 0 + for i in freq: + if i >= 0: + neg_index = neg_index + 1 + else: + break + self.neg_index = neg_index + fft_df = pd.DataFrame() + fft_df["COEF"] = cn + fft_df["FREQ"] = freq + fft_df["TIME_PERIOD"] = fft_df["FREQ"] ** (-1) + fft_df["POWER"] = np.abs(fft_df["COEF"]) + threshold = np.quantile(fft_df["POWER"], [power_quantile]) + fft_df["KEY_FREQ"] = outlier_label_vec(fft_df["POWER"], threshold) + fft_df["K"] = fft_df.index + fft_df.to_csv("fft_check.csv") + + self.isolated_components = fft_df.iloc[1:neg_index][ + ["K", "COEF", "TIME_PERIOD", "KEY_FREQ"] + ] + self.isolated_components = self.isolated_components[ + (self.isolated_components["KEY_FREQ"] == 1) + ] + self.isolated_seasonality = np.array( + self.isolated_components["TIME_PERIOD"].tolist() + ) + self.isolated_components = fft_df.iloc[0:][ + ["K", "COEF", "TIME_PERIOD", "KEY_FREQ"] + ] + self.isolated_components["KEY_COEF"] = ( + self.isolated_components["COEF"] * self.isolated_components["KEY_FREQ"] + ) + + def reconstruct_seasonal_features(self, mode="seperate"): + time_train = np.arange(self.isolated_components.shape[0]) + time_future = np.arange( + self.isolated_components.shape[0], + self.isolated_components.shape[0] + self.forecast_horizon, + ) + self.time_train = time_train + self.time_future = time_future + if mode == "additive": + self.seasonal_feature_train = np.fft.ifft( + self.isolated_components["KEY_COEF"] + ) + self.seasonal_feature_train = self.seasonal_feature_train.real + key_coeffs = self.isolated_components.iloc[0 : self.neg_index][ + ["K", "KEY_COEF", "KEY_FREQ"] + ] + key_coeffs = key_coeffs[(key_coeffs["KEY_FREQ"] == 1)] + seasonal_feature_future = [] + omega = 2 * np.pi / self.isolated_components.shape[0] + for i, j in key_coeffs.iterrows(): + complex_wave_pos = np.exp( + 1j * omega * key_coeffs.loc[i, "K"] * self.time_future + ) + signal_val_pos = key_coeffs.loc[i, "KEY_COEF"] * complex_wave_pos + signal_val_pos = signal_val_pos.real + complex_wave_neg = np.exp( + 1j * omega * key_coeffs.loc[i, "K"] * self.time_future * -1 + ) + signal_val_neg = ( + key_coeffs.loc[i, "KEY_COEF"].conjugate() * complex_wave_neg + ) + signal_val_neg = signal_val_neg.real + signal_val = signal_val_pos + signal_val_neg + seasonal_feature_future.append(signal_val) + seasonal_feature_future = np.array(seasonal_feature_future) + seasonal_feature_future = ( + seasonal_feature_future / self.isolated_components.shape[0] + ) + seasonal_feature_future = np.sum(seasonal_feature_future, axis=0) + self.seasonal_feature_future = seasonal_feature_future + plt.plot( + self.time_train, self.seasonal_feature_train, label="Train Features" + ) + plt.plot( + self.time_future, + self.seasonal_feature_future, + label="Features for forecast", + ) + plt.legend() + plt.show() + + if mode == "seperate": + self.seasonal_feature_train = np.fft.ifft( + self.isolated_components["KEY_COEF"] + ) + self.seasonal_feature_train = self.seasonal_feature_train.real + key_coeffs = self.isolated_components.iloc[0 : self.neg_index][ + ["K", "KEY_COEF", "KEY_FREQ"] + ] + key_coeffs = key_coeffs[(key_coeffs["KEY_FREQ"] == 1)] + omega = 2 * np.pi / self.isolated_components.shape[0] + + seasonal_feature_train = [] + for i, j in key_coeffs.iterrows(): + complex_wave_pos = np.exp( + 1j * omega * key_coeffs.loc[i, "K"] * self.time_train + ) + signal_val_pos = key_coeffs.loc[i, "KEY_COEF"] * complex_wave_pos + signal_val_pos = signal_val_pos.real + complex_wave_neg = np.exp( + 1j * omega * key_coeffs.loc[i, "K"] * self.time_train * -1 + ) + signal_val_neg = ( + key_coeffs.loc[i, "KEY_COEF"].conjugate() * complex_wave_neg + ) + signal_val_neg = signal_val_neg.real + signal_val = signal_val_pos + signal_val_neg + seasonal_feature_train.append(signal_val) + seasonal_feature_train = np.array(seasonal_feature_train) + seasonal_feature_train = ( + seasonal_feature_train / self.isolated_components.shape[0] + ) + self.seasonal_feature_train = seasonal_feature_train + + seasonal_feature_future = [] + + for i, j in key_coeffs.iterrows(): + complex_wave_pos = np.exp( + 1j * omega * key_coeffs.loc[i, "K"] * self.time_future + ) + signal_val_pos = key_coeffs.loc[i, "KEY_COEF"] * complex_wave_pos + signal_val_pos = signal_val_pos.real + complex_wave_neg = np.exp( + 1j * omega * key_coeffs.loc[i, "K"] * self.time_future * -1 + ) + signal_val_neg = ( + key_coeffs.loc[i, "KEY_COEF"].conjugate() * complex_wave_neg + ) + signal_val_neg = signal_val_neg.real + signal_val = signal_val_pos + signal_val_neg + seasonal_feature_future.append(signal_val) + seasonal_feature_future = np.array(seasonal_feature_future) + seasonal_feature_future = ( + seasonal_feature_future / self.isolated_components.shape[0] + ) + self.seasonal_feature_future = seasonal_feature_future + + for i in range(key_coeffs.shape[0]): + plt.subplot(key_coeffs.shape[0], 1, i + 1) + plt.title( + "Time Period {}".format(round(self.isolated_seasonality[i], 2)) + ) + plt.plot( + self.time_train, + self.seasonal_feature_train[i, :], + label="Train Features", + ) + plt.plot( + self.time_future, + self.seasonal_feature_future[i, :], + label="Features for forecast", + ) + plt.legend() + plt.subplots_adjust(hspace=0.5) + plt.show() + + def add_exog(self, exog): + exog_bool = isinstance(exog, np.ndarray) + if exog_bool == False: + exog = np.array(exog) + exog = exog.reshape(exog.shape[0], 1) + if exog.shape[0] != self.endog.shape[0] + self.forecast_horizon: + return "Exog length does not match train + future horizon length " + exog_train = exog[0 : self.endog.shape[0]] + exog_train = exog_train.T + exog_future = exog[ + self.endog.shape[0] : self.endog.shape[0] + self.forecast_horizon + ] + exog_future = exog_future.T + + try: + if ( + self.seasonal_feature_train == None + and self.seasonal_feature_future == None + ): + self.seasonal_feature_train = exog_train + self.seasonal_feature_future = exog_future + except: + self.seasonal_feature_train = np.concatenate( + [self.seasonal_feature_train, exog_train], axis=0 + ) + self.seasonal_feature_future = np.concatenate( + [self.seasonal_feature_future, exog_future], axis=0 + ) + + def auto_arima( + self, p=None, d=None, q=None, max_p=3, max_q=3, max_d=1, auto_fit=True + ): + if p != None and q != None and d != None: + if len(self.outlier_cleaned) == 0: + forecast_model = ARIMA( + endog=self.endog, + exog=self.seasonal_feature_train.T, + order=(p, d, q), + ) + elif len(self.outlier_cleaned) > 0: + forecast_model = ARIMA( + endog=self.outlier_cleaned, + exog=self.seasonal_feature_train.T, + order=(p, d, q), + ) + model_fit = forecast_model.fit() + preds = model_fit.predict( + exog=self.seasonal_feature_future.T, + start=self.endog.shape[0], + end=self.endog.shape[0] + self.forecast_horizon - 1, + ) + self.forecast = preds + plt.plot(range(0, self.endog.shape[0]), self.endog, label="endog") + plt.plot( + range(0, self.endog.shape[0]), + self.outlier_cleaned, + label="outlier cleaned", + ) + plt.plot( + range(self.endog.shape[0], self.endog.shape[0] + self.forecast_horizon), + preds, + label="prediction", + ) + plt.legend() + plt.show() + + elif auto_fit == True: + if len(self.outlier_cleaned) == 0: + train_series = self.endog + elif len(self.outlier_cleaned) > 0: + train_series = self.outlier_cleaned + + min_aic_model = [] + + for p_val in range(max_p + 1): + for d_val in range(max_d + 1): + for q_val in range(max_q + 1): + model_val = ARIMA( + endog=train_series, + exog=self.seasonal_feature_train.T, + order=(p_val, d_val, q_val), + ) + model_val_fit = model_val.fit() + aic_val = model_val_fit.aic + if len(min_aic_model) == 0: + min_aic_model.append([aic_val, model_val_fit.model.order]) + elif len(min_aic_model) > 0: + aic_val = model_val_fit.aic + if aic_val < min_aic_model[-1][0]: + min_aic_model[0] = [aic_val, model_val_fit.model.order] + print(min_aic_model) + self.optimal_order = min_aic_model[0][1] + forecast_model = ARIMA( + endog=train_series, + exog=self.seasonal_feature_train.T, + order=min_aic_model[0][1], + ) + model_fit = forecast_model.fit() + preds = model_fit.predict( + exog=self.seasonal_feature_future.T, + start=self.endog.shape[0], + end=self.endog.shape[0] + self.forecast_horizon - 1, + ) + self.forecast = preds + plt.plot(range(0, self.endog.shape[0]), self.endog, label="endog") + plt.plot( + range(0, self.endog.shape[0]), + self.outlier_cleaned, + label="outlier cleaned", + ) + plt.plot( + range(self.endog.shape[0], self.endog.shape[0] + self.forecast_horizon), + preds, + label="prediction", + ) + plt.legend() + plt.show() + + def exponential_smoothing( + self, trend, damped_trend, seasonal, seasonal_periods, use_boxcox + ): + if len(self.outlier_cleaned) == 0: + forecast_model = ExponentialSmoothing( + endog=self.endog, + trend=trend, + damped_trend=damped_trend, + seasonal=seasonal, + seasonal_periods=seasonal_periods, + use_boxcox=use_boxcox, + ) + elif len(self.outlier_cleaned) > 0: + forecast_model = ExponentialSmoothing( + endog=self.outlier_cleaned, + trend=trend, + damped_trend=damped_trend, + seasonal=seasonal, + seasonal_periods=seasonal_periods, + use_boxcox=use_boxcox, + ) + model_fit = forecast_model.fit() + preds = model_fit.predict( + start=self.endog.shape[0], + end=self.endog.shape[0] + self.forecast_horizon - 1, + ) + self.forecast = preds + plt.plot(range(0, self.endog.shape[0]), self.endog, label="endog") + plt.plot( + range(0, self.endog.shape[0]), self.outlier_cleaned, label="outlier cleaned" + ) + plt.plot( + range(self.endog.shape[0], self.endog.shape[0] + self.forecast_horizon), + preds, + label="prediction", + ) + plt.legend() + plt.show() diff --git a/pyarimafft/constants.py b/pyarimafft/constants.py new file mode 100644 index 0000000..096a997 --- /dev/null +++ b/pyarimafft/constants.py @@ -0,0 +1 @@ +VERSION = "0.0.0.7" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5d7bf33 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.isort] +profile = "black" diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..3438158 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +black +isort[colors] +pre-commit +setuptools diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0c0f71a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +matplotlib +pandas +statsmodels diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..8d3eab4 --- /dev/null +++ b/setup.py @@ -0,0 +1,44 @@ +from io import open +from os import path + +from setuptools import find_packages, setup + +from pyarimafft.constants import VERSION + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the README file +with open(path.join(here, "README.md"), encoding="utf-8") as f: + long_description = f.read() + + +def get_requirements(kind: str = None): + if kind: + filename = f"requirements-{kind}.txt" + else: + filename = "requirements.txt" + with open(filename) as f: + requires = (line.strip() for line in f) + return [req for req in requires if req and not req.startswith("#")] + + +setup( + name="pyarimafft", + version=VERSION, + description="A Time Series Forecasting library which performs outlier cleaning with LOESS regression, extracts multiple cyclicities with fast fourier transform & performs time series forecast via ARIMA.", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/shashboy/pyarimafft", + author="Shashank Sharma", + author_email="shashboy@gmail.com", + classifiers=[ + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + ], + packages=find_packages(exclude=["contrib", "docs", "tests"]), + python_requires=">=3.8", + install_requires=get_requirements(), + extra_require={ + "dev": get_requirements("dev"), + }, +)