SpectralDensityResult#

class cultivars.spectral.density.SpectralDensityResult(*, names, frequencies, density)[source]

Bases: _SummaryMixin

A fitted system’s spectral density matrix, and the readings off it.

The matrix is the closed form

\[f(\omega) = \frac{1}{2\pi}\, \Psi(\omega)\, \Sigma_u\, \Psi(\omega)^\ast, \qquad \Psi(\omega) = \Bigl[I - \sum_{l=1}^{p} A_l e^{-i\omega l}\Bigr]^{-1},\]

evaluated on a grid; every reading here is a function of its entries. All frequencies are radians per observation on [0, pi]; the process is real, so the negative half-circle is the conjugate mirror. The density carries the \(1 / 2\pi\) normalization: integrating it over \([-\pi, \pi]\) returns the model’s unconditional autocovariance at lag zero, so twice the integral of a diagonal entry over \([0, \pi]\) is that variable’s variance.

Variables:
  • names – Variable labels, indexing the matrix axes.

  • frequencies – The (n,) grid on [0, pi].

  • density – The (n, k, k) complex Hermitian density matrix.

Note

Every number is a statement about the fitted model, not about the data: the density inherits whatever the time-domain fit got wrong, and a spectral peak here is where the model puts variance. A near-integrated system has a density that diverges at frequency zero, legitimately, and the summary’s peak search skips that point for that reason. The grid is uniform, so the long periods are coarsely resolved: with 256 points on \([0, \pi]\) the spacing is about \(\pi / 255\), which at the business-cycle end (periods 6 to 32) is twenty or so points and at periods above 100 only two or three.

See also

References

Hamilton, J. D. (1994). Time Series Analysis, chapter 6. Princeton University Press.

Priestley, M. B. (1981). Spectral Analysis and Time Series. Academic Press.

Example

A rate that feeds into output one period ahead; the spectrum integrates to the model’s variance, the two are coherent at low frequencies, and the phase says the rate leads:

>>> import numpy as np
>>> from cultivars.multivariate.reduced_form.vector_autoregression import VAR
>>> rng = np.random.default_rng(0)
>>> y = np.zeros((500, 2))
>>> for t in range(1, 500):
...     y[t, 0] = 0.5 * y[t - 1, 0] + 0.4 * y[t - 1, 1] + rng.standard_normal()
...     y[t, 1] = 0.5 * y[t - 1, 1] + rng.standard_normal()
>>> res = VAR(y, order=1, names=("gdp", "rate")).fit()
>>> density = SpectralDensity(res, n_frequencies=64).compute()
>>> density.density.shape, density.k_endog
((64, 2, 2), 2)
>>> variance = 2.0 * np.trapezoid(density.spectrum("gdp"), density.frequencies)
>>> bool(abs(variance - y[:, 0].var()) < 0.05)
True
>>> density.coherence("gdp", "rate")[[1, -1]].round(2)
array([0.35, 0.07])
>>> bool(density.phase("gdp", "rate")[1] < 0.0)
True

Private methods#

_index(name)

The axis index of a variable label, after checking it exists.

_summary_table()

Build the structured summary: peak frequency and power share per variable.