# AUTOGENERATED! DO NOT EDIT! File to edit: nbs/03_Metrics.ipynb (unless otherwise specified).

__all__ = ['__total_error', '__absolute_error', '__squared_error', '__mean_error', '__mean_absolute_error',
           '__mean_squared_error', '__root_mean_square_error', '__mean_percentage_error',
           '__mean_absolute_percent_error', 'SUMMARY']

# Cell
import numpy as np
from numba import jit
import pandas as pd
from time_series_model_basics import simulate_data, moving_average, metrics
from typing import List

# Cell
def __total_error(
    *,
    ts: np.array,
    f: np.array,
) -> float:
    """
    Computes the total error:

    $\sum^{n}_{i}(ts[i] - f[i]), n = len(ts) = len(f)$

    .Ignores nan values in times-series or the forecast.

    ------
    Parameters
    ------

    ts : np.array with the time-series
    f :  np.array with the forecast

    -------
    Returns
    -------

    """

    d = ts - f
    return np.sum(d, where=~np.isnan(d))

# Cell
def __absolute_error(
    *,
    ts: np.array,
    f: np.array,
) -> float:
    """
    Computes the absolute error:

    $\sum^{n}_{i}|ts[i] - f[i]|, n = len(ts) = len(f)$

    .Ignores nan values in times-series or the forecast.

    ------
    Parameters
    ------

    ts : np.array with the time-series
    f :  np.array with the forecast

    -------
    Returns
    -------

    """

    d = np.abs(ts - f)

    return np.sum(d, where=~np.isnan(d))

# Cell
def __squared_error(
    *,
    ts: np.array,
    f: np.array,
) -> float:
    """
    Computes the squared error:

    $\sum^{n}_{i}|ts[i] - f[i]|**2, n = len(ts) = len(f)$

    .Ignores nan values in times-series or the forecast.

    ------
    Parameters
    ------

    ts : np.array with the time-series
    f :  np.array with the forecast

    -------
    Returns
    -------

    """

    d2 = (ts - f)**2
    return np.sum(d2, where=~np.isnan(d2))

# Cell
def __mean_error(
    *,
    ts: np.array,
    f: np.array,
) -> float:
    """
    Computes the mean error:

    $(1/n)\sum^{n*}_{i}ts[i] - f[i], n* = len(ts) = len(f)$

    .Ignores nan values in times-series or the forecast.
     Value n is n* minus the ignored values

    ------
    Parameters
    ------

    ts : np.array with the time-series
    f :  np.array with the forecast

    -------
    Returns
    -------

    """
    d = ts - f
    w = ~np.isnan(d)
    n = len(d[w])

    return np.divide(
        np.sum(d, where=w),
        n,
        where=n > 0,
    )

# Cell
def __mean_absolute_error(
    *,
    ts: np.array,
    f: np.array,
) -> float:
    """
    Computes the mean absolute error:

    $$(1/n)\sum^{n}_{i} | ts[i] - f[i]|, n = len(ts) = len(f)$$

    .Ignores nan values in times-series or the forecast.

    ------
    Parameters
    ------

    ts : np.array with the time-series
    f :  np.array with the forecast

    -------
    Returns
    -------

    """

    d = np.abs(ts - f)
    w = ~np.isnan(d)
    n = len(d[w])

    return np.divide(
        np.sum(d, where=w),
        n,
        where=n > 0,
    )

# Cell
def __mean_squared_error(
    *,
    ts: np.array,
    f: np.array,
) -> float:
    """
    Computes the mean squared error:

    $$(1/n)\sum^{n}_{i} | ts[i] - f[i]|**2, n = len(ts) = len(f)$$

    .Ignores nan values in times-series or the forecast.

    ------
    Parameters
    ------

    ts : np.array with the time-series
    f :  np.array with the forecast

    -------
    Returns
    -------

    """
    d = (ts - f)**2
    w = ~np.isnan(d)
    n = len(d[w])

    return np.divide(
        np.sum(d, where=w),
        n,
        where=n > 0,
    )

# Cell
def __root_mean_square_error(
    *,
    ts: np.array,
    f: np.array,
) -> float:
    """
    Computes the root mean square error:

    $$\sqrt{(1/n)\sum^{n}_{i} | ts[i] - f[i]|**2},  n = len(ts) = len(f)$$

    .Ignores nan values in times-series or the forecast.

    ------
    Parameters
    ------

    ts : np.array with the time-series
    f :  np.array with the forecast

    -------
    Returns
    -------

    """

    return np.sqrt(__mean_squared_error(ts=ts, f=f))

# Cell
def __mean_percentage_error(
    *,
    ts: np.array,
    f: np.array,
) -> float:
    """
    Computes the root mean percentage error:

    $$ (1/n)\sum ( ts[i] - f[i]) /ts[i],  n* = len(ts) = len(f)$$

    .Ignores nan values and division by zero.
    n is n* minus the ignored values

    ------
    Parameters
    ------

    ts : np.array with the time-series
    f :  np.array with the forecast

    -------
    Returns
    -------

    """

    pe = np.divide(ts - f, ts, where=ts != 0)
    w = ~np.isnan(pe)

    n = len(pe[w])

    return np.divide(
        np.sum(pe, where=w),
        n,
        where=n > 0,
    )

# Cell
def __mean_absolute_percent_error(
    *,
    ts: np.array,
    f: np.array,
) -> float:
    """
    Computes the mean absolute percentage error:

    $$ (1/n)\sum | ts[i] - f[i]  /ts[i] \vert,  n* = len(ts) = len(f)$$

    .Ignores nan values and division by zero.
    n is n* minus the ignored values

    ------
    Parameters
    ------

    ts : np.array with the time-series
    f :  np.array with the forecast

    -------
    Returns
    -------

    """

    pe = abs(np.divide(ts - f, ts, where=ts != 0))
    w = ~np.isnan(pe)

    n = len(pe[w])

    return np.divide(
        np.sum(pe, where=w),
        n,
        where=n > 0,
    )

# Cell
def SUMMARY(
    df: pd.DataFrame = None,
    *,
    val_col: str = None,
    pred_cols: List[str] = None,
) -> pd.DataFrame:
    """
    Summary of Prediction Metrics

    -----
    Parameters
    -----

    df : dataframe , dafault None. If None it will generate a simulated dataframe.
    val_col : str , default None.  Name of the column with the actual values.
              It should be provided when a datafre is provided.
    pred_cols : List[str] . Names of the columns with the predictions for the values
              If not provided it will take all dataframe columns except val_col

    -----
    Returns
    ------

    dataframe : Summary results


    """
    if df is None:
        df = simulate_data.pandas_time_series()
        df, _ = moving_average.SMA(
            1,
            4,
            df=df,
        )

        val_col = 'time_series'

    if val_col is None:
        raise Exception('When df is provided val_col cannot be None')

    if pred_cols is None:
        pred_cols = [x for x in df.columns if x != val_col]

    actual_values = df[val_col].to_numpy()

    metric_names = [fn for fn in metrics.__all__ if fn[:2] == '__']
    result_df = pd.DataFrame()

    for col in pred_cols:
        forecast = df[col].to_numpy()
        result_df[col] = pd.Series({
            ' '.join(fn[2:].split('_')[:-1]): eval(fn)(ts=actual_values,
                                                       f=forecast)
            for fn in metric_names
        })
    result_df.index.name = 'Error'
    return result_df