import pandas as pd
import numpy as np
from sklearn.metrics import mean_squared_error, mean_squared_log_error
from pmdarima.metrics import smape
from statsmodels.tsa.stattools import adfuller
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from statsmodels.tsa.statespace.sarimax import SARIMAX
import re


def persistence_model(series):
    """DOCSTRING
    Wrapper for baseline persistence model"""
    return [x for x in series]


# Need to Find a Context Manager or Other Way to capture STD OUT
# That is not %%capture, so that this will run without Jupyter
# (This works on std out, but we need to capture std out to feed into it)
def get_cv_params(std_text):
    """accepts stdout generated by cross-validate, returns the "Best Model" Objects
     for each validated fold as a list of lists of ints
     Example output for a cv with four folds and s=24 might be
     [[2, 0, 3, 2, 1, 0, 24],
     [2, 0, 1, 2, 1, 0, 24],
     [3, 0, 3, 0, 1, 1, 24],
     [1, 0, 0, 0, 1, 1, 24]]"""

    # build a match pattern that matches a string that looks like
    # this: 'Best model:  ARIMA(1,0,0)(0,1,1)[24]' from full contents of stdout
    # containing the ARIMA parameters from each fold in a cv search
    mod_pattern = re.compile(r'Best.*24]')

    # Match the pattern against stdout
    pat_list = (re.findall(mod_pattern, std_text))
    # Find all digits in each param in pat_list, save as list of params
    params = [re.findall(r'\d', pat_list[j]) for j, _ in enumerate(pat_list)]

    # this is a bit complicated: but does a fairly simple thing:
    # converts params from a list of lists filled with digits as individual strings of len 1
    # to a list of lists filled with digits as ints. since s=24 is specified in our AutoArima pipe
    # the last two digits are always 2 and 4, we fix this by converting them both to 24 and
    # removing the last digit, converting a list like ['2', '0', '3', '2', '1', '0', '2', '4']
    # to for example [2, 0, 3, 2, 1, 0, 24], which we can feed as SARIMA params
    params = [[int(param[p]) if (p < len(param) - 2) else 24 for p,
               _ in enumerate(param)][:-1] for param in params]

    return params


def test_stationarity(series, print_vals=True):
    """Simple wrapper around adfuller that prints in more readable format
    Adapted slightly from Jason Brownlee's machinelearningmastery.com

    Params:
        series (series) a timeseries
    Returns:
        adfuller test result"""

    result = adfuller(series)
    print('ADF Statistic: %f' % result[0])
    print('p-value: %f' % result[1])
    print('Critical Values:')

    for key, value in result[4].items():
        print('\t%s: %.3f' % (key, value))
    return result


def RMSE(y_true, y_pred, last_only=True):
    """Simple wrapper function on mean_squared_error to return RMSE

    Params:
        predictions (series or array like object), the predicted values
        from model test_data (series or array like object), the true 
        target values

    Returns:
        RMSE (list) list of accumulated RMSE values for each
        observation in consecutive time order i.e. the first return
        value will be just the error of first prediction, second the
        sqrt of mean squared error for first 2 predictions, etc."""

    rmse = None
    # Ensure predictions and test_data are same size
    if len(y_pred) != len(y_true):
        rmse = "Test data and predictions must have equal length"
    # If last_only is false, return the rmse for every point in the prediction set
    # (useful for tracking where model went wrong)
    elif last_only == False:
        rmse = [mean_squared_error(
            y_true[:i+1], y_pred[:i+1], squared=False) for i, _ in enumerate(y_pred)]
    # Normal case: return the rmse value for the full prediction set
    else:
        rmse = mean_squared_error(y_true, y_pred, squared=False)

    return rmse


def SMAPE(y_true, y_pred):
    """Wrapper aroumd smape from pmdarima.metrics
    Returns value of smape on a 0-100% scale 
    instead of 0-200% for interpretability see
    https://en.wikipedia.org/wiki/Symmetric_mean_absolute_percentage_error
    and https://alkaline-ml.com/pmdarima/modules/generated/pmdarima.metrics.smape.html
    for reference"""
    return smape(y_true, y_pred) / 2


def fit_sarimax(series, cfg=[(0, 1, 1), (0, 1, 1, 24), 'n'], test_start='2020-07-01 00:00:00'):
    """DOCSTRING accepts a series and SARIMAX configuration, returns model object, train/test sets"""
    train = series[series.index < test_start]
    test = series[test_start:]

    model = SARIMAX(train, order=cfg[0], seasonal_order=cfg[1], trend=cfg[2])

    return train, test, model


def sarima_configs(seasonal=[1, 24, 168]):
    """build configuration list to do light gridsearch of SARIMAX 
    models, function is from Jason Brownlee's website:
    machinelearningmastery.com """

    models = list()
    # define config lists
    p_params = [0, 1, 2]
    d_params = [0, 1]
    q_params = [0, 1, 2]
    t_params = ['n', 'c', 't', 'ct']
    P_params = [0, 1, 2]
    D_params = [0, 1]
    Q_params = [0, 1, 2]
    m_params = seasonal
    # create config instances
    for p in p_params:
        for d in d_params:
            for q in q_params:
                for t in t_params:
                    for P in P_params:
                        for D in D_params:
                            for Q in Q_params:
                                for m in m_params:
                                    cfg = [(p, d, q), (P, D, Q, m), t]
                                    models.append(cfg)
    return models


def SARIMAX_error(series, p=10, d=2, q=2):
    """Simple wrapper that fits SARIMAX model and returns RMSE (raw and
    pct) for the predictions, confidence interval, start of forecast
    and end of actual values"""

    X = series

    # set trainset to include all but last 48 months (4 years)
    # only training on data between 9-4 years ago
    train_size = int(len(X) - 48)
    train, test = X[-108:train_size], X[train_size:]

    model = SARIMAX(train, order=(p, d, q), freq='MS',
                    initialization='approximate_diffuse')

    results = model.fit()

    # Predict 48 months from end of train set
    forecast = results.get_forecast(steps=48)
    pred_ci = forecast.conf_int(alpha=.05)

    predictions = forecast.predicted_mean

    rmse = RMSE(test, predictions)
    # pct = error_as_pct(rmse, train[-1], test[-1])

    return pred_ci, rmse, (train[-1], test[-1])  # , pct


def SARIMAX_forecast(series, cfg, pred_len):
    """DOCSTRING"""

    X = series

    # set trainset to include all but last 48 months (4 years)
    # only training on data between 9-4 years ago
    train_size = int(len(X) - pred_len)
    train, test = X[0:train_size], X[train_size:]

    model = SARIMAX(train, order=cfg[0], seasonal_order=cfg[1],
                    trend=cfg[2], initialization='approximate_diffuse')

    results = model.fit()

    # Predict 48 months from end of train set
    forecast = results.predict(start=test.index[0], end=test.index[-1])

    return forecast


#     pred_ci = forecast.conf_int(alpha=.05)

#     predictions = forecast.predicted_mean

#     rmse = RMSE(test, predictions)
#     pct = error_as_pct(rmse, train[-1], test[-1])

#     ROI = (predictions[-1] - train[-1]) / train[-1]

#     #return {'pred_ci': pred_ci, 'rmse': rmse, 'pct_error': pct, 'test': test, 'predictions': predictions, 'series': X}
#     return ROI

af2dbd066552a8beaf4cf939727296ddf9b80467
<mask token>


def persistence_model(series):
    """DOCSTRING
    Wrapper for baseline persistence model"""
    return [x for x in series]


def get_cv_params(std_text):
    """accepts stdout generated by cross-validate, returns the "Best Model" Objects
     for each validated fold as a list of lists of ints
     Example output for a cv with four folds and s=24 might be
     [[2, 0, 3, 2, 1, 0, 24],
     [2, 0, 1, 2, 1, 0, 24],
     [3, 0, 3, 0, 1, 1, 24],
     [1, 0, 0, 0, 1, 1, 24]]"""
    mod_pattern = re.compile('Best.*24]')
    pat_list = re.findall(mod_pattern, std_text)
    params = [re.findall('\\d', pat_list[j]) for j, _ in enumerate(pat_list)]
    params = [[(int(param[p]) if p < len(param) - 2 else 24) for p, _ in
        enumerate(param)][:-1] for param in params]
    return params


<mask token>


def RMSE(y_true, y_pred, last_only=True):
    """Simple wrapper function on mean_squared_error to return RMSE

    Params:
        predictions (series or array like object), the predicted values
        from model test_data (series or array like object), the true 
        target values

    Returns:
        RMSE (list) list of accumulated RMSE values for each
        observation in consecutive time order i.e. the first return
        value will be just the error of first prediction, second the
        sqrt of mean squared error for first 2 predictions, etc."""
    rmse = None
    if len(y_pred) != len(y_true):
        rmse = 'Test data and predictions must have equal length'
    elif last_only == False:
        rmse = [mean_squared_error(y_true[:i + 1], y_pred[:i + 1], squared=
            False) for i, _ in enumerate(y_pred)]
    else:
        rmse = mean_squared_error(y_true, y_pred, squared=False)
    return rmse


def SMAPE(y_true, y_pred):
    """Wrapper aroumd smape from pmdarima.metrics
    Returns value of smape on a 0-100% scale 
    instead of 0-200% for interpretability see
    https://en.wikipedia.org/wiki/Symmetric_mean_absolute_percentage_error
    and https://alkaline-ml.com/pmdarima/modules/generated/pmdarima.metrics.smape.html
    for reference"""
    return smape(y_true, y_pred) / 2


<mask token>


def sarima_configs(seasonal=[1, 24, 168]):
    """build configuration list to do light gridsearch of SARIMAX 
    models, function is from Jason Brownlee's website:
    machinelearningmastery.com """
    models = list()
    p_params = [0, 1, 2]
    d_params = [0, 1]
    q_params = [0, 1, 2]
    t_params = ['n', 'c', 't', 'ct']
    P_params = [0, 1, 2]
    D_params = [0, 1]
    Q_params = [0, 1, 2]
    m_params = seasonal
    for p in p_params:
        for d in d_params:
            for q in q_params:
                for t in t_params:
                    for P in P_params:
                        for D in D_params:
                            for Q in Q_params:
                                for m in m_params:
                                    cfg = [(p, d, q), (P, D, Q, m), t]
                                    models.append(cfg)
    return models


def SARIMAX_error(series, p=10, d=2, q=2):
    """Simple wrapper that fits SARIMAX model and returns RMSE (raw and
    pct) for the predictions, confidence interval, start of forecast
    and end of actual values"""
    X = series
    train_size = int(len(X) - 48)
    train, test = X[-108:train_size], X[train_size:]
    model = SARIMAX(train, order=(p, d, q), freq='MS', initialization=
        'approximate_diffuse')
    results = model.fit()
    forecast = results.get_forecast(steps=48)
    pred_ci = forecast.conf_int(alpha=0.05)
    predictions = forecast.predicted_mean
    rmse = RMSE(test, predictions)
    return pred_ci, rmse, (train[-1], test[-1])


def SARIMAX_forecast(series, cfg, pred_len):
    """DOCSTRING"""
    X = series
    train_size = int(len(X) - pred_len)
    train, test = X[0:train_size], X[train_size:]
    model = SARIMAX(train, order=cfg[0], seasonal_order=cfg[1], trend=cfg[2
        ], initialization='approximate_diffuse')
    results = model.fit()
    forecast = results.predict(start=test.index[0], end=test.index[-1])
    return forecast

<mask token>


def persistence_model(series):
    """DOCSTRING
    Wrapper for baseline persistence model"""
    return [x for x in series]


def get_cv_params(std_text):
    """accepts stdout generated by cross-validate, returns the "Best Model" Objects
     for each validated fold as a list of lists of ints
     Example output for a cv with four folds and s=24 might be
     [[2, 0, 3, 2, 1, 0, 24],
     [2, 0, 1, 2, 1, 0, 24],
     [3, 0, 3, 0, 1, 1, 24],
     [1, 0, 0, 0, 1, 1, 24]]"""
    mod_pattern = re.compile('Best.*24]')
    pat_list = re.findall(mod_pattern, std_text)
    params = [re.findall('\\d', pat_list[j]) for j, _ in enumerate(pat_list)]
    params = [[(int(param[p]) if p < len(param) - 2 else 24) for p, _ in
        enumerate(param)][:-1] for param in params]
    return params


def test_stationarity(series, print_vals=True):
    """Simple wrapper around adfuller that prints in more readable format
    Adapted slightly from Jason Brownlee's machinelearningmastery.com

    Params:
        series (series) a timeseries
    Returns:
        adfuller test result"""
    result = adfuller(series)
    print('ADF Statistic: %f' % result[0])
    print('p-value: %f' % result[1])
    print('Critical Values:')
    for key, value in result[4].items():
        print('\t%s: %.3f' % (key, value))
    return result


def RMSE(y_true, y_pred, last_only=True):
    """Simple wrapper function on mean_squared_error to return RMSE

    Params:
        predictions (series or array like object), the predicted values
        from model test_data (series or array like object), the true 
        target values

    Returns:
        RMSE (list) list of accumulated RMSE values for each
        observation in consecutive time order i.e. the first return
        value will be just the error of first prediction, second the
        sqrt of mean squared error for first 2 predictions, etc."""
    rmse = None
    if len(y_pred) != len(y_true):
        rmse = 'Test data and predictions must have equal length'
    elif last_only == False:
        rmse = [mean_squared_error(y_true[:i + 1], y_pred[:i + 1], squared=
            False) for i, _ in enumerate(y_pred)]
    else:
        rmse = mean_squared_error(y_true, y_pred, squared=False)
    return rmse


def SMAPE(y_true, y_pred):
    """Wrapper aroumd smape from pmdarima.metrics
    Returns value of smape on a 0-100% scale 
    instead of 0-200% for interpretability see
    https://en.wikipedia.org/wiki/Symmetric_mean_absolute_percentage_error
    and https://alkaline-ml.com/pmdarima/modules/generated/pmdarima.metrics.smape.html
    for reference"""
    return smape(y_true, y_pred) / 2


<mask token>


def sarima_configs(seasonal=[1, 24, 168]):
    """build configuration list to do light gridsearch of SARIMAX 
    models, function is from Jason Brownlee's website:
    machinelearningmastery.com """
    models = list()
    p_params = [0, 1, 2]
    d_params = [0, 1]
    q_params = [0, 1, 2]
    t_params = ['n', 'c', 't', 'ct']
    P_params = [0, 1, 2]
    D_params = [0, 1]
    Q_params = [0, 1, 2]
    m_params = seasonal
    for p in p_params:
        for d in d_params:
            for q in q_params:
                for t in t_params:
                    for P in P_params:
                        for D in D_params:
                            for Q in Q_params:
                                for m in m_params:
                                    cfg = [(p, d, q), (P, D, Q, m), t]
                                    models.append(cfg)
    return models


def SARIMAX_error(series, p=10, d=2, q=2):
    """Simple wrapper that fits SARIMAX model and returns RMSE (raw and
    pct) for the predictions, confidence interval, start of forecast
    and end of actual values"""
    X = series
    train_size = int(len(X) - 48)
    train, test = X[-108:train_size], X[train_size:]
    model = SARIMAX(train, order=(p, d, q), freq='MS', initialization=
        'approximate_diffuse')
    results = model.fit()
    forecast = results.get_forecast(steps=48)
    pred_ci = forecast.conf_int(alpha=0.05)
    predictions = forecast.predicted_mean
    rmse = RMSE(test, predictions)
    return pred_ci, rmse, (train[-1], test[-1])


def SARIMAX_forecast(series, cfg, pred_len):
    """DOCSTRING"""
    X = series
    train_size = int(len(X) - pred_len)
    train, test = X[0:train_size], X[train_size:]
    model = SARIMAX(train, order=cfg[0], seasonal_order=cfg[1], trend=cfg[2
        ], initialization='approximate_diffuse')
    results = model.fit()
    forecast = results.predict(start=test.index[0], end=test.index[-1])
    return forecast

<mask token>


def persistence_model(series):
    """DOCSTRING
    Wrapper for baseline persistence model"""
    return [x for x in series]


def get_cv_params(std_text):
    """accepts stdout generated by cross-validate, returns the "Best Model" Objects
     for each validated fold as a list of lists of ints
     Example output for a cv with four folds and s=24 might be
     [[2, 0, 3, 2, 1, 0, 24],
     [2, 0, 1, 2, 1, 0, 24],
     [3, 0, 3, 0, 1, 1, 24],
     [1, 0, 0, 0, 1, 1, 24]]"""
    mod_pattern = re.compile('Best.*24]')
    pat_list = re.findall(mod_pattern, std_text)
    params = [re.findall('\\d', pat_list[j]) for j, _ in enumerate(pat_list)]
    params = [[(int(param[p]) if p < len(param) - 2 else 24) for p, _ in
        enumerate(param)][:-1] for param in params]
    return params


def test_stationarity(series, print_vals=True):
    """Simple wrapper around adfuller that prints in more readable format
    Adapted slightly from Jason Brownlee's machinelearningmastery.com

    Params:
        series (series) a timeseries
    Returns:
        adfuller test result"""
    result = adfuller(series)
    print('ADF Statistic: %f' % result[0])
    print('p-value: %f' % result[1])
    print('Critical Values:')
    for key, value in result[4].items():
        print('\t%s: %.3f' % (key, value))
    return result


def RMSE(y_true, y_pred, last_only=True):
    """Simple wrapper function on mean_squared_error to return RMSE

    Params:
        predictions (series or array like object), the predicted values
        from model test_data (series or array like object), the true 
        target values

    Returns:
        RMSE (list) list of accumulated RMSE values for each
        observation in consecutive time order i.e. the first return
        value will be just the error of first prediction, second the
        sqrt of mean squared error for first 2 predictions, etc."""
    rmse = None
    if len(y_pred) != len(y_true):
        rmse = 'Test data and predictions must have equal length'
    elif last_only == False:
        rmse = [mean_squared_error(y_true[:i + 1], y_pred[:i + 1], squared=
            False) for i, _ in enumerate(y_pred)]
    else:
        rmse = mean_squared_error(y_true, y_pred, squared=False)
    return rmse


def SMAPE(y_true, y_pred):
    """Wrapper aroumd smape from pmdarima.metrics
    Returns value of smape on a 0-100% scale 
    instead of 0-200% for interpretability see
    https://en.wikipedia.org/wiki/Symmetric_mean_absolute_percentage_error
    and https://alkaline-ml.com/pmdarima/modules/generated/pmdarima.metrics.smape.html
    for reference"""
    return smape(y_true, y_pred) / 2


def fit_sarimax(series, cfg=[(0, 1, 1), (0, 1, 1, 24), 'n'], test_start=
    '2020-07-01 00:00:00'):
    """DOCSTRING accepts a series and SARIMAX configuration, returns model object, train/test sets"""
    train = series[series.index < test_start]
    test = series[test_start:]
    model = SARIMAX(train, order=cfg[0], seasonal_order=cfg[1], trend=cfg[2])
    return train, test, model


def sarima_configs(seasonal=[1, 24, 168]):
    """build configuration list to do light gridsearch of SARIMAX 
    models, function is from Jason Brownlee's website:
    machinelearningmastery.com """
    models = list()
    p_params = [0, 1, 2]
    d_params = [0, 1]
    q_params = [0, 1, 2]
    t_params = ['n', 'c', 't', 'ct']
    P_params = [0, 1, 2]
    D_params = [0, 1]
    Q_params = [0, 1, 2]
    m_params = seasonal
    for p in p_params:
        for d in d_params:
            for q in q_params:
                for t in t_params:
                    for P in P_params:
                        for D in D_params:
                            for Q in Q_params:
                                for m in m_params:
                                    cfg = [(p, d, q), (P, D, Q, m), t]
                                    models.append(cfg)
    return models


def SARIMAX_error(series, p=10, d=2, q=2):
    """Simple wrapper that fits SARIMAX model and returns RMSE (raw and
    pct) for the predictions, confidence interval, start of forecast
    and end of actual values"""
    X = series
    train_size = int(len(X) - 48)
    train, test = X[-108:train_size], X[train_size:]
    model = SARIMAX(train, order=(p, d, q), freq='MS', initialization=
        'approximate_diffuse')
    results = model.fit()
    forecast = results.get_forecast(steps=48)
    pred_ci = forecast.conf_int(alpha=0.05)
    predictions = forecast.predicted_mean
    rmse = RMSE(test, predictions)
    return pred_ci, rmse, (train[-1], test[-1])


def SARIMAX_forecast(series, cfg, pred_len):
    """DOCSTRING"""
    X = series
    train_size = int(len(X) - pred_len)
    train, test = X[0:train_size], X[train_size:]
    model = SARIMAX(train, order=cfg[0], seasonal_order=cfg[1], trend=cfg[2
        ], initialization='approximate_diffuse')
    results = model.fit()
    forecast = results.predict(start=test.index[0], end=test.index[-1])
    return forecast

import pandas as pd
import numpy as np
from sklearn.metrics import mean_squared_error, mean_squared_log_error
from pmdarima.metrics import smape
from statsmodels.tsa.stattools import adfuller
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from statsmodels.tsa.statespace.sarimax import SARIMAX
import re


def persistence_model(series):
    """DOCSTRING
    Wrapper for baseline persistence model"""
    return [x for x in series]


def get_cv_params(std_text):
    """accepts stdout generated by cross-validate, returns the "Best Model" Objects
     for each validated fold as a list of lists of ints
     Example output for a cv with four folds and s=24 might be
     [[2, 0, 3, 2, 1, 0, 24],
     [2, 0, 1, 2, 1, 0, 24],
     [3, 0, 3, 0, 1, 1, 24],
     [1, 0, 0, 0, 1, 1, 24]]"""
    mod_pattern = re.compile('Best.*24]')
    pat_list = re.findall(mod_pattern, std_text)
    params = [re.findall('\\d', pat_list[j]) for j, _ in enumerate(pat_list)]
    params = [[(int(param[p]) if p < len(param) - 2 else 24) for p, _ in
        enumerate(param)][:-1] for param in params]
    return params


def test_stationarity(series, print_vals=True):
    """Simple wrapper around adfuller that prints in more readable format
    Adapted slightly from Jason Brownlee's machinelearningmastery.com

    Params:
        series (series) a timeseries
    Returns:
        adfuller test result"""
    result = adfuller(series)
    print('ADF Statistic: %f' % result[0])
    print('p-value: %f' % result[1])
    print('Critical Values:')
    for key, value in result[4].items():
        print('\t%s: %.3f' % (key, value))
    return result


def RMSE(y_true, y_pred, last_only=True):
    """Simple wrapper function on mean_squared_error to return RMSE

    Params:
        predictions (series or array like object), the predicted values
        from model test_data (series or array like object), the true 
        target values

    Returns:
        RMSE (list) list of accumulated RMSE values for each
        observation in consecutive time order i.e. the first return
        value will be just the error of first prediction, second the
        sqrt of mean squared error for first 2 predictions, etc."""
    rmse = None
    if len(y_pred) != len(y_true):
        rmse = 'Test data and predictions must have equal length'
    elif last_only == False:
        rmse = [mean_squared_error(y_true[:i + 1], y_pred[:i + 1], squared=
            False) for i, _ in enumerate(y_pred)]
    else:
        rmse = mean_squared_error(y_true, y_pred, squared=False)
    return rmse


def SMAPE(y_true, y_pred):
    """Wrapper aroumd smape from pmdarima.metrics
    Returns value of smape on a 0-100% scale 
    instead of 0-200% for interpretability see
    https://en.wikipedia.org/wiki/Symmetric_mean_absolute_percentage_error
    and https://alkaline-ml.com/pmdarima/modules/generated/pmdarima.metrics.smape.html
    for reference"""
    return smape(y_true, y_pred) / 2


def fit_sarimax(series, cfg=[(0, 1, 1), (0, 1, 1, 24), 'n'], test_start=
    '2020-07-01 00:00:00'):
    """DOCSTRING accepts a series and SARIMAX configuration, returns model object, train/test sets"""
    train = series[series.index < test_start]
    test = series[test_start:]
    model = SARIMAX(train, order=cfg[0], seasonal_order=cfg[1], trend=cfg[2])
    return train, test, model


def sarima_configs(seasonal=[1, 24, 168]):
    """build configuration list to do light gridsearch of SARIMAX 
    models, function is from Jason Brownlee's website:
    machinelearningmastery.com """
    models = list()
    p_params = [0, 1, 2]
    d_params = [0, 1]
    q_params = [0, 1, 2]
    t_params = ['n', 'c', 't', 'ct']
    P_params = [0, 1, 2]
    D_params = [0, 1]
    Q_params = [0, 1, 2]
    m_params = seasonal
    for p in p_params:
        for d in d_params:
            for q in q_params:
                for t in t_params:
                    for P in P_params:
                        for D in D_params:
                            for Q in Q_params:
                                for m in m_params:
                                    cfg = [(p, d, q), (P, D, Q, m), t]
                                    models.append(cfg)
    return models


def SARIMAX_error(series, p=10, d=2, q=2):
    """Simple wrapper that fits SARIMAX model and returns RMSE (raw and
    pct) for the predictions, confidence interval, start of forecast
    and end of actual values"""
    X = series
    train_size = int(len(X) - 48)
    train, test = X[-108:train_size], X[train_size:]
    model = SARIMAX(train, order=(p, d, q), freq='MS', initialization=
        'approximate_diffuse')
    results = model.fit()
    forecast = results.get_forecast(steps=48)
    pred_ci = forecast.conf_int(alpha=0.05)
    predictions = forecast.predicted_mean
    rmse = RMSE(test, predictions)
    return pred_ci, rmse, (train[-1], test[-1])


def SARIMAX_forecast(series, cfg, pred_len):
    """DOCSTRING"""
    X = series
    train_size = int(len(X) - pred_len)
    train, test = X[0:train_size], X[train_size:]
    model = SARIMAX(train, order=cfg[0], seasonal_order=cfg[1], trend=cfg[2
        ], initialization='approximate_diffuse')
    results = model.fit()
    forecast = results.predict(start=test.index[0], end=test.index[-1])
    return forecast

import pandas as pd
import numpy as np
from sklearn.metrics import mean_squared_error, mean_squared_log_error
from pmdarima.metrics import smape
from statsmodels.tsa.stattools import adfuller
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from statsmodels.tsa.statespace.sarimax import SARIMAX
import re


def persistence_model(series):
    """DOCSTRING
    Wrapper for baseline persistence model"""
    return [x for x in series]


# Need to Find a Context Manager or Other Way to capture STD OUT
# That is not %%capture, so that this will run without Jupyter
# (This works on std out, but we need to capture std out to feed into it)
def get_cv_params(std_text):
    """accepts stdout generated by cross-validate, returns the "Best Model" Objects
     for each validated fold as a list of lists of ints
     Example output for a cv with four folds and s=24 might be
     [[2, 0, 3, 2, 1, 0, 24],
     [2, 0, 1, 2, 1, 0, 24],
     [3, 0, 3, 0, 1, 1, 24],
     [1, 0, 0, 0, 1, 1, 24]]"""

    # build a match pattern that matches a string that looks like
    # this: 'Best model:  ARIMA(1,0,0)(0,1,1)[24]' from full contents of stdout
    # containing the ARIMA parameters from each fold in a cv search
    mod_pattern = re.compile(r'Best.*24]')

    # Match the pattern against stdout
    pat_list = (re.findall(mod_pattern, std_text))
    # Find all digits in each param in pat_list, save as list of params
    params = [re.findall(r'\d', pat_list[j]) for j, _ in enumerate(pat_list)]

    # this is a bit complicated: but does a fairly simple thing:
    # converts params from a list of lists filled with digits as individual strings of len 1
    # to a list of lists filled with digits as ints. since s=24 is specified in our AutoArima pipe
    # the last two digits are always 2 and 4, we fix this by converting them both to 24 and
    # removing the last digit, converting a list like ['2', '0', '3', '2', '1', '0', '2', '4']
    # to for example [2, 0, 3, 2, 1, 0, 24], which we can feed as SARIMA params
    params = [[int(param[p]) if (p < len(param) - 2) else 24 for p,
               _ in enumerate(param)][:-1] for param in params]

    return params


def test_stationarity(series, print_vals=True):
    """Simple wrapper around adfuller that prints in more readable format
    Adapted slightly from Jason Brownlee's machinelearningmastery.com

    Params:
        series (series) a timeseries
    Returns:
        adfuller test result"""

    result = adfuller(series)
    print('ADF Statistic: %f' % result[0])
    print('p-value: %f' % result[1])
    print('Critical Values:')

    for key, value in result[4].items():
        print('\t%s: %.3f' % (key, value))
    return result


def RMSE(y_true, y_pred, last_only=True):
    """Simple wrapper function on mean_squared_error to return RMSE

    Params:
        predictions (series or array like object), the predicted values
        from model test_data (series or array like object), the true 
        target values

    Returns:
        RMSE (list) list of accumulated RMSE values for each
        observation in consecutive time order i.e. the first return
        value will be just the error of first prediction, second the
        sqrt of mean squared error for first 2 predictions, etc."""

    rmse = None
    # Ensure predictions and test_data are same size
    if len(y_pred) != len(y_true):
        rmse = "Test data and predictions must have equal length"
    # If last_only is false, return the rmse for every point in the prediction set
    # (useful for tracking where model went wrong)
    elif last_only == False:
        rmse = [mean_squared_error(
            y_true[:i+1], y_pred[:i+1], squared=False) for i, _ in enumerate(y_pred)]
    # Normal case: return the rmse value for the full prediction set
    else:
        rmse = mean_squared_error(y_true, y_pred, squared=False)

    return rmse


def SMAPE(y_true, y_pred):
    """Wrapper aroumd smape from pmdarima.metrics
    Returns value of smape on a 0-100% scale 
    instead of 0-200% for interpretability see
    https://en.wikipedia.org/wiki/Symmetric_mean_absolute_percentage_error
    and https://alkaline-ml.com/pmdarima/modules/generated/pmdarima.metrics.smape.html
    for reference"""
    return smape(y_true, y_pred) / 2


def fit_sarimax(series, cfg=[(0, 1, 1), (0, 1, 1, 24), 'n'], test_start='2020-07-01 00:00:00'):
    """DOCSTRING accepts a series and SARIMAX configuration, returns model object, train/test sets"""
    train = series[series.index < test_start]
    test = series[test_start:]

    model = SARIMAX(train, order=cfg[0], seasonal_order=cfg[1], trend=cfg[2])

    return train, test, model


def sarima_configs(seasonal=[1, 24, 168]):
    """build configuration list to do light gridsearch of SARIMAX 
    models, function is from Jason Brownlee's website:
    machinelearningmastery.com """

    models = list()
    # define config lists
    p_params = [0, 1, 2]
    d_params = [0, 1]
    q_params = [0, 1, 2]
    t_params = ['n', 'c', 't', 'ct']
    P_params = [0, 1, 2]
    D_params = [0, 1]
    Q_params = [0, 1, 2]
    m_params = seasonal
    # create config instances
    for p in p_params:
        for d in d_params:
            for q in q_params:
                for t in t_params:
                    for P in P_params:
                        for D in D_params:
                            for Q in Q_params:
                                for m in m_params:
                                    cfg = [(p, d, q), (P, D, Q, m), t]
                                    models.append(cfg)
    return models


def SARIMAX_error(series, p=10, d=2, q=2):
    """Simple wrapper that fits SARIMAX model and returns RMSE (raw and
    pct) for the predictions, confidence interval, start of forecast
    and end of actual values"""

    X = series

    # set trainset to include all but last 48 months (4 years)
    # only training on data between 9-4 years ago
    train_size = int(len(X) - 48)
    train, test = X[-108:train_size], X[train_size:]

    model = SARIMAX(train, order=(p, d, q), freq='MS',
                    initialization='approximate_diffuse')

    results = model.fit()

    # Predict 48 months from end of train set
    forecast = results.get_forecast(steps=48)
    pred_ci = forecast.conf_int(alpha=.05)

    predictions = forecast.predicted_mean

    rmse = RMSE(test, predictions)
    # pct = error_as_pct(rmse, train[-1], test[-1])

    return pred_ci, rmse, (train[-1], test[-1])  # , pct


def SARIMAX_forecast(series, cfg, pred_len):
    """DOCSTRING"""

    X = series

    # set trainset to include all but last 48 months (4 years)
    # only training on data between 9-4 years ago
    train_size = int(len(X) - pred_len)
    train, test = X[0:train_size], X[train_size:]

    model = SARIMAX(train, order=cfg[0], seasonal_order=cfg[1],
                    trend=cfg[2], initialization='approximate_diffuse')

    results = model.fit()

    # Predict 48 months from end of train set
    forecast = results.predict(start=test.index[0], end=test.index[-1])

    return forecast


#     pred_ci = forecast.conf_int(alpha=.05)

#     predictions = forecast.predicted_mean

#     rmse = RMSE(test, predictions)
#     pct = error_as_pct(rmse, train[-1], test[-1])

#     ROI = (predictions[-1] - train[-1]) / train[-1]

#     #return {'pred_ci': pred_ci, 'rmse': rmse, 'pct_error': pct, 'test': test, 'predictions': predictions, 'series': X}
#     return ROI
