diff --git a/arch/univariate/base.py b/arch/univariate/base.py index c2001fde74..f5fa739feb 100644 --- a/arch/univariate/base.py +++ b/arch/univariate/base.py @@ -35,7 +35,7 @@ ) from arch.univariate.distribution import Distribution, Normal from arch.univariate.volatility import ConstantVariance, VolatilityProcess -from arch.utility.array import ensure1d +from arch.utility.array import append_same_type, ensure1d from arch.utility.exceptions import ( ConvergenceWarning, DataScaleWarning, @@ -230,6 +230,28 @@ def name(self) -> str: """The name of the model.""" return self._name + def append(self, y: ArrayLike) -> None: + """ + Append data to the model + + Parameters + ---------- + y : ndarray or Series + Data to append + + Returns + ------- + ARCHModel + Model with data appended + """ + _y = ensure1d(y, "y", series=True) + self._y_original = append_same_type(self._y_original, y) + self._y_series = pd.concat([self._y_series, _y]) + self._y = np.concatenate([self._y, np.asarray(_y)]) + + self._fit_indices: [0, int(self._y.shape[0])] + self._fit_y = self._y + def constraints(self) -> tuple[Float64Array, Float64Array]: """ Construct linear constraint arrays for use in non-linear optimization diff --git a/arch/univariate/mean.py b/arch/univariate/mean.py index 368ac97202..37ae703341 100644 --- a/arch/univariate/mean.py +++ b/arch/univariate/mean.py @@ -39,6 +39,7 @@ SkewStudent, StudentsT, ) +from arch.utility.array import append_same_type if TYPE_CHECKING: # Fake path to satisfy mypy @@ -269,6 +270,7 @@ def __init__( distribution=distribution, rescale=rescale, ) + self._x_original = x self._x = x self._x_names: list[str] = [] self._x_index: None | NDArray | pd.Index = None @@ -307,6 +309,25 @@ def __init__( self._init_model() + def append(self, y: ArrayLike, x: ArrayLike2D | None = None) -> None: + super().append(y) + if x is not None: + if self._x is None: + raise ValueError("x was not provided in the original model") + _x = np.atleast_2d(np.asarray(x)) + if _x.ndim != 2: + raise ValueError("x must be 2-d") + elif _x.shape[1] != self._x.shape[1]: + raise ValueError( + "x must have the same number of columns as the original x" + ) + self._x_original = append_same_type(self._x_original, x) + self._x = np.asarray(self._x_original) + if self._x.shape[0] != self._y.shape[0]: + raise ValueError("x must have the same number of observations as y") + + self._init_model() + def _scale_changed(self): """ Called when the scale has changed. This allows the model diff --git a/arch/utility/array.py b/arch/utility/array.py index cdec75c5e3..437cfc821b 100644 --- a/arch/utility/array.py +++ b/arch/utility/array.py @@ -12,7 +12,16 @@ from typing import Any, Literal, overload import numpy as np -from pandas import DataFrame, DatetimeIndex, Index, NaT, Series, Timestamp, to_datetime +from pandas import ( + DataFrame, + DatetimeIndex, + Index, + NaT, + Series, + Timestamp, + concat, + to_datetime, +) from arch.typing import AnyPandas, ArrayLike, DateLike, NDArray @@ -310,3 +319,23 @@ def find_index(s: AnyPandas, index: int | DateLike) -> int: if loc.size == 0: raise ValueError("index not found") return int(loc) + + +def append_same_type(original, new): + if not isinstance(new, type(original)): + raise TypeError( + "Input data must be the same type as the original data. " + f"Got {type(new)}, expected {type(original)}." + ) + if isinstance(original, (Series, DataFrame)): + extended = concat([original, new], axis=0) + elif isinstance(original, np.ndarray): + extended = np.concatenate([original, new]) + elif isinstance(original, list): + extended = original + new + else: + raise TypeError( + "Input data must be a pandas Series, DataFrame, numpy ndarray, or " + f"list. Got {type(original)}." + ) + return extended