Core concepts#
ns-sdn follows the conventions of cultivars, so that a workflow
written against one library reads the other and so that cultivars can
expose the model under its experimental namespace without adapters. This
page states them once, so that the API reference can assume them.
Model, fit, result#
A model object holds the series and the specification and nothing else.
fit() trains the network and returns a result:
NSSDNResult
Everything that describes what model goes to the constructor: the
number of components, the state dimension, the transition, the heads’
constraints, the alignment, the objective, the penalties, the schedule,
the scaling, the seed. There is nothing to pass to fit(). One
specification can be fitted again by calling fit() again; with a
seed the two fits agree.
Results are records#
A result is a frozen dataclass. It carries the decomposition, the one-step fit, the trained network behind a numpy interface, and the training history, all as attributes, and it cannot be changed after it is built:
print(result.nobs, result.k, result.trend.shape, result.amplitude.shape)
try:
result.k = 3
except AttributeError as error:
print(type(error).__name__)
159 2 (160,) (160, 2)
FrozenInstanceError
Because results are values rather than handles to a model, anything that
reads a result’s attributes can take any result that has them: a
SpectralState is built from any result
with frequency, effective_amplitude, components and time;
compare takes any results with llf, nobs and n_params.
To change a fit, build a new model and fit it; the old result stays
exactly as it was. Results pickle, through the network’s state dict.
Summaries state their assumptions#
summary() returns a SummaryTable, a record
with four parts:
title: the specification;metadata: label/value pairs such as sample size, log-likelihood, information criteria and the two RMSEs;rows: one table with a row per component;notes: what the numbers assume and what they are not.
It renders as text (to_text(), which is also str(result)), as
HTML in a notebook, and as a dataframe (to_pandas()). The notes are
part of the result, not decoration: the first says which alignment the
fit used and what that makes of its one-step numbers, the second that
the likelihood is concentrated at a penalized fit, the third, when it
appears, that the components are identified only up to permutation.
Two alignments#
The network reads the series through a GRU, and the question is whether the emission at step \(t\) is read from the state after the network has seen \(y_t\) or before.
alignment="filtered"(the default, and the published run)The state at \(t\) has consumed \(y_t\). The emission at \(t\) is a reconstruction of \(y_t\), and the decomposition is a filtered reading of the observed series. The one-step forecast of \(y_{t+1}\) is the emission at \(t\), which is the paper’s holdout protocol. Every path has one row per observation.
alignment="predicted"The state at \(t\) has consumed only \(y_{t-1}\). Every path is a one-step-ahead prediction and the network trains as a forecaster. Every path has one row fewer than the series, because the first observation has nothing before it.
Under both, fittedvalues has n - 1 rows aligned with
endog[1:] and is the one-step conditional prediction of each
observation, so one-step diagnostics (resid, sigma2, llf,
r_squared) mean the same thing whichever alignment produced them.
What differs is the decomposition’s meaning: a description of the series
versus a prediction of it. The
forecasting page works through the
consequences.
The schedule#
Training is staged. A Stage is a
learning rate, a number of Adam steps, and optionally its own
Penalties; the model’s
penalties argument supplies the penalties for stages that carry
none. The default schedule is the published two stages:
3000
The two published penalties discourage step-to-step changes in the
gated amplitudes and in the frequencies, which is what keeps the
components slow enough to read. The four further penalties
(phase_curvature, trend, sparsity, spectrum) are off by
default and exist for the experiments the
model page describes. The result’s
history records the loss and its parts by stage and step.
Data conventions#
Inputs are anything numpy.asarray() accepts. The series is
one-dimensional with at least three finite observations; an optional
exog block is (n, p) and is used as given, without scaling; an
optional time grid is strictly increasing and defaults to the
integers, and its spacing enters the phase recursion. Results carry
integer positions and leave your index alone: to_pandas(index=dates)
attaches dates on the way out. The series is min-max scaled to
\([-1, 1]\) on the sample before training (scaling="minmax"), and
every path is mapped back to the original scale unless asked for in
training units. The conventions page
lists every shape.
Refusals, not silent failure#
Three exceptions, one per kind of fix, all under
NSSDNError:
DimensionErrorThe geometry of an input is wrong: a two-dimensional series, an
exogwith the wrong number of rows, a forecast without the covariates the fit used. Reshape the data.SpecificationErrorThe content of a choice is wrong:
k=0, an unknown transition, a negative penalty, a threshold outside the unit interval. Change the argument.NumericalErrorThe arithmetic could not be carried out: non-finite data, a diverged fit. Look at the numbers.
Each also derives from the matching builtin (ValueError for the first
two, RuntimeError for the third), and the hierarchy mirrors
cultivars.exceptions, so one except clause handles both
libraries.