Source code for neuralfieldmanifold.models.tar
"""Two-regime threshold autoregression with residual-bootstrap simulation."""
from dataclasses import dataclass
import numpy as np
from .._validation import positive_integer, signal_array
from ..embedding import lag_matrix
from .ar import ARFit
[docs]
@dataclass(frozen=True)
class TARFit:
"""Lower and upper AR regimes, threshold, delay, and residual pools."""
lower: ARFit
upper: ARFit
threshold: float
delay_samples: int
lower_residuals: np.ndarray
upper_residuals: np.ndarray
[docs]
def simulate(self, initial_lags, *, n_samples, seed=0, match_reference=None):
"""Simulate with bootstrapped residuals and newest-first initial lags.
If match_reference is supplied, the generated signal is rescaled to
its mean and standard deviation. This rescaling is intended for
distribution-matched simulation, not held-out forecast evaluation.
"""
n_samples = positive_integer(n_samples, "n_samples")
state = (
signal_array(initial_lags, min_samples=self.lower.order)
.astype(float)
.copy()
)
if len(state) != self.lower.order:
raise ValueError("initial_lags must contain exactly order values")
rng = np.random.default_rng(seed)
out = np.empty(n_samples)
for t in range(n_samples):
is_lower = state[self.delay_samples - 1] <= self.threshold
model = self.lower if is_lower else self.upper
pool = self.lower_residuals if is_lower else self.upper_residuals
innovation = rng.choice(pool) if len(pool) else 0.0
out[t] = model.intercept + np.dot(model.coefficients, state) + innovation
state[1:] = state[:-1]
state[0] = out[t]
if match_reference is not None:
reference = signal_array(match_reference)
out = (out - out.mean()) * (
reference.std() / (out.std() or 1.0)
) + reference.mean()
return out
[docs]
class TAR:
"""Fit two AR regimes separated by a delayed signal threshold.
Parameters
----------
order : int
AR order in each regime.
delay_samples : int
Lag used for threshold comparison, between 1 and order.
threshold : float
Values at or below the threshold use the lower regime.
"""
def __init__(self, *, order, delay_samples=1, threshold=0.0):
self.order = positive_integer(order, "order")
self.delay_samples = positive_integer(delay_samples, "delay_samples")
if self.delay_samples > self.order or not np.isfinite(threshold):
raise ValueError("Require delay_samples <= order and a finite threshold")
self.threshold = float(threshold)
[docs]
def fit(self, signal):
"""Estimate both regimes by least squares and return a TARFit."""
x = signal_array(signal, min_samples=self.order + 2)
predictors, targets = lag_matrix(x, order=self.order)
variable = x[self.order - self.delay_samples : len(x) - self.delay_samples]
threshold = self.threshold
lower = variable <= threshold
if min(lower.sum(), (~lower).sum()) < self.order + 5:
raise ValueError(
"Too few samples in a regime; select an explicit supported threshold"
)
models, residuals = [], []
for mask in (lower, ~lower):
design = np.column_stack([np.ones(mask.sum()), predictors[mask]])
weights = np.linalg.lstsq(design, targets[mask], rcond=None)[0]
models.append(ARFit(float(weights[0]), weights[1:]))
residuals.append(targets[mask] - design @ weights)
return TARFit(*models, threshold, self.delay_samples, *residuals)