# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/modules/02_models/08_naive.ipynb.

# %% ../../nbs/modules/02_models/08_naive.ipynb #3167faec
from __future__ import annotations
from typing import List, Dict, Optional, Callable, Tuple, Any, Union
import numpy as np
import pandas as pd
import copy
from ..transformations import (box_cox_transform, back_box_cox_transform)
from ..model_selection import SplitTimeSeries
# dot not show warnings
import warnings
warnings.filterwarnings("ignore")
import copy
from scipy.stats import t
import re # for regex escaping to build drop patterns
import warnings
warnings.filterwarnings("ignore")

class naive:
    def __init__(
        self,
        target_col: str,
        season_period: Optional[int] = None,
        box_cox: bool = False,
        box_cox_lmda: Optional[float] = None,
        box_cox_biasadj: bool = False,
    ) -> None:
 
        """
        Naïve forecaster.
    
        Two modes controlled by ``season_period``:
    
        * **Non-seasonal** (``season_period=None``): every forecast step repeats
        the last observed value in the training series.
        * **Seasonal** (``season_period=m``): forecast values are taken from the
        last complete season and cycled forward — i.e. step ``h`` is predicted
        by ``y[T - m + ((h-1) % m)]``, where ``T`` is the last training index.
    
        Parameters
        ----------
        target_col : str
            Name of the target variable column.
        season_period : int or None, default None
            Seasonal period ``m``.  ``None`` selects the non-seasonal naïve
            method.  When provided and the training series is shorter than
            ``m``, ``forecast`` returns an array of ``NaN``.
        box_cox : bool, default False
            Apply a manual Box-Cox transformation to the target before storing
            the training values.  The transformation is inverted on the output
            of ``forecast``.
        box_cox_lmda : float or None, default None
            Lambda for the manual Box-Cox transform.  ``None`` means
            auto-estimate from the training data.
        box_cox_biasadj : bool, default False
            Bias adjustment when inverting the manual Box-Cox on forecasts.
        """
 
        self.target_col  = target_col
        self.season_period = season_period
        self.box_cox     = box_cox
        self.lamda       = box_cox_lmda
        self.biasadj     = box_cox_biasadj
 
        # ── placeholders set during fit ────────────────────────────────────────
        self.tuned_params   = None
        self.actuals        = None
        self.prob_forecasts = None
 
    # ─────────────────────────────────────────────────────────────────────────
    # DATA PREPARATION
    # ─────────────────────────────────────────────────────────────────────────
 
    def data_prep(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Apply preprocessing and return a cleaned DataFrame.
 
        Pipeline: manual Box-Cox → ``dropna`` → return target column only.
 
        Parameters
        ----------
        df : pd.DataFrame
            Input DataFrame containing the target column.
 
        Returns
        -------
        pd.DataFrame
        """
        dfc = df.copy()
 
        # ── manual Box-Cox ────────────────────────────────────────────────────
        if self.box_cox:
            self.is_zero = np.any(np.array(dfc[self.target_col]) < 1)
            trans_data, self.lamda = box_cox_transform(
                x=dfc[self.target_col], shift=self.is_zero,
                box_cox_lmda=self.lamda,
            )
            dfc[self.target_col] = trans_data
 
        return dfc[[self.target_col]].dropna()
 
    # ─────────────────────────────────────────────────────────────────────────
    # FIT
    # ─────────────────────────────────────────────────────────────────────────
 
    def fit(self, df: pd.DataFrame) -> None:
        """
        Store the values needed for naïve forecasting.
 
        No statistical model is estimated.  ``fit`` simply applies
        ``data_prep`` and records the training series so that ``forecast``
        can replicate the correct naïve pattern.
 
        Parameters
        ----------
        df : pd.DataFrame
            Training DataFrame containing the target column.
 
        Returns
        -------
        None
        """
        model_df = self.data_prep(df)
        self.y   = model_df[self.target_col].to_numpy()
 
    # ─────────────────────────────────────────────────────────────────────────
    # FORECAST
    # ─────────────────────────────────────────────────────────────────────────
 
    def forecast(
        self,
        H: int,
        exog: Optional[pd.DataFrame] = None,
    ) -> np.ndarray:
        """
        Generate naïve forecasts.
 
        Parameters
        ----------
        H : int
            Forecast horizon.
        exog : pd.DataFrame or None
            Accepted for API consistency with other models but silently ignored — naïve forecasts do not use exogenous variables.
 
        Returns
        -------
        np.ndarray
            Forecast values of length `H`.
        """
        s = self.y  # already cleaned and Box-Cox transformed by data_prep
 
        if self.season_period is None:
            # ── Non-seasonal naïve: repeat the last observed value ─────────────
            if len(s) == 0:
                forecasts = np.full(H, np.nan)
            else:
                forecasts = np.full(H, s[-1])
 
        else:
            # ── Seasonal naïve: cycle the last season forward ──────────────────
            m = self.season_period
            if len(s) < m:
                forecasts = np.full(H, np.nan)
            else:
                last_season = s[-m:]
                repeats     = H // m
                remainder   = H % m
                forecasts   = np.tile(last_season, repeats)
                if remainder > 0:
                    forecasts = np.concatenate([forecasts, last_season[:remainder]])
 
        # ── non-negativity ────────────────────────────────────────────────────
        forecasts = np.array([max(0, x) for x in forecasts])
 
        # ── invert manual Box-Cox ─────────────────────────────────────────────
        if self.box_cox:
            forecasts = back_box_cox_transform(
                y_pred=forecasts, lmda=self.lamda,
                shift=self.is_zero, box_cox_biasadj=self.biasadj,
            )
 
        return forecasts
 
    def copy(self):
        return copy.deepcopy(self)
 
    # ─────────────────────────────────────────────────────────────────────────
    # CROSS-VALIDATION  (identical signature and logic to ets.cross_validate)
    # ─────────────────────────────────────────────────────────────────────────
 
    def cross_validate(
        self,
        df: pd.DataFrame,
        cv_split: int,
        test_size: int,
        metrics: List[Callable],
        step_size: int = 1,
        h_split_point: Optional[int] = None,
        cv_df: bool = False,
    ) -> Tuple[pd.DataFrame, pd.DataFrame]:
        """
        Run time-series cross-validation.
 
        Parameters
        ----------
        df : pd.DataFrame
            Full dataset.
        cv_split : int
            Number of CV folds.
        test_size : int
            Test window size per fold.
        metrics : list of callable
            Metric functions (e.g. ``[MAE, RMSE]``).
        step_size : int, default 1
            Step size to advance the test window each fold.
        h_split_point : int or None, default None
            Split the test window into two sub-horizons for separate short- and long-term evaluation.
        cv_df : bool, default False
            If ``True``, return a fold-level prediction DataFrame alongside the summary.
 
        Returns
        -------
        overall_performance : pd.DataFrame and cv_predictions : pd.DataFrame
            Summary DataFrame with mean metric scores across folds, and (optionally) a fold-level DataFrame with true vs. predicted values for each fold.
        """
        cv_df_ = pd.DataFrame()
        tscv = SplitTimeSeries(
            n_splits=cv_split, test_size=test_size, step_size=step_size
        )
        metrics_dict = {m.__name__: [] for m in metrics}
        if h_split_point is not None:
            metrics_dict1 = {m.__name__: [] for m in metrics}
            metrics_dict2 = {m.__name__: [] for m in metrics}
 
        for idx, (train_index, test_index) in enumerate(tscv.split(df)):
            train, test = df.iloc[train_index], df.iloc[test_index]
            x_test = test.drop(columns=[self.target_col])
            y_test = np.array(test[self.target_col])
 
            self.fit(train)
            bb_forecast = np.array(self.forecast(test_size, x_test))
 
            for m in metrics:
                if m.__name__ in ["MASE", "SMAE", "SRMSE", "RMSSE"]:
                    eval_val = m(y_test, bb_forecast, train[self.target_col])
                else:
                    eval_val = m(y_test, bb_forecast)
                metrics_dict[m.__name__].append(eval_val)
 
            if h_split_point is not None and isinstance(h_split_point, int):
                y_test_1, y_test_2 = y_test[:h_split_point], y_test[h_split_point:]
                bb_forecast_1, bb_forecast_2 = (
                    bb_forecast[:h_split_point], bb_forecast[h_split_point:]
                )
                for m in metrics:
                    if m.__name__ in ["MASE", "SMAE", "SRMSE", "RMSSE"]:
                        eval_val1 = m(y_test_1, bb_forecast_1, np.array(train[self.target_col]))
                        eval_val2 = m(y_test_2, bb_forecast_2, np.array(train[self.target_col]))
                    else:
                        eval_val1 = m(y_test_1, bb_forecast_1)
                        eval_val2 = m(y_test_2, bb_forecast_2)
                    metrics_dict1[m.__name__].append(eval_val1)
                    metrics_dict2[m.__name__].append(eval_val2)
 
            if cv_df:
                split_results = {
                    "cutoff": np.repeat(test.index[0], len(test)),
                    "index":  test.index,
                    "split":  np.repeat(f"fold_{idx + 1}", len(test)),
                    "y_true": y_test,
                    "y_pred": bb_forecast,
                }
                cv_df_ = pd.concat(
                    [cv_df_, pd.DataFrame(split_results)], ignore_index=True
                )
 
        overall_performance = pd.DataFrame(
            [[m.__name__, np.mean(metrics_dict[m.__name__])] for m in metrics],
            columns=["eval_metric", "score"],
        )
 
        if h_split_point is not None and isinstance(h_split_point, int):
            perf_1_df = pd.DataFrame(
                [[m.__name__, np.mean(metrics_dict1[m.__name__])] for m in metrics],
                columns=["eval_metric", f"score_before_{h_split_point}"],
            )
            perf_2_df = pd.DataFrame(
                [[m.__name__, np.mean(metrics_dict2[m.__name__])] for m in metrics],
                columns=["eval_metric", f"score_after_{h_split_point}"],
            )
            overall_performance = (
                overall_performance
                .merge(perf_1_df, on="eval_metric")
                .merge(perf_2_df, on="eval_metric")
            )
 
        return overall_performance, cv_df_

# %% auto #0
__all__ = ['naive']
