Source code for finstmt.forecast.models.cagr
import warnings
from typing import Optional
import pandas as pd
from finstmt.exc import ForecastNotFitException
from finstmt.forecast.models.base import ForecastModel
[docs]class CAGRModel(ForecastModel):
    cagr: Optional[float] = None
    stderr: Optional[float] = None
    last_value: Optional[float] = None
[docs]    def fit(self, series: pd.Series):
        y_T = series.iloc[-1]
        self.last_value = y_T
        y_0 = series.iloc[0]
        if y_0 <= 0 or y_T <= 0:
            # Invalid data for CAGR method
            self.cagr = 0
            self.stderr = 0
            specific_messages = []
            if y_0 == 0:
                specific_messages.append("y_0 is 0")
            elif y_0 < 0:
                specific_messages.append("y_0 is negative")
            if y_T == 0:
                specific_messages.append("y_T is 0")
            elif y_T < 0:
                specific_messages.append("y_T is negative")
            specific_message = ", ".join(specific_messages)
            message = (
                f"CAGR not an appropriate method for {self.base_config.display_name} "
                f"as {specific_message}. Setting to 0 growth (recent value forecast)"
            )
            warnings.warn(message)
        else:
            n = len(series)
            self.cagr = (y_T / y_0) ** (1 / n) - 1
            self.stderr = series.pct_change().std() / (n**0.5)
        super().fit(series) 
[docs]    def predict(self) -> pd.Series:
        if (
            self.cagr is None
            or self.stderr is None
            or self.last_value is None
            or self.orig_series is None
        ):
            raise ForecastNotFitException("call .fit before .predict")
        adj_cagr = (1 + self.cagr) ** (self.desired_freq_t_multiplier) - 1
        adj_stderr = (1 + self.stderr) ** (self.desired_freq_t_multiplier) - 1
        cagr_dict = dict(
            lower=self.cagr - self.stderr * 2,
            upper=self.cagr + self.stderr * 2,
            mean=self.cagr,
        )
        adj_cagr_dict = dict(
            lower=adj_cagr - adj_stderr * 2,
            upper=adj_cagr + adj_stderr * 2,
            mean=adj_cagr,
        )
        # Start from last period, apply growth to predict into future
        future_df = pd.DataFrame(index=self._future_date_range)
        for col_name, cagr in adj_cagr_dict.items():
            last_value = self.last_value
            future_values = []
            for _ in range(self.config.periods):
                next_value = last_value * (1 + cagr)
                future_values.append(next_value)
                last_value = next_value
            future_df[col_name] = future_values
        self.result = future_df["mean"]
        # Start from last period, work back to earliest period removing growth to assess fit
        orig_dates = self.orig_series.index
        past_df = pd.DataFrame(index=reversed(orig_dates))
        for col_name, cagr in cagr_dict.items():
            last_value = self.last_value
            past_values = [self.last_value]
            for _ in range(len(orig_dates) - 1):
                next_value = last_value / (1 + cagr)
                past_values.append(next_value)
                last_value = next_value
            past_df[col_name] = past_values
        self.result_df = past_df.append(future_df).sort_index()
        super().predict()
        return self.result