diff --git a/arch/bootstrap/_samplers.pyi b/arch/bootstrap/_samplers.pyi index 7168aff3a5..c832be4d46 100644 --- a/arch/bootstrap/_samplers.pyi +++ b/arch/bootstrap/_samplers.pyi @@ -1,5 +1,5 @@ -from arch.typing import Float64Array, Int64Array +from arch.typing import Float64Array, Int64Array1D def stationary_bootstrap_sample( - indices: Int64Array, u: Float64Array, p: float -) -> Int64Array: ... + indices: Int64Array1D, u: Float64Array, p: float +) -> Int64Array1D: ... diff --git a/arch/bootstrap/base.py b/arch/bootstrap/base.py index aceaa3fa2f..b4d54d7c5f 100644 --- a/arch/bootstrap/base.py +++ b/arch/bootstrap/base.py @@ -15,6 +15,7 @@ BootstrapIndexT, Float64Array, Int64Array, + Int64Array1D, Literal, NDArray, RandomStateState, @@ -55,12 +56,12 @@ def _get_prng_state( def _get_random_integers( prng: Union[Generator, RandomState], upper: int, *, size: int = 1 -) -> Int64Array: +) -> Int64Array1D: if isinstance(prng, Generator): - return prng.integers(upper, size=size, dtype=np.int64) + return cast(Int64Array1D, prng.integers(upper, size=size, dtype=np.int64)) else: assert isinstance(prng, RandomState) - return prng.randint(upper, size=size, dtype=np.int64) + return cast(Int64Array1D, prng.randint(upper, size=size, dtype=np.int64)) def _single_optimal_block(x: Float64Array) -> tuple[float, float]: @@ -438,7 +439,7 @@ def __init__( raise ValueError( "All inputs must have the same number of elements in axis 0" ) - self._index: BootstrapIndexT = np.arange(self._num_items) + self._index: BootstrapIndexT = np.arange(self._num_items, dtype=np.int64) self._parameters: list[int] = [] self.pos_data: tuple[Union[AnyArray, pd.Series, pd.DataFrame], ...] = args @@ -626,7 +627,7 @@ def reset(self, use_seed: bool = True) -> None: False or if no seed has been set, the bootstrap will be reset to the initial state. Default is True """ - self._index = np.arange(self._num_items) + self._index = np.arange(self._num_items, dtype=np.int64) self._resample() self.state = self._initial_state @@ -1380,7 +1381,7 @@ def __init__( def update_indices( self, - ) -> tuple[list[Int64Array], dict[str, Int64Array]]: + ) -> tuple[list[Int64Array1D], dict[str, Int64Array1D]]: """ Update indices for the next iteration of the bootstrap. This must be overridden when creating new bootstraps. @@ -1424,8 +1425,14 @@ def reset(self, use_seed: bool = True) -> None: False or if no seed has been set, the bootstrap will be reset to the initial state. Default is True """ - pos_indices = [np.arange(self._num_arg_items[i]) for i in range(self._num_args)] - kw_indices = {key: np.arange(self._num_kw_items[key]) for key in self._kwargs} + pos_indices: list[Int64Array1D] = [ + np.arange(self._num_arg_items[i], dtype=np.int64) + for i in range(self._num_args) + ] + kw_indices: dict[str, Int64Array1D] = { + key: np.arange(self._num_kw_items[key], dtype=np.int64) + for key in self._kwargs + } self._index = pos_indices, kw_indices self._resample() self.state = self._initial_state @@ -1595,21 +1602,21 @@ def _repr_html(self) -> str: html += ", ID: " + hex(id(self)) + ")" return html - def update_indices(self) -> Int64Array: + def update_indices(self) -> Int64Array1D: num_blocks = self._num_items // self.block_size if num_blocks * self.block_size < self._num_items: num_blocks += 1 indices = _get_random_integers( self._generator, self._num_items, size=num_blocks ) - indices = indices[:, None] + np.arange(self.block_size) - indices = indices.flatten() + _indices = indices[:, None] + np.arange(self.block_size, dtype=np.int64) + indices = _indices.flatten() indices %= self._num_items if indices.shape[0] > self._num_items: - return indices[: self._num_items] + return cast(Int64Array1D, indices[: self._num_items]) else: - return indices + return cast(Int64Array1D, indices) class StationaryBootstrap(CircularBlockBootstrap): @@ -1707,7 +1714,7 @@ def __init__( ) self._p = 1.0 / block_size - def update_indices(self) -> Int64Array: + def update_indices(self) -> Int64Array1D: indices = _get_random_integers( self._generator, self._num_items, size=self._num_items ) @@ -1816,7 +1823,7 @@ def __init__( block_size, *args, random_state=random_state, seed=seed, **kwargs ) - def update_indices(self) -> Int64Array: + def update_indices(self) -> Int64Array1D: num_blocks = self._num_items // self.block_size if num_blocks * self.block_size < self._num_items: num_blocks += 1 @@ -1826,7 +1833,7 @@ def update_indices(self) -> Int64Array: indices = indices.flatten() if indices.shape[0] > self._num_items: - return indices[: self._num_items] + return cast(Int64Array1D, indices[: self._num_items]) else: return indices @@ -1846,6 +1853,6 @@ def __init__( def update_indices( self, ) -> Union[ - Int64Array, tuple[list[Int64Array], dict[str, Int64Array]] + Int64Array1D, tuple[list[Int64Array1D], dict[str, Int64Array1D]] ]: # pragma: no cover raise NotImplementedError diff --git a/arch/bootstrap/multiple_comparison.py b/arch/bootstrap/multiple_comparison.py index 40bd790824..62ddfebf92 100644 --- a/arch/bootstrap/multiple_comparison.py +++ b/arch/bootstrap/multiple_comparison.py @@ -16,7 +16,7 @@ ArrayLike2D, BoolArray, Float64Array, - Int64Array, + Int64Array1D, IntArray, Literal, Uint32Array, @@ -448,7 +448,7 @@ def __init__( self.k: int = self.models.shape[1] self.reps: int = reps self.size: float = size - self._superior_models: Optional[list[Hashable]] = None + self._superior_models: Optional[list[int]] = None self.bootstrap: CircularBlockBootstrap = self.spa.bootstrap self._model = "StepM" @@ -472,16 +472,13 @@ def compute(self) -> None: # 1. Run SPA self.spa.compute() # 2. If any models superior, store indices, remove and re-run SPA - better_models = list(self.spa.better_models(self.size)) + better_models = list(int(i) for i in self.spa.better_models(self.size)) all_better_models = better_models[:] # 3. Stop if nothing superior while better_models and (len(better_models) < self.k): # A. Use Selector to remove better models selector = np.ones(self.k, dtype=np.bool_) - if isinstance(self.models, pd.DataFrame): # Columns - selector[self.models.columns.isin(all_better_models)] = False - else: - selector[np.array(all_better_models)] = False + selector[np.array(all_better_models)] = False self.spa.subset(selector) # B. Rerun self.spa.compute() @@ -494,7 +491,7 @@ def compute(self) -> None: self._superior_models = all_better_models @property - def superior_models(self) -> list[Hashable]: + def superior_models(self) -> Union[list[int], Sequence[Hashable]]: """ List of the indices or column names of the superior models @@ -507,6 +504,8 @@ def superior_models(self) -> list[Hashable]: if self._superior_models is None: msg = "compute must be called before accessing superior_models" raise RuntimeError(msg) + if isinstance(self.models, pd.DataFrame): + return list(self.models.columns[self._superior_models]) return self._superior_models @@ -781,7 +780,7 @@ def better_models( self, pvalue: float = 0.05, pvalue_type: Literal["lower", "consistent", "upper"] = "consistent", - ) -> Union[Int64Array, list[Hashable]]: + ) -> Union[Int64Array1D]: """ Returns set of models rejected as being equal-or-worse than the benchmark @@ -811,10 +810,7 @@ def better_models( crit_val = self.critical_values(pvalue=pvalue)[pvalue_type] better_models = self._loss_diff.mean(0) > crit_val better_models = np.logical_and(better_models, self._selector) - if isinstance(self.models, pd.DataFrame): - return list(self.models.columns[better_models]) - else: - return np.argwhere(better_models).flatten() + return np.argwhere(better_models).flatten() def _check_compute(self) -> None: if self._pvalues: diff --git a/arch/compat/numba.py b/arch/compat/numba.py index c59b68687c..c69b5aa2b0 100644 --- a/arch/compat/numba.py +++ b/arch/compat/numba.py @@ -4,6 +4,13 @@ from arch.utility.exceptions import PerformanceWarning +try: + import numba # noqa + + HAS_NUMBA = True +except ImportError: + HAS_NUMBA = False + DISABLE_NUMBA = os.environ.get("ARCH_DISABLE_NUMBA", False) in ("1", "true", "True") performance_warning: str = """ @@ -51,4 +58,4 @@ def wrapper(*args: Any, **kwargs: Any) -> Callable[..., Any]: return wrap -__all__ = ["jit", "PerformanceWarning"] +__all__ = ["jit", "PerformanceWarning", "HAS_NUMBA", "DISABLE_NUMBA"] diff --git a/arch/covariance/kernel.py b/arch/covariance/kernel.py index 1a0a88591a..84e79efacd 100644 --- a/arch/covariance/kernel.py +++ b/arch/covariance/kernel.py @@ -1,4 +1,4 @@ -from arch.compat.numba import jit +from arch.compat.numba import DISABLE_NUMBA, HAS_NUMBA, jit from abc import ABC, abstractmethod from functools import cached_property @@ -136,8 +136,7 @@ def one_sided_strict(self) -> Union[Float64Array, DataFrame]: return self._wrap(self._oss) -@jit(nopython=True) -def _cov_jit( +def _cov_kernel( df: int, k: int, num_weights: int, w: np.ndarray, x: np.ndarray ) -> np.ndarray: oss = np.zeros((k, k)) @@ -146,6 +145,12 @@ def _cov_jit( return oss +if HAS_NUMBA and not DISABLE_NUMBA: + _cov_jit = jit(_cov_kernel, nopython=True) +else: + _cov_jit = _cov_kernel + + class CovarianceEstimator(ABC): r""" %(kernel_name)s kernel covariance estimation. diff --git a/arch/tests/unitroot/cointegration_data.py b/arch/tests/unitroot/cointegration_data.py index 594e8f362f..597097dcdb 100644 --- a/arch/tests/unitroot/cointegration_data.py +++ b/arch/tests/unitroot/cointegration_data.py @@ -1,10 +1,12 @@ from arch.compat.pandas import MONTH_END +from typing import cast + import numpy as np import pandas as pd import pytest -from arch.typing import ArrayLike2D, Float64Array +from arch.typing import ArrayLike2D, Float64Array, Float64Array2D @pytest.fixture(scope="module", params=[True, False]) @@ -65,6 +67,8 @@ def trivariate_data(request) -> tuple[ArrayLike2D, ArrayLike2D]: dt_index = pd.date_range("1-1-2000", periods=nobs, freq=MONTH_END) cols = [f"y{i}" for i in range(1, 4)] data = pd.DataFrame(y, columns=cols, index=dt_index) - return data.iloc[:, :1], data.iloc[:, 1:] + return cast(pd.DataFrame, data.iloc[:, :1]), cast( + pd.DataFrame, data.iloc[:, 1:] + ) - return y[:, :1], y[:, 1:] + return cast(Float64Array2D, y[:, :1]), cast(Float64Array2D, y[:, 1:]) diff --git a/arch/tests/univariate/test_arch_in_mean.py b/arch/tests/univariate/test_arch_in_mean.py index d160d2bf72..f72e05c3d3 100644 --- a/arch/tests/univariate/test_arch_in_mean.py +++ b/arch/tests/univariate/test_arch_in_mean.py @@ -17,10 +17,11 @@ SP500 = 100 * sp500.load()["Adj Close"].pct_change().dropna() SP500 = SP500.iloc[SP500.shape[0] // 2 :] -X = pd.concat([SP500, SP500], axis=1) -X.columns = pd.Index([0, 1]) RANDOMSTATE = np.random.RandomState(12349876) -X.loc[:, :] = RANDOMSTATE.standard_normal(X.shape) +X = pd.DataFrame( + RANDOMSTATE.standard_normal((SP500.shape[0], 2)), columns=[0, 1], index=SP500.index +) + SUPPORTED = [ HARCH, diff --git a/arch/tests/utility/test_array.py b/arch/tests/utility/test_array.py index c18aca1be9..821cb8dcda 100644 --- a/arch/tests/utility/test_array.py +++ b/arch/tests/utility/test_array.py @@ -18,6 +18,7 @@ ensure2d, find_index, parse_dataframe, + to_array_1d, ) @@ -295,3 +296,29 @@ class Dummy(metaclass=ConcreteClassMeta): @abstractmethod def func(self): pass + + +@pytest.mark.parametrize( + "arr", + [ + np.array([1.0], dtype=float), + np.array(0), + np.array([0, 1]), + np.array([[2, 3, 4]]), + Series([1, 2, 3]), + ], +) +def test_to_array_1d(arr): + converted = to_array_1d(arr) + assert isinstance(converted, np.ndarray) + assert converted.ndim == 1 + assert converted.dtype == np.float64 + + +def test_to_array_1d_err(): + with pytest.raises(ValueError, match="x must be 1D"): + to_array_1d(np.array([[1, 2], [3, 4]])) + with pytest.raises(TypeError, match="x must be a Series or ndarray"): + to_array_1d(0) + with pytest.raises(TypeError, match="x must be a Series or ndarray"): + to_array_1d(DataFrame([[0, 1]])) diff --git a/arch/typing.py b/arch/typing.py index 3d3bb1560a..98598e9bbf 100644 --- a/arch/typing.py +++ b/arch/typing.py @@ -38,17 +38,21 @@ Float64Array = np.ndarray[tuple[int, ...], np.dtype[np.float64]] # pragma: no cover Float64Array1D = np.ndarray[tuple[int], np.dtype[np.float64]] # pragma: no cover Float64Array2D = np.ndarray[tuple[int, int], np.dtype[np.float64]] # pragma: no cover -Int64Array = np.ndarray[tuple[int, ...], np.dtype[np.longlong]] # pragma: no cover -Int64Array2D = np.ndarray[tuple[int, int], np.dtype[np.longlong]] # pragma: no cover +Int64Array = np.ndarray[tuple[int, ...], np.dtype[np.int64]] # pragma: no cover +Int64Array1D = np.ndarray[tuple[int], np.dtype[np.int64]] # pragma: no cover +# tuple[list[np.ndarray[tuple[int], np.dtype[np.signedinteger[Any]]]], dict[str, np.ndarray[tuple[int], np.dtype[np.signedinteger[Any]]]]] +Int64Array2D = np.ndarray[tuple[int, int], np.dtype[np.int64]] # pragma: no cover Int32Array = np.ndarray[tuple[int, ...], np.dtype[np.intc]] # pragma: no cover -IntArray = np.ndarray[tuple[int, ...], np.dtype[np.int_]] # pragma: no cover +IntArray = np.ndarray[tuple[int, ...], np.dtype[np.int64]] # pragma: no cover BoolArray = np.ndarray[tuple[int, ...], np.dtype[np.bool_]] # pragma: no cover AnyArray = np.ndarray[tuple[int, ...], Any] # pragma: no cover AnyArray1D = np.ndarray[tuple[int], Any] # pragma: no cover Uint32Array = np.ndarray[tuple[int, ...], np.dtype[np.uintc]] # pragma: no cover BootstrapIndexT = Union[ - Int64Array, tuple[Int64Array, ...], tuple[list[Int64Array], dict[str, Int64Array]] + Int64Array1D, + tuple[Int64Array1D, ...], + tuple[list[Int64Array1D], dict[str, Int64Array1D]], ] RandomStateState = tuple[str, Uint32Array, int, int, float] diff --git a/arch/unitroot/critical_values/simulation/kpss_critical_values_simulation.py b/arch/unitroot/critical_values/simulation/kpss_critical_values_simulation.py index e3addc82bd..666c7c2e07 100644 --- a/arch/unitroot/critical_values/simulation/kpss_critical_values_simulation.py +++ b/arch/unitroot/critical_values/simulation/kpss_critical_values_simulation.py @@ -10,6 +10,7 @@ from numpy.random import RandomState import pandas as pd +from arch.typing import Float64Array from arch.utility.timeseries import add_trend @@ -30,7 +31,7 @@ def simulate_kpss( standard_normal = rng.standard_normal e = standard_normal((nobs, b)) - z = np.ones((nobs, 1)) + z: Float64Array = np.ones((nobs, 1)) if trend == "ct": z = add_trend(z, trend="t") zinv = np.linalg.pinv(z) diff --git a/arch/unitroot/unitroot.py b/arch/unitroot/unitroot.py index 0120a31d46..f336417db1 100644 --- a/arch/unitroot/unitroot.py +++ b/arch/unitroot/unitroot.py @@ -3,6 +3,7 @@ from typing import Optional, Union, cast import warnings +import numpy as np from numpy import ( abs, amax, @@ -48,6 +49,8 @@ ArrayLike1D, ArrayLike2D, Float64Array, + Float64Array1D, + Float64Array2D, Literal, UnitRootTrend, ) @@ -76,7 +79,12 @@ from arch.unitroot.critical_values.kpss import kpss_critical_values from arch.unitroot.critical_values.zivot_andrews import za_critical_values from arch.utility import cov_nw -from arch.utility.array import AbstractDocStringInheritor, ensure1d, ensure2d +from arch.utility.array import ( + AbstractDocStringInheritor, + ensure1d, + ensure2d, + to_array_1d, +) from arch.utility.exceptions import ( InfeasibleTestException, InvalidLengthWarning, @@ -339,10 +347,12 @@ def _autolag_ols( max_lags=maxlag, lag=max(exog_rank - startlag, 0) ) ) - q, r = qr(exog) - qpy = q.T @ endog - ypy = endog.T @ endog - xpx = exog.T @ exog + _exog = np.asarray(exog, dtype=float) + _endog = to_array_1d(endog) + q, r = qr(_exog) + qpy = q.T @ _endog + ypy = _endog.T @ _endog + xpx: Float64Array2D = _exog.T @ _exog sigma2 = empty(maxlag + 1) tstat = empty(maxlag + 1) @@ -422,7 +432,7 @@ def _df_select_lags( rhs = lagmat(delta_y[:, None], max_lags, trim="both", original="in") nobs = rhs.shape[0] rhs[:, 0] = y[-nobs - 1 : -1] # replace 0 with level of y - lhs = delta_y[-nobs:] + lhs = to_array_1d(delta_y[-nobs:]) if trend != "n": full_rhs = add_trend(rhs, trend, prepend=True) @@ -1340,12 +1350,14 @@ def _autolag(self) -> None: """ resids = self._resids assert resids is not None + _resids = to_array_1d(resids) covlags = int(power(self._nobs, 2.0 / 9.0)) - s0 = sum(resids**2) / self._nobs - s1 = 0 + s0 = sum(_resids**2) / self._nobs + s1 = 0.0 + nobs = float(self._nobs) for i in range(1, covlags + 1): - resids_prod = resids[i:] @ resids[: self._nobs - i] - resids_prod /= self._nobs / 2 + resids_prod = float(_resids[i:] @ _resids[: self._nobs - i]) + resids_prod /= nobs / 2 s0 += resids_prod s1 += i * resids_prod if s0 <= 0: @@ -1696,7 +1708,7 @@ def _compute_statistic(self) -> None: # Check length of y if nq % q != 0: extra = nq % q - y = y[:-extra] + y = cast(Float64Array1D, y[:-extra]) warnings.warn( invalid_length_doc.format(var="y", block=q, drop=extra), InvalidLengthWarning, diff --git a/arch/univariate/base.py b/arch/univariate/base.py index b99f8ecdde..9392d1de16 100644 --- a/arch/univariate/base.py +++ b/arch/univariate/base.py @@ -28,13 +28,14 @@ DateLike, Float64Array, Float64Array1D, + Float64Array2D, ForecastingMethod, Label, Literal, ) 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 ensure1d, to_array_1d from arch.utility.exceptions import ( ConvergenceWarning, DataScaleWarning, @@ -197,8 +198,9 @@ def __init__( self._y_series = cast(pd.Series, ensure1d(y, "y", series=True)) else: self._y_series = cast(pd.Series, ensure1d(np.empty((0,)), "y", series=True)) - self._y: Float64Array1D - self._y = np.ascontiguousarray(self._y_series) + self._y = to_array_1d( + np.ascontiguousarray(self._y_series.to_numpy()).astype(float) + ) if not np.all(np.isfinite(self._y)): raise ValueError( "NaN or inf values found in y. y must contains only finite values." @@ -214,7 +216,7 @@ def __init__( self.rescale: Optional[bool] = rescale self.scale: float = 1.0 - self._backcast: Union[float, Float64Array, None] = None + self._backcast: Union[float, Float64Array1D, None] = None self._var_bounds: Optional[Float64Array] = None if isinstance(volatility, VolatilityProcess): @@ -236,7 +238,7 @@ def name(self) -> str: """The name of the model.""" return self._name - def constraints(self) -> tuple[Float64Array, Float64Array]: + def constraints(self) -> tuple[Float64Array, Float64Array1D]: """ Construct linear constraint arrays for use in non-linear optimization @@ -298,11 +300,11 @@ def distribution(self, value: Distribution) -> None: raise ValueError("Must subclass Distribution") self._distribution = value - def _check_scale(self, resids: Float64Array) -> None: + def _check_scale(self, resids: ArrayLike1D) -> None: check = self.rescale in (None, True) if not check: return - orig_scale = scale = resids.var() + orig_scale = scale = float(np.var(resids)) rescale = 1.0 while not 0.1 <= scale < 10000.0 and scale > 0: if scale < 1.0: @@ -335,7 +337,7 @@ def _r2(self, params: ArrayLike1D) -> Optional[float]: raise NotImplementedError("Subclasses optionally may provide.") @abstractmethod - def _fit_no_arch_normal_errors_params(self) -> Float64Array: + def _fit_no_arch_normal_errors_params(self) -> Float64Array1D: """ Must be overridden with closed form estimator the return parameters ony """ @@ -349,7 +351,7 @@ def _fit_no_arch_normal_errors( """ @staticmethod - def _static_gaussian_loglikelihood(resids: Float64Array) -> float: + def _static_gaussian_loglikelihood(resids: Float64Array1D) -> float: nobs = resids.shape[0] sigma2 = resids.dot(resids) / nobs @@ -362,7 +364,7 @@ def _static_gaussian_loglikelihood(resids: Float64Array) -> float: def _fit_parameterless_model( self, cov_type: Literal["robust", "classic"], - backcast: Union[float, Float64Array], + backcast: Union[float, Float64Array1D], ) -> "ARCHModelResult": """ When models have no parameters, fill return values @@ -385,17 +387,20 @@ def _fit_parameterless_model( var_bounds = self.volatility.variance_bounds(y) vol = np.zeros(y.shape, dtype=float) self.volatility.compute_variance(params, y, vol, backcast, var_bounds) - vol = cast(Float64Array, np.sqrt(vol)) + vol = cast(Float64Array1D, np.sqrt(vol)) # Reshape resids vol - vol_final = np.full(self._y.shape, np.nan, dtype=np.double) + vol_final = np.full(self._y.shape, np.nan, dtype=float) vol_final[first_obs:last_obs] = vol names = self._all_parameter_names() r2 = self._r2(params) fit_start, fit_stop = self._fit_indices loglikelihood = -1.0 * self._loglikelihood( - params, vol**2 * np.ones(fit_stop - fit_start), backcast, var_bounds + params, + cast(Float64Array1D, vol**2 * np.ones(fit_stop - fit_start)), + backcast, + var_bounds, ) assert isinstance(r2, float) @@ -422,7 +427,7 @@ def _loglikelihood( parameters: Float64Array1D, sigma2: Float64Array1D, backcast: Union[float, Float64Array1D], - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> float: # pragma: no cover ... # pragma: no cover @@ -432,7 +437,7 @@ def _loglikelihood( parameters: Float64Array1D, sigma2: Float64Array1D, backcast: Union[float, Float64Array1D], - var_bounds: Float64Array, + var_bounds: Float64Array2D, individual: Literal[False] = ..., ) -> float: # pragma: no cover ... # pragma: no cover @@ -443,7 +448,7 @@ def _loglikelihood( parameters: Float64Array1D, sigma2: Float64Array1D, backcast: Union[float, Float64Array1D], - var_bounds: Float64Array, + var_bounds: Float64Array2D, individual: Literal[True] = ..., ) -> Float64Array1D: # pragma: no cover ... # pragma: no cover @@ -453,7 +458,7 @@ def _loglikelihood( parameters: Float64Array1D, sigma2: Float64Array1D, backcast: Union[float, Float64Array1D], - var_bounds: Float64Array, + var_bounds: Float64Array2D, individual: bool = False, ) -> Union[float, Float64Array1D]: """ @@ -476,14 +481,14 @@ def _loglikelihood( # 1. Resids mp, vp, dp = self._parse_parameters(parameters) - resids = np.asarray(self.resids(mp), dtype=float) + _resids = self.resids(mp) # 2. Compute sigma2 using VolatilityModel sigma2 = self.volatility.compute_variance( - vp, resids, sigma2, backcast, var_bounds + vp, _resids, sigma2, backcast, var_bounds ) # 3. Compute log likelihood using Distribution - llf = self.distribution.loglikelihood(dp, resids, sigma2, individual) + llf = self.distribution.loglikelihood(dp, _resids, sigma2, individual) if not individual: _callback_info["llf"] = llf_f = -float(llf) @@ -506,9 +511,13 @@ def _parse_parameters( x: Union[ArrayLike1D, Sequence[float]], ) -> tuple[Float64Array1D, Float64Array1D, Float64Array1D]: """Return the parameters of each model in a tuple""" - x = np.asarray(x, dtype=float) + _x = to_array_1d(np.asarray(x, dtype=float)) km, kv = int(self.num_params), int(self.volatility.num_params) - return x[:km], x[km : km + kv], x[km + kv :] + return ( + to_array_1d(_x[:km]), + to_array_1d(_x[km : km + kv]), + to_array_1d(_x[km + kv :]), + ) def fix( self, @@ -542,8 +551,8 @@ def fix( v = self.volatility self._adjust_sample(first_obs, last_obs) - resids = np.asarray(self.resids(self.starting_values()), dtype=float) - sigma2 = np.zeros_like(resids) + resids = self.resids(self.starting_values()) + sigma2 = np.zeros(resids.shape[0], dtype=float) backcast = v.backcast(resids) self._backcast = backcast @@ -555,10 +564,10 @@ def fix( mp, vp, dp = self._parse_parameters(params) - resids = np.asarray(self.resids(mp), dtype=float) - vol = np.zeros_like(resids) + resids = to_array_1d(self.resids(mp)) + vol = np.zeros(resids.shape[0], dtype=float) self.volatility.compute_variance(vp, resids, vol, backcast, var_bounds) - vol = np.asarray(np.sqrt(vol)) + vol = to_array_1d(np.sqrt(vol)) names = self._all_parameter_names() # Reshape resids and vol @@ -612,7 +621,7 @@ def fit( last_obs: Union[int, DateLike, None] = None, tol: Optional[float] = None, options: Optional[dict[str, Any]] = None, - backcast: Union[float, Float64Array, None] = None, + backcast: Union[float, Float64Array1D, None] = None, ) -> "ARCHModelResult": r""" Estimate model parameters @@ -677,14 +686,16 @@ def fit( self._adjust_sample(first_obs, last_obs) - resids = np.asarray(self.resids(self.starting_values()), dtype=float) + resids = self.resids(self.starting_values()) self._check_scale(resids) if self.scale != 1.0: # Scale changed, rescale data and reset model - self._y = cast(np.ndarray, self.scale * np.asarray(self._y_original)) + self._y = to_array_1d( + self.scale * ensure1d(self._y_original, "y", series=False) + ) self._scale_changed() self._adjust_sample(first_obs, last_obs) - resids = np.asarray(self.resids(self.starting_values()), dtype=float) + resids = self.resids(self.starting_values()) if backcast is None: backcast = v.backcast(resids) @@ -701,7 +712,7 @@ def fit( if total_params == 0: return self._fit_parameterless_model(cov_type=cov_type, backcast=backcast) - sigma2 = np.zeros_like(resids) + sigma2 = np.zeros(resids.shape[0], dtype=float) self._backcast = backcast sv_volatility = v.starting_values(resids) self._var_bounds = var_bounds = v.variance_bounds(resids) @@ -755,8 +766,14 @@ def fit( starting_values = None if starting_values is None: - sv = np.hstack( - [self.starting_values(), sv_volatility, d.starting_values(std_resids)] + sv = to_array_1d( + np.hstack( + [ + self.starting_values(), + sv_volatility, + d.starting_values(std_resids), + ] + ) ) # 4. Estimate models using constrained optimization @@ -814,10 +831,10 @@ def fit( mp, vp, dp = self._parse_parameters(params) - resids = np.asarray(self.resids(mp), dtype=float) - vol = np.zeros_like(resids) + resids = self.resids(mp) + vol = np.zeros(resids.shape[0], dtype=float) self.volatility.compute_variance(vp, resids, vol, backcast, var_bounds) - vol = cast(Float64Array, np.sqrt(vol)) + vol = cast(Float64Array1D, np.sqrt(vol)) try: r2 = self._r2(mp) @@ -862,7 +879,7 @@ def parameter_names(self) -> list[str]: List of variable names for the mean model """ - def starting_values(self) -> Float64Array: + def starting_values(self) -> Float64Array1D: """ Returns starting values for the mean model, often the same as the values returned from fit @@ -920,8 +937,8 @@ def resids( def compute_param_cov( self, - params: Float64Array, - backcast: Union[float, Float64Array, None] = None, + params: Float64Array1D, + backcast: Union[float, Float64Array1D, None] = None, robust: bool = True, ) -> Float64Array: """ @@ -938,7 +955,7 @@ def compute_param_cov( classic MLE (False) """ - resids = np.asarray(self.resids(self.starting_values()), dtype=float) + resids = self.resids(self.starting_values()) var_bounds = self.volatility.variance_bounds(resids) nobs = resids.shape[0] if backcast is None and self._backcast is None: @@ -948,7 +965,7 @@ def compute_param_cov( backcast = self._backcast kwargs = { - "sigma2": np.zeros_like(resids), + "sigma2": np.zeros(resids.shape[0], dtype=float), "backcast": backcast, "var_bounds": var_bounds, "individual": False, @@ -1139,9 +1156,9 @@ class ARCHModelFixedResult(_SummaryRepr): def __init__( self, - params: Float64Array, - resid: Float64Array, - volatility: Float64Array, + params: Float64Array1D, + resid: Float64Array1D, + volatility: Float64Array1D, dep_var: pd.Series, names: list[str], loglikelihood: float, @@ -1300,7 +1317,7 @@ def params(self) -> pd.Series: return pd.Series(self._params, index=self._names, name="params") @cached_property - def conditional_volatility(self) -> Union[Float64Array, pd.Series]: + def conditional_volatility(self) -> Union[Float64Array1D, pd.Series]: """ Estimated conditional volatility @@ -1325,7 +1342,7 @@ def nobs(self) -> int: return self._nobs @cached_property - def resid(self) -> Union[Float64Array, pd.Series]: + def resid(self) -> Union[Float64Array1D, pd.Series]: """ Model residuals """ @@ -1335,7 +1352,7 @@ def resid(self) -> Union[Float64Array, pd.Series]: return self._resid @cached_property - def std_resid(self) -> Union[Float64Array, pd.Series]: + def std_resid(self) -> Union[Float64Array1D, pd.Series]: """ Residuals standardized by conditional volatility """ @@ -1769,11 +1786,11 @@ class ARCHModelResult(ARCHModelFixedResult): def __init__( self, - params: Float64Array, + params: Float64Array1D, param_cov: Optional[Float64Array], r2: float, - resid: Float64Array, - volatility: Float64Array, + resid: Float64Array1D, + volatility: Float64Array1D, cov_type: str, dep_var: pd.Series, names: list[str], @@ -1967,7 +1984,7 @@ def param_cov(self) -> pd.DataFrame: if self._param_cov is not None: param_cov = self._param_cov else: - params = np.asarray(self.params) + params = to_array_1d(self.params) if self.cov_type == "robust": param_cov = self.model.compute_param_cov(params) else: diff --git a/arch/univariate/distribution.py b/arch/univariate/distribution.py index c2feab49f8..3c97f0043a 100644 --- a/arch/univariate/distribution.py +++ b/arch/univariate/distribution.py @@ -33,7 +33,7 @@ import scipy.stats as stats from arch.typing import ArrayLike, ArrayLike1D, Float64Array, Float64Array1D -from arch.utility.array import AbstractDocStringInheritor, ensure1d +from arch.utility.array import AbstractDocStringInheritor, ensure1d, to_array_1d __all__ = ["Distribution", "Normal", "StudentsT", "SkewStudent", "GeneralizedError"] @@ -185,7 +185,7 @@ def constraints(self) -> tuple[Float64Array, Float64Array]: """ @abstractmethod - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: """ Parameter bounds for use in optimization. @@ -230,7 +230,7 @@ def loglikelihood( """ @abstractmethod - def starting_values(self, std_resid: Float64Array) -> Float64Array: + def starting_values(self, std_resid: ArrayLike1D) -> Float64Array1D: """ Construct starting values for use in optimization. @@ -407,7 +407,7 @@ def __init__( def constraints(self) -> tuple[Float64Array, Float64Array]: return empty(0), empty(0) - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: return [] def loglikelihood( @@ -454,7 +454,7 @@ def loglikelihood( else: return sum(lls) - def starting_values(self, std_resid: Float64Array) -> Float64Array: + def starting_values(self, std_resid: ArrayLike1D) -> Float64Array1D: return empty(0) def _simulator(self, size: Union[int, tuple[int, ...]]) -> Float64Array: @@ -551,7 +551,7 @@ def __init__( def constraints(self) -> tuple[Float64Array, Float64Array]: return array([[1], [-1]]), array([2.05, -500.0]) - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: return [(2.05, 500.0)] def loglikelihood( @@ -605,7 +605,7 @@ def loglikelihood( else: return sum(lls) - def starting_values(self, std_resid: Float64Array) -> Float64Array: + def starting_values(self, std_resid: ArrayLike1D) -> Float64Array1D: """ Construct starting values for use in optimization. @@ -641,7 +641,9 @@ def simulate( parameters = ensure1d(parameters, "parameters", False) if parameters[0] <= 2.0: raise ValueError("The shape parameter must be larger than 2") - self._parameters = ensure1d(parameters, "parameters", series=False).astype(float) + self._parameters = ensure1d(parameters, "parameters", series=False).astype( + float + ) return self._simulator def parameter_names(self) -> list[str]: @@ -786,7 +788,7 @@ def __init__( def constraints(self) -> tuple[Float64Array, Float64Array]: return array([[1, 0], [-1, 0], [0, 1], [0, -1]]), array([2.05, -300.0, -1, -1]) - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: return [(2.05, 300.0), (-1, 1)] def loglikelihood( @@ -864,7 +866,7 @@ def loglikelihood( else: return sum(lls) - def starting_values(self, std_resid: Float64Array) -> Float64Array: + def starting_values(self, std_resid: ArrayLike1D) -> Float64Array1D: """ Construct starting values for use in optimization. @@ -909,7 +911,9 @@ def simulate( raise ValueError( "The skew parameter must be smaller than 1 in absolute value" ) - self._parameters = ensure1d(parameters, "parameters", series=False).astype(float) + self._parameters = ensure1d(parameters, "parameters", series=False).astype( + float + ) return self._simulator def parameter_names(self) -> list[str]: @@ -1141,7 +1145,7 @@ def __init__( def constraints(self) -> tuple[Float64Array, Float64Array]: return array([[1], [-1]]), array([1.01, -500.0]) - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: return [(1.01, 500.0)] def loglikelihood( @@ -1201,7 +1205,7 @@ def loglikelihood( else: return sum(lls) - def starting_values(self, std_resid: Float64Array) -> Float64Array: + def starting_values(self, std_resid: ArrayLike1D) -> Float64Array1D: """ Construct starting values for use in optimization. @@ -1220,7 +1224,7 @@ def starting_values(self, std_resid: Float64Array) -> Float64Array: ----- Defaults to 1.5 which is implies heavier tails than a normal """ - return array([1.5]) + return to_array_1d(array([1.5])) def _simulator(self, size: Union[int, tuple[int, ...]]) -> Float64Array: assert self._parameters is not None @@ -1242,7 +1246,9 @@ def simulate( parameters = ensure1d(parameters, "parameters", False) if parameters[0] <= 1.0: raise ValueError("The shape parameter must be larger than 1") - self._parameters = ensure1d(parameters, "parameters", series=False).astype(float) + self._parameters = ensure1d(parameters, "parameters", series=False).astype( + float + ) return self._simulator def parameter_names(self) -> list[str]: diff --git a/arch/univariate/mean.py b/arch/univariate/mean.py index 063aa77ecb..22d8625a05 100644 --- a/arch/univariate/mean.py +++ b/arch/univariate/mean.py @@ -41,6 +41,7 @@ SkewStudent, StudentsT, ) +from arch.utility.array import to_array_1d if TYPE_CHECKING: # Fake path to satisfy mypy @@ -130,7 +131,7 @@ def _ar_forecast( fcasts[:, i] = constant + fcasts[:, i - p : i].dot(arp_rev) if x.shape[0] > 0: fcasts[:, i] += x[:, :, i - p].T @ exogp - fcasts = fcasts[:, p:] + fcasts = cast(Float64Array2D, fcasts[:, p:]) return fcasts @@ -267,7 +268,8 @@ def __init__( distribution=distribution, rescale=rescale, ) - self._x = x + self._x: Optional[Union[pd.DataFrame, Float64Array2D]] = None + self._x_original = x self._x_names: list[str] = [] self._x_index: Union[NDArray, pd.Index, None] = None self.lags: Union[ @@ -276,10 +278,10 @@ def __init__( self._lags: Int64Array2D = np.empty((0, 0), dtype=int) self.constant: bool = constant self.use_rotated: bool = use_rotated - self.regressors: Float64Array = np.empty((0, 0), dtype=np.double) + self.regressors: Float64Array2D = np.empty((0, 0), dtype=np.double) self._name = "HAR" - if self._x is not None: + if self._x_original is not None: self._name += "-X" if lags is not None: max_lags = int(np.max(np.asarray(lags, dtype=np.int32))) @@ -366,10 +368,10 @@ def resids( y: Optional[ArrayLike1D] = None, regressors: Optional[ArrayLike2D] = None, ) -> ArrayLike1D: - regressors = self._fit_regressors if y is None else regressors + _regressors = self._fit_regressors if y is None else regressors y = self._fit_y if y is None else y - assert regressors is not None - return y - np.asarray(regressors, dtype=float).dot(params) + assert _regressors is not None + return y - np.asarray(_regressors, dtype=float).dot(params) @cached_property def num_params(self) -> int: @@ -500,7 +502,7 @@ def simulate( vc = self.volatility.num_params dc = self.distribution.num_params num_params = mc + vc + dc - params = cast(Float64Array, ensure1d(params, "params", series=False)) + params = cast(Float64Array1D, ensure1d(params, "params", series=False)) if params.shape[0] != num_params: raise ValueError( "params has the wrong number of elements. " @@ -508,8 +510,8 @@ def simulate( ) dist_params = np.empty(0) if dc == 0 else params[-dc:] - vol_params = params[mc : mc + vc] - simulator = self.distribution.simulate(dist_params) + vol_params = cast(Float64Array1D, params[mc : mc + vc]) + simulator = self.distribution.simulate(cast(Float64Array1D, dist_params)) sim_data = self.volatility.simulate( vol_params, nobs + burn, simulator, burn, initial_value_vol ) @@ -546,22 +548,26 @@ def _generate_lag_names(self) -> list[str]: def _check_specification(self) -> None: """Checks the specification for obvious errors""" - if self._x is not None: - if isinstance(self._x, pd.Series): - self._x = pd.DataFrame(self._x) - elif self._x.ndim == 1: - assert isinstance(self._x, np.ndarray) - self._x = np.asarray(self._x)[:, None] + err_msg = ( + "x must be nobs by n, where nobs is the same as the number of elements in y" + ) + if self._x_original is not None: + if isinstance(self._x_original, pd.Series): + self._x = pd.DataFrame(self._x_original) + elif isinstance(self._x_original, pd.DataFrame): + self._x = self._x_original + else: + x_original = np.asarray(self._x_original, dtype=float) + if x_original.ndim == 1: + x_original = x_original[:, None] + self._x = cast(Float64Array2D, x_original) assert isinstance(self._x, (np.ndarray, pd.DataFrame)) if self._x.ndim != 2 or self._x.shape[0] != self._y.shape[0]: - raise ValueError( - "x must be nobs by n, where nobs is the same as " - "the number of elements in y" - ) + raise ValueError(err_msg) def_names = ["x" + str(i) for i in range(self._x.shape[1])] names, self._x_index = parse_dataframe(self._x, def_names) self._x_names = [str(name) for name in names] - self._x = np.asarray(self._x) + self._x = cast(Float64Array2D, np.asarray(self._x, dtype=float)) def _reformat_lags(self) -> None: """ @@ -584,7 +590,7 @@ def _reformat_lags(self) -> None: "When using the 1-d format of lags, values must be positive" ) lags = np.unique(lags) - temp = np.array([lags, lags], dtype=int) + temp = cast(Int64Array2D, np.array([lags, lags], dtype=int)) if self.use_rotated: temp[0, 1:] = temp[0, 0:-1] temp[0, 0] = 0 @@ -600,7 +606,7 @@ def _reformat_lags(self) -> None: "lags[0,j] <= lags[1,j] for all lags values." ) ind = np.lexsort(np.flipud(lags)) - lags = lags[:, ind] + lags = cast(Int64Array2D, lags[:, ind]) test_mat = np.zeros((lags.shape[1], np.max(lags)), dtype=int) # Subtract 1 so first is 0 indexed lags = lags - np.array([[1], [0]]) @@ -609,8 +615,8 @@ def _reformat_lags(self) -> None: rank = np.linalg.matrix_rank(test_mat.astype(float)) if rank != lags.shape[1]: raise ValueError("lags contains redundant entries") + self._lags = cast(Int64Array2D, lags) - self._lags = lags if self.use_rotated: from warnings import warn @@ -655,7 +661,9 @@ def _init_model(self) -> None: else: reg_x = np.empty((nobs_orig, 0), dtype=np.double) - self.regressors = np.hstack((reg_constant, reg_lags, reg_x)) + self.regressors = cast( + Float64Array2D, np.hstack((reg_constant, reg_lags, reg_x)) + ) def _r2(self, params: ArrayLike1D) -> float: y = self._fit_y @@ -671,7 +679,7 @@ def _r2(self, params: ArrayLike1D) -> float: tss = float(y.dot(y)) if tss <= 0.0: return np.nan - e = np.asarray(self.resids(np.asarray(params, dtype=float)), dtype=float) + e = to_array_1d(self.resids(to_array_1d(params))) return 1.0 - float(e.T.dot(e)) / tss @@ -687,12 +695,12 @@ def _adjust_sample( if _last_obs_index <= _first_obs_index: raise ValueError("first_obs and last_obs produce in an empty array.") self._fit_indices = [_first_obs_index, _last_obs_index] - self._fit_y = self._y[_first_obs_index:_last_obs_index] + self._fit_y = cast(Float64Array1D, self._y[_first_obs_index:_last_obs_index]) reg = self.regressors self._fit_regressors = reg[_first_obs_index:_last_obs_index] self.volatility.start, self.volatility.stop = self._fit_indices - def _fit_no_arch_normal_errors_params(self) -> Float64Array: + def _fit_no_arch_normal_errors_params(self) -> Float64Array1D: """ Estimates model parameters excluding sigma2 @@ -759,7 +767,9 @@ def _fit_no_arch_normal_errors( opt = OptimizeResult({"status": 0, "message": ""}) if x.shape[1] > 0: - regression_params = np.linalg.pinv(x).dot(y) + regression_params: Float64Array1D = cast( + Float64Array1D, np.linalg.pinv(x).dot(y) + ) xpxi = np.linalg.inv(x.T.dot(x) / nobs) fitted = x.dot(regression_params) else: @@ -770,7 +780,7 @@ def _fit_no_arch_normal_errors( e = y - fitted sigma2 = e.T.dot(e) / nobs - params = np.hstack((regression_params, sigma2)) + params = to_array_1d(np.hstack((regression_params, sigma2))) hessian = np.zeros((self.num_params + 1, self.num_params + 1)) hessian[: self.num_params, : self.num_params] = -xpxi hessian[-1, -1] = -1 @@ -874,7 +884,7 @@ def _reformat_forecast_x( f"the included exogenous regressors. {key} not found in: " f"{keys}" ) - temp = np.asarray(x[key], dtype=np.double) + temp = np.asarray(x[key], dtype=float) if temp.ndim == 1: temp = temp.reshape((1, -1)) collected.append(temp) @@ -956,7 +966,7 @@ def forecast( f"this value is {max(0, earliest - 1)}." ) # Parse params - params = np.asarray(params) + params = to_array_1d(params) mp, vp, dp = self._parse_parameters(params) ##################################### @@ -964,9 +974,13 @@ def forecast( ##################################### # Back cast should use only the sample used in fitting resids = self.resids(mp) - backcast = self._volatility.backcast(np.asarray(resids, dtype=float)) - full_resids = np.asarray( - self.resids(mp, self._y[earliest:], self.regressors[earliest:]), dtype=float + backcast = self._volatility.backcast(resids) + full_resids = to_array_1d( + self.resids( + mp, + cast(Float64Array1D, self._y[earliest:]), + cast(Float64Array2D, self.regressors[earliest:]), + ) ) vb = self._volatility.variance_bounds(full_resids, 2.0) if rng is None: @@ -1204,8 +1218,8 @@ def resids( y: Optional[ArrayLike1D] = None, regressors: Optional[ArrayLike2D] = None, ) -> ArrayLike1D: - _y = self._fit_y if y is None else np.asarray(y, dtype=np.double) - return _y - params + _y = self._fit_y if y is None else to_array_1d(ensure1d(y, "y", series=False)) + return _y - params[0] class ZeroMean(HARX): @@ -1459,7 +1473,7 @@ def __init__( rescale=rescale, ) self._name = "AR" - if self._x is not None: + if self._x_original is not None: self._name += "-X" def _model_description(self, include_lags: bool = True) -> dict[str, str]: @@ -1724,9 +1738,11 @@ def resids( y: Optional[ArrayLike1D] = None, regressors: Optional[ArrayLike2D] = None, ) -> ArrayLike1D: - return super().resids(params[:-1], y=y, regressors=regressors) + return super().resids( + cast(Float64Array1D, params[:-1]), y=y, regressors=regressors + ) - def starting_values(self) -> Float64Array: + def starting_values(self) -> Float64Array1D: return np.r_[super().starting_values(), 0.0] @overload @@ -1735,7 +1751,7 @@ def _loglikelihood( parameters: Float64Array1D, sigma2: Float64Array1D, backcast: Union[float, Float64Array1D], - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> float: # pragma: no cover ... # pragma: no cover @@ -1745,7 +1761,7 @@ def _loglikelihood( parameters: Float64Array1D, sigma2: Float64Array1D, backcast: Union[float, Float64Array1D], - var_bounds: Float64Array, + var_bounds: Float64Array2D, individual: Literal[False] = ..., ) -> float: # pragma: no cover ... # pragma: no cover @@ -1756,7 +1772,7 @@ def _loglikelihood( parameters: Float64Array1D, sigma2: Float64Array1D, backcast: Union[float, Float64Array1D], - var_bounds: Float64Array, + var_bounds: Float64Array2D, individual: Literal[True] = ..., ) -> Float64Array1D: # pragma: no cover ... # pragma: no cover @@ -1766,7 +1782,7 @@ def _loglikelihood( parameters: Float64Array1D, sigma2: Float64Array1D, backcast: Union[float, Float64Array1D], - var_bounds: Float64Array, + var_bounds: Float64Array2D, individual: bool = False, ) -> Union[float, Float64Array1D]: # Parse parameters diff --git a/arch/univariate/recursions.pyi b/arch/univariate/recursions.pyi index 63bdeb5a49..4ab0fb0231 100644 --- a/arch/univariate/recursions.pyi +++ b/arch/univariate/recursions.pyi @@ -1,103 +1,103 @@ from typing import Optional, Union -from arch.typing import Float64Array, Int32Array +from arch.typing import Float64Array, Float64Array1D, Float64Array2D, Int32Array def harch_recursion( - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, lags: Int32Array, nobs: int, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> Float64Array: ... def arch_recursion( - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, p: int, nobs: int, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> Float64Array: ... def garch_recursion( - parameters: Float64Array, - fresids: Float64Array, - sresids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + fresids: Float64Array1D, + sresids: Float64Array1D, + sigma2: Float64Array1D, p: int, o: int, q: int, nobs: int, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> Float64Array: ... def egarch_recursion( - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, p: int, o: int, q: int, nobs: int, backcast: float, - var_bounds: Float64Array, - lnsigma2: Float64Array, - std_resids: Float64Array, - abs_std_resids: Float64Array, + var_bounds: Float64Array2D, + lnsigma2: Float64Array1D, + std_resids: Float64Array1D, + abs_std_resids: Float64Array1D, ) -> Float64Array: ... def midas_recursion( - parameters: Float64Array, - weights: Float64Array, - resids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + weights: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, nobs: int, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> Float64Array: ... def figarch_weights( - parameters: Float64Array, p: int, q: int, trunc_lag: int + parameters: Float64Array1D, p: int, q: int, trunc_lag: int ) -> Float64Array: ... def figarch_recursion( - parameters: Float64Array, - fresids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + fresids: Float64Array1D, + sigma2: Float64Array1D, p: int, q: int, nobs: int, trunc_lag: int, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> Float64Array: ... def aparch_recursion( - parameters: Float64Array, - resids: Float64Array, - abs_resids: Float64Array, - sigma2: Float64Array, - sigma_delta: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + abs_resids: Float64Array1D, + sigma2: Float64Array1D, + sigma_delta: Float64Array1D, p: int, o: int, q: int, nobs: int, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> Float64Array: ... def harch_core( t: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, lags: Int32Array, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> Float64Array: ... def garch_core( t: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, p: int, o: int, q: int, @@ -106,15 +106,18 @@ def garch_core( class VolatilityUpdater: def initialize_update( - self, parameters: Float64Array, backcast: Union[float, Float64Array], nobs: int + self, + parameters: Float64Array1D, + backcast: Union[float, Float64Array1D], + nobs: int, ) -> None: ... def _update_tester( self, t: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, + var_bounds: Float64Array2D, ) -> None: ... class GARCHUpdater(VolatilityUpdater): @@ -136,19 +139,19 @@ class RiskMetrics2006Updater(VolatilityUpdater): def __init__( self, kmax: int, - combination_weights: Float64Array, - smoothing_parameters: Float64Array, + combination_weights: Float64Array1D, + smoothing_parameters: Float64Array1D, ) -> None: ... class ARCHInMeanRecursion: def __init__(self, updater: VolatilityUpdater) -> None: ... def recursion( self, - y: Float64Array, - x: Float64Array, - mean_parameters: Float64Array, - variance_params: Float64Array, - sigma2: Float64Array, - var_bounds: Float64Array, + y: Float64Array1D, + x: Float64Array2D, + mean_parameters: Float64Array1D, + variance_params: Float64Array1D, + sigma2: Float64Array1D, + var_bounds: Float64Array2D, power: float, ) -> Float64Array: ... diff --git a/arch/univariate/recursions_python.py b/arch/univariate/recursions_python.py index de02e4c478..25bc38c2cd 100644 --- a/arch/univariate/recursions_python.py +++ b/arch/univariate/recursions_python.py @@ -13,7 +13,7 @@ import numpy as np from scipy.special import gammaln -from arch.typing import Float64Array, Int32Array +from arch.typing import Float64Array, Float64Array1D, Float64Array2D, Int32Array from arch.utility.array import AbstractDocStringInheritor __all__ = [ @@ -40,7 +40,7 @@ SQRT2_OV_PI = 0.79788456080286541 # E[abs(e)], e~N(0,1) -def bounds_check_python(sigma2: float, var_bounds: Float64Array) -> float: +def bounds_check_python(sigma2: float, var_bounds: Float64Array2D) -> float: if sigma2 < var_bounds[0]: sigma2 = var_bounds[0] elif sigma2 > var_bounds[1]: @@ -56,12 +56,12 @@ def bounds_check_python(sigma2: float, var_bounds: Float64Array) -> float: def harch_core_python( t: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, lags: Int32Array, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> float: sigma2[t] = parameters[0] for i in range(lags.shape[0]): @@ -80,13 +80,13 @@ def harch_core_python( def harch_recursion_python( - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, lags: Int32Array, nobs: int, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> Float64Array: """ Parameters @@ -127,13 +127,13 @@ def harch_recursion_python( def arch_recursion_python( - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, p: int, nobs: int, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> Float64Array: """ Parameters @@ -172,11 +172,11 @@ def arch_recursion_python( def garch_core_python( t: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, p: int, o: int, q: int, @@ -244,16 +244,16 @@ def garch_core_python( def garch_recursion_python( - parameters: Float64Array, - fresids: Float64Array, - sresids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + fresids: Float64Array1D, + sresids: Float64Array1D, + sigma2: Float64Array1D, p: int, o: int, q: int, nobs: int, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> Float64Array: """ Compute variance recursion for GARCH and related models @@ -317,18 +317,18 @@ def garch_recursion_python( def egarch_recursion_python( - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, p: int, o: int, q: int, nobs: int, backcast: float, - var_bounds: Float64Array, - lnsigma2: Float64Array, - std_resids: Float64Array, - abs_std_resids: Float64Array, + var_bounds: Float64Array2D, + lnsigma2: Float64Array1D, + std_resids: Float64Array1D, + abs_std_resids: Float64Array1D, ) -> Float64Array: """ Compute variance recursion for EGARCH models @@ -401,13 +401,13 @@ def egarch_recursion_python( def midas_recursion_python( - parameters: Float64Array, + parameters: Float64Array1D, weights: Float64Array, - resids: Float64Array, - sigma2: Float64Array, + resids: Float64Array1D, + sigma2: Float64Array1D, nobs: int, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> Float64Array: """ Parameters @@ -466,7 +466,7 @@ def midas_recursion_python( def figarch_weights_python( - parameters: Float64Array, p: int, q: int, trunc_lag: int + parameters: Float64Array1D, p: int, q: int, trunc_lag: int ) -> Float64Array: r""" Parameters @@ -507,15 +507,15 @@ def figarch_weights_python( def figarch_recursion_python( - parameters: Float64Array, - fresids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + fresids: Float64Array1D, + sigma2: Float64Array1D, p: int, q: int, nobs: int, trunc_lag: int, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> Float64Array: """ Parameters @@ -569,17 +569,17 @@ def figarch_recursion_python( def aparch_recursion_python( - parameters: Float64Array, - resids: Float64Array, - abs_resids: Float64Array, - sigma2: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + abs_resids: Float64Array1D, + sigma2: Float64Array1D, sigma_delta: Float64Array, p: int, o: int, q: int, nobs: int, backcast: float, - var_bounds: Float64Array, + var_bounds: Float64Array2D, ) -> Float64Array: """ Compute variance recursion for GARCH and related models @@ -659,7 +659,10 @@ def __init__(self) -> None: @abstractmethod def initialize_update( - self, parameters: Float64Array, backcast: Union[float, Float64Array], nobs: int + self, + parameters: Float64Array1D, + backcast: Union[float, Float64Array1D], + nobs: int, ) -> None: """ Initialize the recursion prior to calling update @@ -685,10 +688,10 @@ def initialize_update( def update( self, t: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, + var_bounds: Float64Array2D, ) -> None: """ Update the current variance at location t @@ -717,10 +720,10 @@ def update( def _update_tester( self, t: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, + var_bounds: Float64Array2D, ) -> None: """ Testing shim for update with compatibility with the Cythonized version @@ -753,17 +756,20 @@ def __init__(self, p: int, o: int, q: int, power: float) -> None: self.backcast = -1.0 def initialize_update( - self, parameters: Float64Array, backcast: Union[float, Float64Array], nobs: int + self, + parameters: Float64Array1D, + backcast: Union[float, Float64Array1D], + nobs: int, ) -> None: self.backcast = cast(float, backcast) def update( self, t: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, + var_bounds: Float64Array2D, ) -> None: loc = 0 sigma2[t] = parameters[loc] @@ -800,17 +806,20 @@ def __init__(self, lags: Int32Array) -> None: self.backcast = -1.0 def initialize_update( - self, parameters: Float64Array, backcast: Union[float, Float64Array], nobs: int + self, + parameters: Float64Array1D, + backcast: Union[float, Float64Array1D], + nobs: int, ) -> None: self.backcast = cast(float, backcast) def update( self, t: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, + var_bounds: Float64Array2D, ) -> None: backcast = self.backcast @@ -836,7 +845,10 @@ def __init__(self, lam: Optional[float]) -> None: self.params[2] = lam def initialize_update( - self, parameters: Float64Array, backcast: Union[float, Float64Array], nobs: int + self, + parameters: Float64Array1D, + backcast: Union[float, Float64Array1D], + nobs: int, ) -> None: if self.estimate_lam: self.params[1] = 1.0 - parameters[0] @@ -846,10 +858,10 @@ def initialize_update( def update( self, t: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, + var_bounds: Float64Array2D, ) -> None: sigma2[t] = self.params[0] if t == 0: @@ -890,7 +902,10 @@ def update_weights(self, theta: float) -> None: self.weights[i] /= sum_w def initialize_update( - self, parameters: Float64Array, backcast: Union[float, Float64Array], nobs: int + self, + parameters: Float64Array1D, + backcast: Union[float, Float64Array1D], + nobs: int, ) -> None: self.update_weights(parameters[2 + self.asym]) alpha = parameters[1] @@ -909,10 +924,10 @@ def initialize_update( def update( self, t: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, + var_bounds: Float64Array2D, ) -> None: omega = parameters[0] if t > 0: @@ -941,7 +956,10 @@ def __init__(self, p: int, q: int, power: float, truncation: int) -> None: self.fresids = np.empty(0) def initialize_update( - self, parameters: Float64Array, backcast: Union[float, Float64Array], nobs: int + self, + parameters: Float64Array1D, + backcast: Union[float, Float64Array1D], + nobs: int, ) -> None: self.lam = figarch_weights(parameters[1:], self.p, self.q, self.truncation) self.backcast = backcast @@ -951,10 +969,10 @@ def initialize_update( def update( self, t: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, + var_bounds: Float64Array2D, ) -> None: p = self.p q = self.q @@ -981,27 +999,30 @@ def __init__( self, kmax: int, combination_weights: Float64Array, - smoothing_parameters: Float64Array, + smoothing_parameters: Float64Array1D, ) -> None: super().__init__() self.kmax = kmax self.combination_weights = combination_weights self.smoothing_parameters = smoothing_parameters - self.backcast: Float64Array = np.empty(kmax) + self.backcast: Float64Array1D = np.empty(kmax) self.last_sigma2s: Float64Array = np.empty((1, kmax)) def initialize_update( - self, parameters: Float64Array, backcast: Union[float, Float64Array], nobs: int + self, + parameters: Float64Array1D, + backcast: Union[float, Float64Array1D], + nobs: int, ) -> None: - self.backcast = cast(Float64Array, backcast) + self.backcast = cast(Float64Array1D, backcast) def update( self, t: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, + var_bounds: Float64Array2D, ) -> None: w = self.combination_weights mus = self.smoothing_parameters @@ -1031,7 +1052,10 @@ def _resize(self, nobs: int) -> None: self.std_resids = np.empty(nobs) def initialize_update( - self, parameters: Float64Array, backcast: Union[float, Float64Array], nobs: int + self, + parameters: Float64Array1D, + backcast: Union[float, Float64Array1D], + nobs: int, ) -> None: self.backcast = cast(float, backcast) self._resize(nobs) @@ -1039,10 +1063,10 @@ def initialize_update( def update( self, t: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, + var_bounds: Float64Array2D, ) -> None: if t > 0: self.std_resids[t - 1] = resids[t - 1] / np.sqrt(sigma2[t - 1]) @@ -1087,10 +1111,10 @@ def recursion( self, y: Float64Array, x: Float64Array, - mean_parameters: Float64Array, - variance_params: Float64Array, - sigma2: Float64Array, - var_bounds: Float64Array, + mean_parameters: Float64Array1D, + variance_params: Float64Array1D, + sigma2: Float64Array1D, + var_bounds: Float64Array2D, power: float, ) -> Float64Array: nobs = y.shape[0] diff --git a/arch/univariate/volatility.py b/arch/univariate/volatility.py index a61af654c9..32e1e00b17 100644 --- a/arch/univariate/volatility.py +++ b/arch/univariate/volatility.py @@ -18,12 +18,13 @@ ArrayLike1D, Float64Array, Float64Array1D, + Float64Array2D, ForecastingMethod, Int32Array, RNGType, ) from arch.univariate.distribution import Normal -from arch.utility.array import AbstractDocStringInheritor, ensure1d +from arch.utility.array import AbstractDocStringInheritor, ensure1d, to_array_1d from arch.utility.exceptions import ( InitialValueWarning, ValueWarning, @@ -79,7 +80,7 @@ class BootstrapRng: def __init__( self, - std_resid: Float64Array, + std_resid: Float64Array1D, start: int, random_state: Optional[RandomState] = None, ) -> None: @@ -114,8 +115,12 @@ def _rng(size: Union[int, tuple[int, ...]]) -> Float64Array: def ewma_recursion( - lam: float, resids: Float64Array, sigma2: Float64Array, nobs: int, backcast: float -) -> Float64Array: + lam: float, + resids: Float64Array1D, + sigma2: Float64Array1D, + nobs: int, + backcast: float, +) -> Float64Array1D: """ Compute variance recursion for EWMA/RiskMetrics Variance @@ -275,11 +280,11 @@ def volatility_updater(self) -> rec.VolatilityUpdater: def update( self, index: int, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + sigma2: Float64Array1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, ) -> float: """ Compute the variance for a single observation @@ -331,13 +336,13 @@ def _check_forecasting_method( def _one_step_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: Float64Array1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, horizon: int, start_index: int, - ) -> tuple[Float64Array, Float64Array]: + ) -> tuple[Float64Array1D, Float64Array]: """ One-step ahead forecast @@ -364,8 +369,8 @@ def _one_step_forecast( location """ t = resids.shape[0] - _resids: Float64Array = np.concatenate((resids, np.array([0.0]))) - _var_bounds: Float64Array = np.concatenate( + _resids: Float64Array1D = to_array_1d(np.concatenate((resids, np.array([0.0])))) + _var_bounds: Float64Array2D = np.concatenate( (var_bounds, np.array([[0, np.inf]])) ) sigma2: Float64Array1D @@ -373,17 +378,17 @@ def _one_step_forecast( self.compute_variance(parameters, _resids, sigma2, backcast, _var_bounds) forecasts = np.zeros((t - start_index, horizon)) forecasts[:, 0] = sigma2[start_index + 1 :] - sigma2 = sigma2[:-1] + sigma2 = cast(Float64Array1D, sigma2[:-1]) return sigma2, forecasts @abstractmethod def _analytic_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, ) -> VarianceForecast: @@ -417,10 +422,10 @@ def _analytic_forecast( @abstractmethod def _simulation_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, simulations: int, @@ -465,10 +470,10 @@ def _simulation_forecast( def _bootstrap_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, simulations: int, @@ -505,9 +510,9 @@ def _bootstrap_forecast( Class containing the variance forecasts, and, if using simulation or bootstrap, the simulated paths. """ - sigma2 = np.empty_like(resids) + sigma2 = np.empty(resids.shape[0], dtype=float) self.compute_variance(parameters, resids, sigma2, backcast, var_bounds) - std_resid = resids / np.sqrt(sigma2) + std_resid = to_array_1d(resids / np.sqrt(sigma2)) if start < self._min_bootstrap_obs: raise ValueError( f"start must include more than {self._min_bootstrap_obs} observations" @@ -517,7 +522,9 @@ def _bootstrap_forecast( parameters, resids, backcast, var_bounds, start, horizon, simulations, rng ) - def variance_bounds(self, resids: Float64Array, power: float = 2.0) -> Float64Array: + def variance_bounds( + self, resids: ArrayLike1D, power: float = 2.0 + ) -> Float64Array2D: """ Construct loose bounds for conditional variances. @@ -546,12 +553,14 @@ def variance_bounds(self, resids: Float64Array, power: float = 2.0) -> Float64Ar w = w / sum(w) var_bound = np.zeros(nobs) initial_value = w.dot(resids[:tau] ** 2.0) - ewma_recursion(0.94, resids, var_bound, resids.shape[0], initial_value) + ewma_recursion( + 0.94, to_array_1d(resids), var_bound, resids.shape[0], initial_value + ) var_bounds = np.vstack((var_bound / 1e6, var_bound * 1e6)).T - var = resids.var() - min_upper_bound = 1 + (resids**2.0).max() - lower_bound, upper_bound = var / 1e8, 1e7 * (1 + (resids**2.0).max()) + var = float(np.var(resids)) + min_upper_bound = 1 + float(np.max(resids**2.0)) + lower_bound, upper_bound = var / 1e8, 1e7 * (1 + float(np.max(resids**2.0))) var_bounds[var_bounds[:, 0] < lower_bound, 0] = lower_bound var_bounds[var_bounds[:, 1] < min_upper_bound, 1] = min_upper_bound var_bounds[var_bounds[:, 1] > upper_bound, 1] = upper_bound @@ -559,10 +568,10 @@ def variance_bounds(self, resids: Float64Array, power: float = 2.0) -> Float64Ar if power != 2.0: var_bounds **= power / 2.0 - return np.ascontiguousarray(var_bounds) + return cast(Float64Array2D, np.ascontiguousarray(var_bounds)) @abstractmethod - def starting_values(self, resids: Float64Array) -> Float64Array: + def starting_values(self, resids: ArrayLike1D) -> Float64Array1D: """ Returns starting values for the ARCH model @@ -578,7 +587,7 @@ def starting_values(self, resids: Float64Array) -> Float64Array: Array of starting values """ - def backcast(self, resids: Float64Array) -> Union[float, Float64Array]: + def backcast(self, resids: ArrayLike1D) -> Union[float, Float64Array1D]: """ Construct values for backcasting to start the recursion @@ -599,8 +608,8 @@ def backcast(self, resids: Float64Array) -> Union[float, Float64Array]: return float(np.sum((resids[:tau] ** 2.0) * w)) def backcast_transform( - self, backcast: Union[float, Float64Array] - ) -> Union[float, Float64Array]: + self, backcast: Union[float, Float64Array1D] + ) -> Union[float, Float64Array1D]: """ Transformation to apply to user-provided backcast values @@ -619,7 +628,7 @@ def backcast_transform( return backcast @abstractmethod - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: """ Returns bounds for parameters @@ -637,12 +646,12 @@ def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: @abstractmethod def compute_variance( self, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, - ) -> Float64Array: + parameters: Float64Array1D, + resids: ArrayLike1D, + sigma2: Float64Array1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, + ) -> Float64Array1D: """ Compute the variance for the ARCH model @@ -683,9 +692,9 @@ def constraints(self) -> tuple[Float64Array, Float64Array]: def forecast( self, parameters: ArrayLike1D, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: Optional[int] = None, horizon: int = 1, method: ForecastingMethod = "analytic", @@ -742,7 +751,7 @@ def forecast( The analytic ``method`` is not supported for all models. Attempting to use this method when not available will raise a ValueError. """ - _parameters = np.asarray(parameters) + _parameters = to_array_1d(parameters) method_name = method.lower() if method_name not in ("analytic", "simulation", "bootstrap"): raise ValueError(f"{method} is not a known forecasting method") @@ -825,17 +834,17 @@ def simulate( def _gaussian_loglikelihood( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, ) -> float: """ Private implementation of a Gaussian log-likelihood for use in constructing starting values or other quantities that do not depend on the distribution used by the model. """ - sigma2 = np.zeros_like(resids) + sigma2 = np.zeros(resids.shape[0], dtype=float) self.compute_variance(parameters, resids, sigma2, backcast, var_bounds) return float(self._normal.loglikelihood([], resids, sigma2)) @@ -868,17 +877,17 @@ def __init__(self) -> None: def compute_variance( self, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, - ) -> Float64Array: + parameters: Float64Array1D, + resids: ArrayLike1D, + sigma2: Float64Array1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, + ) -> Float64Array1D: sigma2[:] = parameters[0] return sigma2 - def starting_values(self, resids: Float64Array) -> Float64Array: - return np.array([resids.var()]) + def starting_values(self, resids: ArrayLike1D) -> Float64Array1D: + return to_array_1d(np.array([np.var(resids)])) def simulate( self, @@ -898,16 +907,16 @@ def constraints(self) -> tuple[Float64Array, Float64Array]: return np.ones((1, 1)), np.zeros(1) def backcast_transform( - self, backcast: Union[float, Float64Array] - ) -> Union[float, Float64Array]: + self, backcast: Union[float, Float64Array1D] + ) -> Union[float, Float64Array1D]: backcast = super().backcast_transform(backcast) return backcast - def backcast(self, resids: Float64Array) -> Union[float, Float64Array]: - return float(resids.var()) + def backcast(self, resids: ArrayLike1D) -> Union[float, Float64Array1D]: + return float(np.var(resids)) - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: - v = float(resids.var()) + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: + v = float(np.var(resids)) return [(v / 100000.0, 10.0 * (v + float(resids.mean()) ** 2.0))] def parameter_names(self) -> list[str]: @@ -920,10 +929,10 @@ def _check_forecasting_method( def _analytic_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, ) -> VarianceForecast: @@ -935,10 +944,10 @@ def _analytic_forecast( def _simulation_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, simulations: int, @@ -1048,7 +1057,9 @@ def __str__(self) -> str: descr = descr[:-2] + ")" return descr - def variance_bounds(self, resids: Float64Array, power: float = 2.0) -> Float64Array: + def variance_bounds( + self, resids: ArrayLike1D, power: float = 2.0 + ) -> Float64Array2D: return super().variance_bounds(resids, self.power) def _generate_name(self) -> str: @@ -1075,8 +1086,8 @@ def _generate_name(self) -> str: else: return f"Asym. Power GARCH (power: {self.power:0.1f})" - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: - v = float(np.mean(abs(resids) ** self.power)) + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: + v = float(np.mean(np.absolute(resids) ** self.power)) bounds = [(1e-8 * v, 10.0 * float(v))] bounds.extend([(0.0, 1.0)] * self.p) @@ -1112,16 +1123,16 @@ def constraints(self) -> tuple[Float64Array, Float64Array]: def compute_variance( self, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, - ) -> Float64Array: + parameters: Float64Array1D, + resids: ArrayLike1D, + sigma2: Float64Array1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, + ) -> Float64Array1D: # fresids is abs(resids) ** power # sresids is I(resids<0) power = self.power - fresids = np.abs(resids) ** power + fresids = np.absolute(resids) ** power sresids = np.sign(resids) p, o, q = self.p, self.o, self.q @@ -1136,17 +1147,21 @@ def compute_variance( return sigma2 def backcast_transform( - self, backcast: Union[float, Float64Array] - ) -> Union[float, Float64Array]: + self, backcast: Union[float, Float64Array1D] + ) -> Union[float, Float64Array1D]: backcast = super().backcast_transform(backcast) - return np.sqrt(backcast) ** self.power + _backcast = np.sqrt(backcast) ** self.power + if np.isscalar(_backcast): + return float(cast(np.float64, _backcast)) + else: + return to_array_1d(_backcast) - def backcast(self, resids: Float64Array) -> Union[float, Float64Array]: + def backcast(self, resids: ArrayLike1D) -> Union[float, Float64Array1D]: power = self.power tau = min(75, resids.shape[0]) w = 0.94 ** np.arange(tau) w = w / sum(w) - backcast = np.sum((abs(resids[:tau]) ** power) * w) + backcast = np.sum((np.absolute(resids[:tau]) ** power) * w) return float(backcast) @@ -1182,7 +1197,7 @@ def simulate( fsigma[:max_lag] = initial_value sigma2[:max_lag] = initial_value ** (2.0 / power) data[:max_lag] = np.sqrt(sigma2[:max_lag]) * errors[:max_lag] - fdata[:max_lag] = abs(data[:max_lag]) ** power + fdata[:max_lag] = np.absolute(data[:max_lag]) ** power for t in range(max_lag, nobs + burn): loc = 0 @@ -1204,7 +1219,7 @@ def simulate( return data[burn:], sigma2[burn:] - def starting_values(self, resids: Float64Array) -> Float64Array: + def starting_values(self, resids: ArrayLike1D) -> Float64Array1D: p, o, q = self.p, self.o, self.q power = self.power alphas = [0.01, 0.05, 0.1, 0.2] @@ -1212,17 +1227,17 @@ def starting_values(self, resids: Float64Array) -> Float64Array: abg = [0.5, 0.7, 0.9, 0.98] abgs = list(itertools.product(*[alphas, gammas, abg])) - target = np.mean(abs(resids) ** power) + target = np.mean(np.absolute(resids) ** power) scale = np.mean(resids**2) / (target ** (2.0 / power)) target *= scale ** (power / 2) - svs = [] + svs: list[Float64Array1D] = [] var_bounds = self.variance_bounds(resids) backcast = self.backcast(resids) llfs = np.zeros(len(abgs)) for i, values in enumerate(abgs): alpha, gamma, agb = values - sv = (1.0 - agb) * target * np.ones(p + o + q + 1) + sv = (1.0 - agb) * target * np.ones(p + o + q + 1, dtype=float) if p > 0: sv[1 : 1 + p] = alpha / p agb -= alpha @@ -1231,8 +1246,10 @@ def starting_values(self, resids: Float64Array) -> Float64Array: agb -= gamma / 2.0 if q > 0: sv[1 + p + o : 1 + p + o + q] = agb / q - svs.append(sv) - llfs[i] = self._gaussian_loglikelihood(sv, resids, backcast, var_bounds) + svs.append(cast(Float64Array1D, sv)) + llfs[i] = self._gaussian_loglikelihood( + cast(Float64Array1D, sv), resids, backcast, var_bounds + ) loc = np.argmax(llfs) return svs[int(loc)] @@ -1254,15 +1271,15 @@ def _check_forecasting_method( def _analytic_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, ) -> VarianceForecast: sigma2, forecasts = self._one_step_forecast( - parameters, resids, backcast, var_bounds, horizon, start + parameters, to_array_1d(resids), backcast, var_bounds, horizon, start ) if horizon == 1: return VarianceForecast(forecasts) @@ -1353,7 +1370,7 @@ def _simulate_paths( 1.0 / power ) lt_zero = shock[:, h + m] < 0 - scaled_shock[:, h + m] = np.abs(shock[:, h + m]) ** power + scaled_shock[:, h + m] = np.absolute(shock[:, h + m]) ** power asym_scaled_shock[:, h + m] = scaled_shock[:, h + m] * lt_zero forecast_paths = scaled_forecast_paths[:, m:] ** (2.0 / power) @@ -1362,17 +1379,17 @@ def _simulate_paths( def _simulation_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, simulations: int, rng: RNGType, ) -> VarianceForecast: sigma2, forecasts = self._one_step_forecast( - parameters, resids, backcast, var_bounds, horizon, start + parameters, to_array_1d(resids), backcast, var_bounds, horizon, start ) t = resids.shape[0] paths = np.empty((t - start, simulations, horizon)) @@ -1395,14 +1412,14 @@ def _simulation_forecast( scaled_forecast_paths[:, m - count : m] = sigma2[:count] ** ( power / 2.0 ) - scaled_shock[:, m - count : m] = np.abs(resids[:count]) ** power - asym = np.abs(resids[:count]) ** power * (resids[:count] < 0) + scaled_shock[:, m - count : m] = np.absolute(resids[:count]) ** power + asym = np.absolute(resids[:count]) ** power * (resids[:count] < 0) asym_scaled_shock[:, m - count : m] = asym else: scaled_forecast_paths[:, :m] = sigma2[i - m + 1 : i + 1] ** ( power / 2.0 ) - scaled_shock[:, :m] = np.abs(resids[i - m + 1 : i + 1]) ** power + scaled_shock[:, :m] = np.absolute(resids[i - m + 1 : i + 1]) ** power asym_scaled_shock[:, :m] = scaled_shock[:, :m] * ( resids[i - m + 1 : i + 1] < 0 ) @@ -1484,7 +1501,7 @@ def __str__(self) -> str: return descr - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: lags = self.lags k_arch = lags.shape[0] @@ -1505,12 +1522,12 @@ def constraints(self) -> tuple[Float64Array, Float64Array]: def compute_variance( self, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, - ) -> Float64Array: + parameters: Float64Array1D, + resids: ArrayLike1D, + sigma2: Float64Array1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, + ) -> Float64Array1D: lags = self.lags nobs = resids.shape[0] @@ -1553,14 +1570,14 @@ def simulate( return data[burn:], sigma2[burn:] - def starting_values(self, resids: Float64Array) -> Float64Array: + def starting_values(self, resids: ArrayLike1D) -> Float64Array1D: k_arch = self._num_lags alpha = 0.9 - sv = (1.0 - alpha) * resids.var() * np.ones(k_arch + 1) + sv = (1.0 - alpha) * np.var(resids) * np.ones(k_arch + 1) sv[1:] = alpha / k_arch - return sv + return to_array_1d(sv) def parameter_names(self) -> list[str]: names = ["omega"] @@ -1578,9 +1595,9 @@ def _harch_to_arch(self, params: Float64Array) -> Float64Array: def _common_forecast_components( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], horizon: int, ) -> tuple[float, Float64Array, Float64Array]: arch_params = self._harch_to_arch(parameters) @@ -1603,10 +1620,10 @@ def _check_forecasting_method( def _analytic_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, ) -> VarianceForecast: @@ -1623,10 +1640,10 @@ def _analytic_forecast( def _simulation_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, simulations: int, @@ -1726,7 +1743,7 @@ def __str__(self) -> str: return descr - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: bounds = [(0.0, 10 * float(np.mean(resids**2.0)))] # omega bounds.extend([(0.0, 1.0)]) # 0 <= alpha < 1 if self._asym: @@ -1775,12 +1792,12 @@ def constraints(self) -> tuple[Float64Array, Float64Array]: def compute_variance( self, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, - ) -> Float64Array: + parameters: Float64Array1D, + resids: ArrayLike1D, + sigma2: Float64Array1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, + ) -> Float64Array1D: nobs = resids.shape[0] weights = self._weights(parameters) if not self._asym: @@ -1838,14 +1855,14 @@ def simulate( return data[burn:], sigma2[burn:] - def starting_values(self, resids: Float64Array) -> Float64Array: + def starting_values(self, resids: ArrayLike1D) -> Float64Array1D: theta = [0.1, 0.5, 0.8, 0.9] alpha = [0.8, 0.9, 0.95, 0.98] var = (resids**2).mean() var_bounds = self.variance_bounds(resids) backcast = self.backcast(resids) llfs = [] - svs = [] + svs: list[Float64Array1D] = [] for a, t in itertools.product(alpha, theta): gamma = [0.0] if self._asym: @@ -1854,13 +1871,15 @@ def starting_values(self, resids: Float64Array) -> Float64Array: total = a + g / 2 o = (1 - min(total, 0.99)) * var if self._asym: - sv = np.array([o, a, g, t]) + sv = cast(Float64Array1D, np.array([o, a, g, t], dtype=float)) else: - sv = np.array([o, a, t]) + sv = cast(Float64Array1D, np.array([o, a, t], dtype=float)) svs.append(sv) - llf = self._gaussian_loglikelihood(sv, resids, backcast, var_bounds) + llf = self._gaussian_loglikelihood( + cast(Float64Array1D, sv), resids, backcast, var_bounds + ) llfs.append(llf) loc = np.argmax(llfs) @@ -1886,7 +1905,7 @@ def _common_forecast_components( self, parameters: Float64Array, resids: Float64Array, - backcast: Union[float, Float64Array], + backcast: Union[float, Float64Array1D], horizon: int, ) -> tuple[int, Float64Array, Float64Array, Float64Array, Float64Array]: if self._asym: @@ -1918,15 +1937,15 @@ def _check_forecasting_method( def _analytic_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, ) -> VarianceForecast: omega, aw, gw, resids2, indicator = self._common_forecast_components( - parameters, resids, backcast, horizon + parameters, to_array_1d(resids), backcast, horizon ) m = self.m resids2 = resids2[start:].copy() @@ -1944,17 +1963,17 @@ def _analytic_forecast( def _simulation_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, simulations: int, rng: RNGType, ) -> VarianceForecast: omega, aw, gw, resids2, indicator = self._common_forecast_components( - parameters, resids, backcast, horizon + parameters, to_array_1d(resids), backcast, horizon ) t = resids.shape[0] m = self.m @@ -2023,19 +2042,21 @@ def __init__(self, p: int = 1) -> None: super().__init__(p, 0, 0, 2.0) self._num_params = p + 1 - def starting_values(self, resids: Float64Array) -> Float64Array: + def starting_values(self, resids: ArrayLike1D) -> Float64Array1D: p = self.p alphas = np.arange(0.1, 0.95, 0.05) - svs = [] + svs: list[Float64Array1D] = [] backcast = self.backcast(resids) llfs = alphas.copy() var_bounds = self.variance_bounds(resids) for i, alpha in enumerate(alphas): - sv = (1.0 - alpha) * resids.var() * np.ones(p + 1) + sv = (1.0 - alpha) * np.var(resids) * np.ones(p + 1) sv[1:] = alpha / p - svs.append(sv) - llfs[i] = self._gaussian_loglikelihood(sv, resids, backcast, var_bounds) + svs.append(cast(Float64Array1D, sv)) + llfs[i] = self._gaussian_loglikelihood( + cast(Float64Array1D, sv), resids, backcast, var_bounds + ) loc = np.argmax(llfs) return svs[int(loc)] @@ -2088,32 +2109,34 @@ def __str__(self) -> str: descr = self.name + "(lam: " + f"{self.lam:0.2f}" + ")" return descr - def starting_values(self, resids: Float64Array) -> Float64Array: + def starting_values(self, resids: ArrayLike1D) -> Float64Array1D: if self._estimate_lam: - return np.array([0.94]) - return np.array([]) + return to_array_1d(np.array([0.94])) + return to_array_1d(np.array([])) def parameter_names(self) -> list[str]: if self._estimate_lam: return ["lam"] return [] - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: if self._estimate_lam: return [(0, 1)] return [] def compute_variance( self, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, - ) -> Float64Array: + parameters: Float64Array1D, + resids: ArrayLike1D, + sigma2: Float64Array1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, + ) -> Float64Array1D: lam = parameters[0] if self._estimate_lam else self.lam assert isinstance(lam, float) - return ewma_recursion(lam, resids, sigma2, resids.shape[0], float(backcast)) + return ewma_recursion( + lam, to_array_1d(resids), sigma2, resids.shape[0], float(backcast) + ) def constraints(self) -> tuple[Float64Array, Float64Array]: if self._estimate_lam: @@ -2159,15 +2182,20 @@ def _check_forecasting_method( def _analytic_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, ) -> VarianceForecast: _, forecasts = self._one_step_forecast( - parameters, resids, backcast, var_bounds, horizon, start_index=start + parameters, + to_array_1d(resids), + backcast, + var_bounds, + horizon, + start_index=start, ) for i in range(1, horizon): forecasts[:, i] = forecasts[:, 0] @@ -2175,10 +2203,10 @@ def _analytic_forecast( def _simulation_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, simulations: int, @@ -2296,13 +2324,13 @@ def _ewma_combination_weights(self) -> Float64Array: return w - def _ewma_smoothing_parameters(self) -> Float64Array: + def _ewma_smoothing_parameters(self) -> Float64Array1D: tau1, kmax, rho = self.tau1, self.kmax, self.rho taus = tau1 * (rho ** np.arange(kmax)) - mus = cast(Float64Array, np.exp(-1.0 / taus)) + mus = cast(Float64Array1D, np.exp(-1.0 / taus)) return mus - def backcast(self, resids: Float64Array) -> Union[float, Float64Array]: + def backcast(self, resids: ArrayLike1D) -> Union[float, Float64Array1D]: """ Construct values for backcasting to start the recursion @@ -2332,31 +2360,33 @@ def backcast(self, resids: Float64Array) -> Union[float, Float64Array]: return backcast def backcast_transform( - self, backcast: Union[float, Float64Array] - ) -> Union[float, Float64Array]: + self, backcast: Union[float, Float64Array1D] + ) -> Union[float, Float64Array1D]: backcast = super().backcast_transform(backcast) mus = self._ewma_smoothing_parameters() backcast_arr = np.asarray(backcast) if backcast_arr.ndim == 0: - backcast_arr = cast(np.ndarray, backcast * np.ones(mus.shape[0])) - if backcast_arr.shape[0] != mus.shape[0] and backcast_arr.ndim != 0: + backcast_arr = cast(Float64Array1D, backcast * np.ones(mus.shape[0])) + if backcast_arr.shape[0] != mus.shape[0] or backcast_arr.ndim != 1: raise ValueError( - "User backcast must be either a scalar or an vector containing the " + "User backcast must be either a scalar or a vector containing the " "number of\ncomponent EWMAs in the model." ) - return backcast_arr + return cast(Float64Array1D, backcast_arr) - def starting_values(self, resids: Float64Array) -> Float64Array: + def starting_values(self, resids: ArrayLike1D) -> Float64Array1D: return np.empty((0,)) def parameter_names(self) -> list[str]: return [] - def variance_bounds(self, resids: Float64Array, power: float = 2.0) -> Float64Array: + def variance_bounds( + self, resids: ArrayLike1D, power: float = 2.0 + ) -> Float64Array2D: return np.ones((resids.shape[0], 1)) * np.array([-1.0, np.finfo(np.double).max]) - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: return [] def constraints(self) -> tuple[Float64Array, Float64Array]: @@ -2364,22 +2394,22 @@ def constraints(self) -> tuple[Float64Array, Float64Array]: def compute_variance( self, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, - ) -> Float64Array: + parameters: Float64Array1D, + resids: ArrayLike1D, + sigma2: Float64Array1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, + ) -> Float64Array1D: nobs = resids.shape[0] kmax = self.kmax w = self._ewma_combination_weights() mus = self._ewma_smoothing_parameters() - sigma2_temp = np.zeros_like(sigma2) - backcast = cast(Float64Array, backcast) + sigma2_temp = np.zeros(sigma2.shape[0], dtype=float) + backcast = cast(Float64Array1D, backcast) for k in range(kmax): mu = mus[k] - ewma_recursion(mu, resids, sigma2_temp, nobs, backcast[k]) + ewma_recursion(mu, to_array_1d(resids), sigma2_temp, nobs, backcast[k]) if k == 0: sigma2[:] = w[k] * sigma2_temp else: @@ -2423,15 +2453,20 @@ def _check_forecasting_method( def _analytic_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, ) -> VarianceForecast: _, forecasts = self._one_step_forecast( - parameters, resids, backcast, var_bounds, horizon, start_index=start + parameters, + to_array_1d(resids), + backcast, + var_bounds, + horizon, + start_index=start, ) for i in range(1, horizon): forecasts[:, i] = forecasts[:, 0] @@ -2439,10 +2474,10 @@ def _analytic_forecast( def _simulation_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, simulations: int, @@ -2451,7 +2486,7 @@ def _simulation_forecast( kmax = self.kmax w = self._ewma_combination_weights() mus = self._ewma_smoothing_parameters() - backcast = cast(Float64Array, np.asarray(backcast)) + backcast = cast(Float64Array1D, to_array_1d(np.asarray(backcast))) t = resids.shape[0] paths = np.empty((t - start, simulations, horizon)) @@ -2463,7 +2498,13 @@ def _simulation_forecast( _resids = np.ascontiguousarray(resids) for k in range(kmax): mu = mus[k] - ewma_recursion(mu, _resids, component_one_step[k, :], t + 1, backcast[k]) + ewma_recursion( + mu, + to_array_1d(_resids), + to_array_1d(component_one_step[k, :]), + t + 1, + backcast[k], + ) # Transpose to be (t+1, kmax) component_one_step = component_one_step.T @@ -2556,10 +2597,12 @@ def __str__(self) -> str: descr = descr[:-2] + ")" return descr - def variance_bounds(self, resids: Float64Array, power: float = 2.0) -> Float64Array: + def variance_bounds( + self, resids: ArrayLike1D, power: float = 2.0 + ) -> Float64Array2D: return super().variance_bounds(resids, 2.0) - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: v = np.mean(resids**2.0) log_const = np.log(10000.0) lnv = np.log(v) @@ -2580,12 +2623,12 @@ def constraints(self) -> tuple[Float64Array, Float64Array]: def compute_variance( self, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, - ) -> Float64Array: + parameters: Float64Array1D, + resids: ArrayLike1D, + sigma2: Float64Array1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, + ) -> Float64Array1D: p, o, q = self.p, self.o, self.q nobs = resids.shape[0] if (self._arrays is not None) and (self._arrays[0].shape[0] == nobs): @@ -2614,12 +2657,12 @@ def compute_variance( return sigma2 def backcast_transform( - self, backcast: Union[float, Float64Array] - ) -> Union[float, Float64Array]: + self, backcast: Union[float, Float64Array1D] + ) -> Union[float, Float64Array1D]: backcast = super().backcast_transform(backcast) return float(np.log(backcast)) - def backcast(self, resids: Float64Array) -> Union[float, Float64Array]: + def backcast(self, resids: ArrayLike1D) -> Union[float, Float64Array1D]: return float(np.log(super().backcast(resids))) def simulate( @@ -2648,9 +2691,8 @@ def simulate( sigma2 = np.zeros(nobs + burn) data = np.zeros(nobs + burn) - lnsigma2: Float64Array1D - lnsigma2 = np.zeros(nobs + burn) - abserrors = cast(Float64Array1D, np.abs(errors)) + lnsigma2: Float64Array1D = np.zeros(nobs + burn) + abserrors = cast(Float64Array1D, np.absolute(errors)) norm_const = np.sqrt(2 / np.pi) max_lag = np.max([p, o, q]) @@ -2672,12 +2714,12 @@ def simulate( lnsigma2[t] += parameters[loc] * lnsigma2[t - 1 - j] loc += 1 - sigma2 = np.exp(lnsigma2) + sigma2 = cast(Float64Array1D, np.exp(lnsigma2)) data = errors * np.sqrt(sigma2) return data[burn:], sigma2[burn:] - def starting_values(self, resids: Float64Array) -> Float64Array: + def starting_values(self, resids: ArrayLike1D) -> Float64Array1D: p, o, q = self.p, self.o, self.q alphas = [0.01, 0.05, 0.1, 0.2] gammas = [-0.1, 0.0, 0.1] @@ -2717,32 +2759,32 @@ def _check_forecasting_method( def _analytic_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, ) -> VarianceForecast: _, forecasts = self._one_step_forecast( - parameters, resids, backcast, var_bounds, horizon, start + parameters, to_array_1d(resids), backcast, var_bounds, horizon, start ) return VarianceForecast(forecasts) def _simulation_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, simulations: int, rng: RNGType, ) -> VarianceForecast: sigma2, forecasts = self._one_step_forecast( - parameters, resids, backcast, var_bounds, horizon, start + parameters, to_array_1d(resids), backcast, var_bounds, horizon, start ) t = resids.shape[0] p, o, q = self.p, self.o, self.q @@ -2758,7 +2800,7 @@ def _simulation_forecast( for i in range(m): lnsigma2_mat[m - i - 1 :, i] = lnsigma2[: (t - (m - 1) + i)] e_mat[m - i - 1 :, i] = e[: (t - (m - 1) + i)] - abs_e_mat[m - i - 1 :, i] = np.abs(e[: (t - (m - 1) + i)]) + abs_e_mat[m - i - 1 :, i] = np.absolute(e[: (t - (m - 1) + i)]) paths = np.empty((t - start, simulations, horizon)) shocks = np.empty((t - start, simulations, horizon)) @@ -2773,7 +2815,7 @@ def _simulation_forecast( _e[:, :m] = e_mat[i, :] _e[:, m:] = std_shocks _abs_e[:, :m] = abs_e_mat[i, :] - _abs_e[:, m:] = np.abs(std_shocks) + _abs_e[:, m:] = np.absolute(std_shocks) for j in range(horizon): loc = 0 _lnsigma2[:, m + j] = parameters[loc] @@ -2830,12 +2872,12 @@ def __init__(self, variance: Float64Array, unit_scale: bool = False) -> None: def compute_variance( self, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, - ) -> Float64Array: + parameters: Float64Array1D, + resids: ArrayLike1D, + sigma2: Float64Array1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, + ) -> Float64Array1D: if self._stop - self._start != sigma2.shape[0]: raise ValueError("start and stop do not have the correct values.") sigma2[:] = self._variance[self._start : self._stop] @@ -2843,14 +2885,14 @@ def compute_variance( sigma2 *= parameters[0] return sigma2 - def starting_values(self, resids: Float64Array) -> Float64Array: + def starting_values(self, resids: ArrayLike1D) -> Float64Array1D: if self._variance.ndim != 1 or self._variance.shape[0] < self._stop: raise ValueError( f"variance must be a 1-d array with at least {self._stop} elements" ) if not self._unit_scale: _resids = resids / np.sqrt(self._variance[self._start : self._stop]) - return np.array([_resids.var()]) + return to_array_1d(np.array([np.var(_resids)], dtype=float)) return np.empty(0) def simulate( @@ -2869,10 +2911,10 @@ def constraints(self) -> tuple[Float64Array, Float64Array]: else: return np.ones((0, 0)), np.zeros(0) - def backcast(self, resids: Float64Array) -> Union[float, Float64Array]: + def backcast(self, resids: ArrayLike1D) -> Union[float, Float64Array1D]: return 1.0 - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: if not self._unit_scale: v = float(np.squeeze(self.starting_values(resids))) _resids = resids / np.sqrt(self._variance[self._start : self._stop]) @@ -2892,10 +2934,10 @@ def _check_forecasting_method( def _analytic_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, ) -> VarianceForecast: @@ -2906,10 +2948,10 @@ def _analytic_forecast( def _simulation_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, simulations: int, @@ -3031,7 +3073,9 @@ def __str__(self) -> str: return descr - def variance_bounds(self, resids: Float64Array, power: float = 2.0) -> Float64Array: + def variance_bounds( + self, resids: ArrayLike1D, power: float = 2.0 + ) -> Float64Array2D: return super().variance_bounds(resids, self.power) def _generate_name(self) -> str: @@ -3052,9 +3096,9 @@ def _generate_name(self) -> str: else: return f"Power FIGARCH (power: {self.power:0.1f})" - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: eps_half = np.sqrt(np.finfo(np.double).eps) - v = np.mean(abs(resids) ** self.power) + v = np.mean(np.absolute(resids) ** self.power) bounds = [(0.0, 10.0 * float(v))] bounds.extend([(0.0, 0.5)] * self.p) # phi @@ -3093,15 +3137,15 @@ def constraints(self) -> tuple[Float64Array, Float64Array]: def compute_variance( self, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, - ) -> Float64Array: + parameters: Float64Array1D, + resids: ArrayLike1D, + sigma2: Float64Array1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, + ) -> Float64Array1D: # fresids is abs(resids) ** power power = self.power - fresids = np.abs(resids) ** power + fresids = np.absolute(resids) ** power p, q, truncation = self.p, self.q, self.truncation @@ -3115,17 +3159,21 @@ def compute_variance( return sigma2 def backcast_transform( - self, backcast: Union[float, Float64Array] - ) -> Union[float, Float64Array]: + self, backcast: Union[float, Float64Array1D] + ) -> Union[float, Float64Array1D]: backcast = super().backcast_transform(backcast) - return np.sqrt(backcast) ** self.power + _backcast = np.sqrt(backcast) ** self.power + if np.isscalar(_backcast): + return float(cast(np.float64, _backcast)) + else: + return to_array_1d(_backcast) - def backcast(self, resids: Float64Array) -> Union[float, Float64Array]: + def backcast(self, resids: ArrayLike1D) -> Union[float, Float64Array1D]: power = self.power tau = min(75, resids.shape[0]) w = 0.94 ** np.arange(tau) w = w / sum(w) - backcast = float(np.sum((abs(resids[:tau]) ** power) * w)) + backcast = float(np.sum((np.absolute(resids[:tau]) ** power) * w)) return backcast @@ -3164,7 +3212,7 @@ def simulate( fsigma[:truncation] = initial_value sigma2[:truncation] = initial_value ** (2.0 / power) data[:truncation] = np.sqrt(sigma2[:truncation]) * errors[:truncation] - fdata[:truncation] = abs(data[:truncation]) ** power + fdata[:truncation] = np.absolute(data[:truncation]) ** power omega = parameters[0] beta = parameters[-1] if q else 0 if beta < 1: @@ -3184,14 +3232,14 @@ def simulate( return data[truncation + burn :], sigma2[truncation + burn :] - def starting_values(self, resids: Float64Array) -> Float64Array: + def starting_values(self, resids: ArrayLike1D) -> Float64Array1D: truncation = self.truncation ds = [0.2, 0.5, 0.7] phi_ratio = [0.2, 0.5, 0.8] if self.p else [0] beta_ratio = [0.1, 0.5, 0.9] if self.q else [0] power = self.power - target = np.mean(abs(resids) ** power) + target = np.mean(np.absolute(resids) ** power) scale = np.mean(resids**2) / (target ** (2.0 / power)) target *= scale ** (power / 2) @@ -3244,15 +3292,15 @@ def _check_forecasting_method( def _analytic_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, ) -> VarianceForecast: sigma2, forecasts = self._one_step_forecast( - parameters, resids, backcast, var_bounds, horizon, start + parameters, to_array_1d(resids), backcast, var_bounds, horizon, start ) if horizon == 1: return VarianceForecast(forecasts) @@ -3286,17 +3334,17 @@ def _analytic_forecast( def _simulation_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, simulations: int, rng: RNGType, ) -> VarianceForecast: sigma2, forecasts = self._one_step_forecast( - parameters, resids, backcast, var_bounds, horizon, start + parameters, to_array_1d(resids), backcast, var_bounds, horizon, start ) t = resids.shape[0] paths = np.empty((t - start, simulations, horizon)) @@ -3313,7 +3361,7 @@ def _simulation_forecast( beta = parameters[-1] if q else 0.0 omega_tilde = omega / (1 - beta) fpath = np.empty((simulations, truncation + horizon)) - fresids = np.abs(resids) ** power + fresids = np.absolute(resids) ** power for i in range(start, t): std_shocks = rng((simulations, horizon)) @@ -3335,7 +3383,7 @@ def _simulation_forecast( paths[path_loc, :, h] = sigma2 forecasts[path_loc, h] = sigma2.mean() # 4. Transform new residual - fpath[:, truncation + h] = np.abs(shocks[path_loc, :, h]) ** power + fpath[:, truncation + h] = np.absolute(shocks[path_loc, :, h]) ** power return VarianceForecast(forecasts, paths, shocks) @@ -3439,7 +3487,7 @@ def common_asym(self) -> bool: """The value of delta in the model. NaN is delta is estimated.""" return self._common_asym - def _repack_parameters(self, parameters: Float64Array) -> Float64Array: + def _repack_parameters(self, parameters: Float64Array1D) -> Float64Array1D: if not self._repack: return parameters p, o, q = self.p, self.o, self.q @@ -3456,13 +3504,13 @@ def _repack_parameters(self, parameters: Float64Array) -> Float64Array: def compute_variance( self, - parameters: Float64Array, - resids: Float64Array, - sigma2: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, - ) -> Float64Array: - abs_resids = np.abs(resids) + parameters: Float64Array1D, + resids: ArrayLike1D, + sigma2: Float64Array1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, + ) -> Float64Array1D: + abs_resids = np.absolute(resids) if self._sigma_delta.shape[0] != resids.shape[0]: self._sigma_delta = np.empty(resids.shape[0]) sigma_delta = self._sigma_delta @@ -3485,8 +3533,8 @@ def compute_variance( return sigma2 - def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: - v = max(float(np.mean(abs(resids) ** 0.5)), float(np.mean(resids**2))) + def bounds(self, resids: ArrayLike1D) -> list[tuple[float, float]]: + v = max(float(np.mean(np.absolute(resids) ** 0.5)), float(np.mean(resids**2))) bounds = [(0.0, 10.0 * float(v))] bounds.extend([(0.0, 1.0)] * self.p) @@ -3498,7 +3546,7 @@ def bounds(self, resids: Float64Array) -> list[tuple[float, float]]: return bounds - def starting_values(self, resids: Float64Array) -> Float64Array: + def starting_values(self, resids: ArrayLike1D) -> Float64Array1D: p, o, q = self.p, self.o, self.q alphas = [0.03, 0.05, 0.08, 0.15] alpha_beta = [0.8, 0.9, 0.95, 0.975, 0.99] @@ -3514,7 +3562,7 @@ def starting_values(self, resids: Float64Array) -> Float64Array: for i, values in enumerate(abgs): alpha, gamma, ab, delta = values - target = np.mean(abs(resids) ** delta) + target = np.mean(np.absolute(resids) ** delta) scale = np.mean(resids**2) / (target ** (2.0 / delta)) target *= scale ** (delta / 2) @@ -3528,7 +3576,9 @@ def starting_values(self, resids: Float64Array) -> Float64Array: if est_delta: sv[-1] = delta svs.append(sv) - llfs[i] = self._gaussian_loglikelihood(sv, resids, backcast, var_bounds) + llfs[i] = self._gaussian_loglikelihood( + to_array_1d(sv), to_array_1d(resids), backcast, var_bounds + ) loc = np.argmax(llfs) sv = svs[int(loc)] if self._common_asym: @@ -3584,7 +3634,7 @@ def simulate( burn: int = 500, initial_value: Union[float, Float64Array, None] = None, ) -> tuple[Float64Array, Float64Array]: - params = np.asarray(ensure1d(parameters, "parameters", False), dtype=float) + params = ensure1d(parameters, "parameters", False).astype(float) params = self._repack_parameters(params) p, o, q = self.p, self.o, self.q errors = rng(nobs + burn) @@ -3608,7 +3658,7 @@ def simulate( sigma2[:max_lag] = initial_value ** (2.0 / delta) data[:max_lag] = np.sqrt(sigma2[:max_lag]) * errors[:max_lag] - adata[:max_lag] = np.abs(data[:max_lag]) + adata[:max_lag] = np.absolute(data[:max_lag]) for t in range(max_lag, nobs + burn): sigma_delta[t] = params[0] @@ -3622,7 +3672,7 @@ def simulate( sigma2[t] = sigma_delta[t] ** (2.0 / delta) data[t] = errors[t] * np.sqrt(sigma2[t]) - adata[t] = np.abs(data[t]) + adata[t] = np.absolute(data[t]) return data[burn:], sigma2[burn:] @@ -3660,7 +3710,7 @@ def _simulate_paths( sigma_delta[:, h + m] += beta[j] * sigma_delta[:, loc - j] shock[:, h + m] = std_shocks[:, h] * sigma_delta[:, h + m] ** (1.0 / delta) - abs_shock[:, h + m] = np.abs(shock[:, h + m]) + abs_shock[:, h + m] = np.absolute(shock[:, h + m]) forecast_paths = sigma_delta[:, m:] ** (2.0 / delta) @@ -3668,17 +3718,17 @@ def _simulate_paths( def _simulation_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, simulations: int, rng: RNGType, ) -> VarianceForecast: sigma2, forecasts = self._one_step_forecast( - parameters, resids, backcast, var_bounds, horizon, start + parameters, to_array_1d(resids), backcast, var_bounds, horizon, start ) parameters = self._repack_parameters(parameters) t = resids.shape[0] @@ -3701,11 +3751,11 @@ def _simulation_forecast( count = i + 1 sigma_delta[:, m - count : m] = sigma2[:count] ** (delta / 2.0) shock[:, m - count : m] = resids[:count] - abs_shock[:, m - count : m] = np.abs(resids[:count]) + abs_shock[:, m - count : m] = np.absolute(resids[:count]) else: sigma_delta[:, :m] = sigma2[i - m + 1 : i + 1] ** (delta / 2.0) shock[:, :m] = resids[i - m + 1 : i + 1] - abs_shock[:, :m] = np.abs(resids[i - m + 1 : i + 1]) + abs_shock[:, :m] = np.absolute(resids[i - m + 1 : i + 1]) f, p, s = self._simulate_paths( m, @@ -3723,15 +3773,15 @@ def _simulation_forecast( def _analytic_forecast( self, - parameters: Float64Array, - resids: Float64Array, - backcast: Union[float, Float64Array], - var_bounds: Float64Array, + parameters: Float64Array1D, + resids: ArrayLike1D, + backcast: Union[float, Float64Array1D], + var_bounds: Float64Array2D, start: int, horizon: int, ) -> VarianceForecast: _, forecasts = self._one_step_forecast( - parameters, resids, backcast, var_bounds, horizon, start + parameters, to_array_1d(resids), backcast, var_bounds, horizon, start ) return VarianceForecast(forecasts) diff --git a/arch/utility/array.py b/arch/utility/array.py index ed0c1e1da3..d6e2fcb16a 100644 --- a/arch/utility/array.py +++ b/arch/utility/array.py @@ -8,12 +8,20 @@ from collections.abc import Hashable, Sequence import datetime as dt from functools import cached_property -from typing import Any, Literal, Optional, Union, overload +from typing import Any, Literal, Optional, Union, cast, overload import numpy as np from pandas import DataFrame, DatetimeIndex, Index, NaT, Series, Timestamp, to_datetime -from arch.typing import AnyArray1D, AnyPandas, ArrayLike, DateLike, NDArray +from arch.typing import ( + AnyArray, + AnyArray1D, + AnyPandas, + ArrayLike, + DateLike, + Float64Array1D, + NDArray, +) __all__ = [ "ensure1d", @@ -24,6 +32,7 @@ "ensure2d", "AbstractDocStringInheritor", "find_index", + "to_array_1d", ] deprecation_doc: str = """ @@ -31,6 +40,36 @@ """ +def to_array_1d(x: Union[AnyArray, Series]) -> Float64Array1D: + """ + Ensure array is 1D and float64 + + Parameters + ---------- + x : {ndarray, Series} + Array to convert + + Returns + ------- + ndarray + 1D float64 array + """ + if isinstance(x, np.ndarray): + if x.ndim == 1 and x.dtype == np.float64: + return cast(Float64Array1D, x) + _x = x.squeeze() + if _x.ndim == 1: + return cast(Float64Array1D, _x.astype(float)) + elif _x.ndim == 0: + return cast(Float64Array1D, np.atleast_1d(_x).astype(float, copy=False)) + else: + raise ValueError("x must be 1D or 1D convertible") + elif isinstance(x, Series): + return x.to_numpy().astype(float, copy=False) + else: + raise TypeError("x must be a Series or ndarray") + + @overload def ensure1d( x: Union[int, float, Sequence[Union[int, float]], ArrayLike], diff --git a/examples/bootstrap_examples.ipynb b/examples/bootstrap_examples.ipynb index 71c566049f..20398f1795 100644 --- a/examples/bootstrap_examples.ipynb +++ b/examples/bootstrap_examples.ipynb @@ -50,11 +50,10 @@ "metadata": {}, "outputs": [], "source": [ + "import arch.data.frenchdata\n", "import numpy as np\n", "import pandas as pd\n", "\n", - "import arch.data.frenchdata\n", - "\n", "ff = arch.data.frenchdata.load()" ] }, @@ -472,7 +471,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/examples/multiple-comparison_examples.ipynb b/examples/multiple-comparison_examples.ipynb index abdbe14f98..88be806cfe 100644 --- a/examples/multiple-comparison_examples.ipynb +++ b/examples/multiple-comparison_examples.ipynb @@ -229,14 +229,14 @@ "source": [ "import pandas as pd\n", "\n", - "pvalues = pd.DataFrame(pvalues)\n", - "for col in pvalues:\n", - " values = pvalues[col].to_numpy(copy=True)\n", + "pvalues_df = pd.DataFrame(pvalues)\n", + "for col in pvalues_df:\n", + " values = pvalues_df[col].to_numpy(copy=True)\n", " values.sort()\n", - " pvalues[col] = values\n", + " pvalues_df[col] = values\n", "# Change the index so that the x-values are between 0 and 1\n", - "pvalues.index = np.linspace(0.005, 0.995, 100)\n", - "fig = pvalues.plot()" + "pvalues_df.index = pd.Index(np.linspace(0.005, 0.995, 100))\n", + "fig = pvalues_df.plot()" ] }, { @@ -288,8 +288,10 @@ "metadata": {}, "outputs": [], "source": [ - "model_losses = pd.DataFrame(model_losses, columns=[\"model.\" + str(i) for i in range(k)])\n", - "avg_model_losses = pd.DataFrame(model_losses.mean(0), columns=[\"Average loss\"])\n", + "model_losses_df = pd.DataFrame(\n", + " model_losses, columns=[\"model.\" + str(i) for i in range(k)]\n", + ")\n", + "avg_model_losses = pd.DataFrame(model_losses_df.mean(axis=0), columns=[\"Average loss\"])\n", "fig = avg_model_losses.plot(style=[\"o\"])" ] }, @@ -315,7 +317,7 @@ "source": [ "from arch.bootstrap import StepM\n", "\n", - "stepm = StepM(bm_losses, model_losses)\n", + "stepm = StepM(bm_losses, model_losses_df)\n", "stepm.compute()\n", "print(\"Model indices:\")\n", "print([model.split(\".\")[1] for model in stepm.superior_models])" @@ -327,7 +329,9 @@ "metadata": {}, "outputs": [], "source": [ - "better_models = pd.concat([model_losses.mean(0), model_losses.mean(0)], axis=1)\n", + "better_models = pd.concat(\n", + " [model_losses_df.mean(axis=0), model_losses_df.mean(axis=0)], axis=1\n", + ")\n", "better_models.columns = [\"Same or worse\", \"Better\"]\n", "better = better_models.index.isin(stepm.superior_models)\n", "worse = np.logical_not(better)\n", @@ -359,7 +363,7 @@ "from arch.bootstrap import MCS\n", "\n", "# Limit the size of the set\n", - "losses = model_losses.iloc[:, ::20]\n", + "losses = model_losses_df.iloc[:, ::20]\n", "mcs = MCS(losses, size=0.10)\n", "mcs.compute()\n", "print(\"MCS P-values\")\n", @@ -379,7 +383,7 @@ "outputs": [], "source": [ "status = pd.DataFrame(\n", - " [losses.mean(0), losses.mean(0)], index=[\"Excluded\", \"Included\"]\n", + " [losses.mean(axis=0), losses.mean(axis=0)], index=[\"Excluded\", \"Included\"]\n", ").T\n", "status.loc[status.index.isin(included), \"Excluded\"] = np.nan\n", "status.loc[status.index.isin(excluded), \"Included\"] = np.nan\n", @@ -403,7 +407,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/examples/unitroot_cointegration_examples.ipynb b/examples/unitroot_cointegration_examples.ipynb index 898993458b..bd17785808 100644 --- a/examples/unitroot_cointegration_examples.ipynb +++ b/examples/unitroot_cointegration_examples.ipynb @@ -54,7 +54,6 @@ "outputs": [], "source": [ "import numpy as np\n", - "\n", "from arch.data import crude\n", "\n", "data = crude.load()\n", @@ -390,7 +389,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/examples/unitroot_examples.ipynb b/examples/unitroot_examples.ipynb index d8ade3f3a0..a668f8d76d 100644 --- a/examples/unitroot_examples.ipynb +++ b/examples/unitroot_examples.ipynb @@ -54,11 +54,10 @@ "metadata": {}, "outputs": [], "source": [ + "import arch.data.default\n", "import pandas as pd\n", "import statsmodels.api as sm\n", "\n", - "import arch.data.default\n", - "\n", "default_data = arch.data.default.load()\n", "default = default_data.BAA.copy()\n", "default.name = \"default\"\n", @@ -436,9 +435,8 @@ "metadata": {}, "outputs": [], "source": [ - "import pandas as pd\n", - "\n", "import arch.data.frenchdata\n", + "import pandas as pd\n", "\n", "ff = arch.data.frenchdata.load()\n", "excess_market = ff.iloc[:, 0] # Excess Market\n", @@ -509,7 +507,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/examples/univariate_forecasting_with_exogenous_variables.ipynb b/examples/univariate_forecasting_with_exogenous_variables.ipynb index 263df8b387..cde65ac79c 100644 --- a/examples/univariate_forecasting_with_exogenous_variables.ipynb +++ b/examples/univariate_forecasting_with_exogenous_variables.ipynb @@ -711,7 +711,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/examples/univariate_using_fixed_variance.ipynb b/examples/univariate_using_fixed_variance.ipynb index ffeff85e96..0adb460d74 100644 --- a/examples/univariate_using_fixed_variance.ipynb +++ b/examples/univariate_using_fixed_variance.ipynb @@ -214,7 +214,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/examples/univariate_volatility_forecasting.ipynb b/examples/univariate_volatility_forecasting.ipynb index 763b90cb6c..f87b574961 100644 --- a/examples/univariate_volatility_forecasting.ipynb +++ b/examples/univariate_volatility_forecasting.ipynb @@ -46,10 +46,9 @@ "source": [ "import sys\n", "\n", + "import arch.data.sp500\n", "import numpy as np\n", "import pandas as pd\n", - "\n", - "import arch.data.sp500\n", "from arch import arch_model\n", "\n", "data = arch.data.sp500.load()\n", @@ -493,7 +492,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/examples/univariate_volatility_modeling.ipynb b/examples/univariate_volatility_modeling.ipynb index f9d2d3f924..d83878592b 100644 --- a/examples/univariate_volatility_modeling.ipynb +++ b/examples/univariate_volatility_modeling.ipynb @@ -640,7 +640,6 @@ "outputs": [], "source": [ "import numpy as np\n", - "\n", "from arch.univariate import ConstantMean, SkewStudent\n", "\n", "rs = np.random.RandomState([892380934, 189201902, 129129894, 9890437])\n", @@ -700,7 +699,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.12.9" }, "pycharm": { "stem_cell": { diff --git a/examples/univariate_volatility_scenarios.ipynb b/examples/univariate_volatility_scenarios.ipynb index de6a9221a8..56babe77eb 100644 --- a/examples/univariate_volatility_scenarios.ipynb +++ b/examples/univariate_volatility_scenarios.ipynb @@ -29,7 +29,6 @@ "import numpy as np\n", "import pandas as pd\n", "import seaborn as sns\n", - "\n", "from arch.univariate import GARCH, ConstantMean, Normal\n", "\n", "sns.set_style(\"darkgrid\")\n", @@ -357,7 +356,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/requirements-dev.txt b/requirements-dev.txt index e15525dc0b..5c4c27cd2a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -16,7 +16,7 @@ pytest-xdist pytest-cov # formatting -black[jupyter]~=24.10.0 +black[jupyter]~=25.1.0 isort~=5.12 colorama flake8