Skip to content

Commit bcd6208

Browse files
DOC add example for basic of NARX (#32)
1 parent 3ed8e5c commit bcd6208

File tree

4 files changed

+649
-507
lines changed

4 files changed

+649
-507
lines changed

examples/plot_narx.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""
2+
===============================================
3+
Nonlinear AutoRegressive eXogenous (NARX) model
4+
===============================================
5+
6+
.. currentmodule:: fastcan
7+
8+
In this example, we illustrate how to build a polynomial
9+
NARX model for time series prediction.
10+
"""
11+
12+
# Authors: Sikai Zhang
13+
# SPDX-License-Identifier: MIT
14+
15+
# %%
16+
# Prepare data
17+
# ------------
18+
#
19+
# First, a simulated time series dataset is generated from the following nonlinear
20+
# system.
21+
#
22+
# .. math::
23+
# y(k) = 0.5y(k-1) + 0.3u_0(k)^2 + 2u_0(k-1)u_0(k-3) + 1.5u_0(k-2)u_1(k-3) + 1
24+
#
25+
# where :math:`k` is the time index,
26+
# :math:`u_0` and :math:`u_1` are input signals,
27+
# and :math:`y` is the output signal.
28+
29+
30+
import numpy as np
31+
32+
rng = np.random.default_rng(12345)
33+
n_samples = 1000
34+
max_delay = 3
35+
e = rng.normal(0, 0.1, n_samples)
36+
u0 = rng.uniform(0, 1, n_samples+max_delay)
37+
u1 = rng.normal(0, 0.1, n_samples+max_delay)
38+
y = np.zeros(n_samples+max_delay)
39+
for i in range(max_delay, n_samples+max_delay):
40+
y[i] = 0.5*y[i-1]+0.3*u0[i]**2+2*u0[i-1]*u0[i-3]+1.5*u0[i-2]*u1[i-3]+1
41+
y = y[max_delay:]+e
42+
X = np.c_[u0[max_delay:], u1[max_delay:]]
43+
44+
# %%
45+
# Build term libriary
46+
# -------------------
47+
#
48+
# To build a polynomial NARX model, it is normally have two steps:
49+
#
50+
# 1. Search the structure of the model, i.e., the terms in the model, e.g.,
51+
# :math:`u_0(k-1)u_0(k-3)`, :math:`u_0(k-2)u_1(k-3)`, etc.
52+
# 2. Learn the coefficients before the terms.
53+
#
54+
# To search the structure of the model, the candidate term libriary should be
55+
# constructed.
56+
#
57+
# 1. Time-shifted variables: the raw input-output data, i.e., :math:`u0(k)`,
58+
# :math:`u1(k)`, and :math:`y(k)`, are converted into :math:`u0(k-1)`, :math:`u1(k-2)`,
59+
# etc.
60+
# 2. Nonlinear terms: the time-shifted variables are onverted to nonlinear terms
61+
# via polynomial basis functions, e.g., :math:`u_0(k-1)^2`, :math:`u_0(k-1)u_0(k-3)`,
62+
# etc.
63+
#
64+
# .. rubric:: References
65+
#
66+
# * `"Nonlinear system identification: NARMAX methods in the time, frequency,
67+
# and spatio-temporal domains" <https://doi.org/10.1002/9781118535561>`_
68+
# Billings, S. A. John Wiley & Sons, (2013).
69+
#
70+
#
71+
# Make time-shifted variables
72+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^
73+
74+
from fastcan.narx import make_time_shift_features, make_time_shift_ids
75+
76+
time_shift_ids = make_time_shift_ids(
77+
n_features=3, # Number of inputs (2) and output (1) signals
78+
max_delay=3, # Maximum time delays
79+
include_zero_delay = [True, True, False], # Whether to include zero delay
80+
# for each signal. The output signal should not have zero delay.
81+
)
82+
83+
time_shift_vars = make_time_shift_features(np.c_[X, y], time_shift_ids)
84+
85+
# %%
86+
# Make nonlinear terms
87+
# ^^^^^^^^^^^^^^^^^^^^
88+
89+
from fastcan.narx import make_poly_features, make_poly_ids
90+
91+
poly_ids = make_poly_ids(
92+
n_features=time_shift_vars.shape[1], # Number of time-shifted variables
93+
degree=2, # Maximum polynomial degree
94+
)
95+
96+
poly_terms = make_poly_features(time_shift_vars, poly_ids)
97+
98+
# %%
99+
# Term selection
100+
# --------------
101+
# After the term library is constructed, the terms can be selected by :class:`FastCan`,
102+
# whose :math:`X` is the nonlinear terms and :math:`y` is the output signal.
103+
104+
from fastcan import FastCan
105+
106+
selector = FastCan(
107+
n_features_to_select=4, # 4 terms should be selected
108+
).fit(poly_terms, y)
109+
110+
support = selector.get_support()
111+
selected_poly_ids = poly_ids[support]
112+
113+
114+
# %%
115+
# Build NARX model
116+
# ----------------
117+
# As the polynomical NARX is a linear function of the nonlinear tems,
118+
# the coefficient of each term can be easily estimated by oridnary least squares.
119+
120+
from fastcan.narx import NARX, print_narx
121+
122+
narx_model = NARX(
123+
time_shift_ids=time_shift_ids,
124+
poly_ids = selected_poly_ids,
125+
)
126+
127+
narx_model.fit(X, y)
128+
129+
# In the printed NARX model, it is found that :class:`FastCan` selects the correct
130+
# terms and the coefficients are close to the true values.
131+
print_narx(narx_model)
132+
133+
# %%
134+
# Plot NARX prediction performance
135+
# --------------------------------
136+
137+
import matplotlib.pyplot as plt
138+
from sklearn.metrics import r2_score
139+
140+
y_pred = narx_model.predict(
141+
X[:100],
142+
y_init=y[:narx_model.max_delay_] # Set the initial values of the prediction to
143+
# the true values
144+
)
145+
146+
plt.plot(y[:100], label="True")
147+
plt.plot(y_pred, label="Predicted")
148+
plt.xlabel("Time index k")
149+
plt.legend()
150+
plt.title(f"NARX prediction R-squared: {r2_score(y[:100], y_pred):.5f}")
151+
plt.show()

fastcan/narx.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,9 @@ def _mask_missing_value(*arr):
274274
return tuple([x[mask_nomissing] for x in arr])
275275

276276

277-
class Narx(RegressorMixin, BaseEstimator):
277+
class NARX(RegressorMixin, BaseEstimator):
278278
"""The Nonlinear Autoregressive eXogenous (NARX) model class.
279-
For example, a (polynomial) Narx model is like
279+
For example, a (polynomial) NARX model is like
280280
y(t) = y(t-1)*u(t-1) + u(t-1)^2 + u(t-2) + 1.5
281281
where y(t) is the system output at time t,
282282
u(t) is the system input at time t,
@@ -333,7 +333,7 @@ class Narx(RegressorMixin, BaseEstimator):
333333
Examples
334334
--------
335335
>>> import numpy as np
336-
>>> from fastcan.narx import Narx, print_narx
336+
>>> from fastcan.narx import NARX, print_narx
337337
>>> rng = np.random.default_rng(12345)
338338
>>> n_samples = 1000
339339
>>> max_delay = 3
@@ -351,7 +351,7 @@ class Narx(RegressorMixin, BaseEstimator):
351351
>>> poly_ids = [[0, 2], # 1*u(k-1)
352352
... [0, 4], # 1*y(k-1)
353353
... [1, 3]] # u(k-1)*u(k-3)
354-
>>> narx = Narx(time_shift_ids=time_shift_ids,
354+
>>> narx = NARX(time_shift_ids=time_shift_ids,
355355
... poly_ids=poly_ids).fit(X, y, coef_init="one_step_ahead")
356356
>>> print_narx(narx)
357357
| Term | Coef |
@@ -397,16 +397,16 @@ def fit(self, X, y, sample_weight=None, coef_init=None, **params):
397397
398398
sample_weight : array-like of shape (n_samples,), default=None
399399
Individual weights for each sample, which are used for a One-Step-Ahead
400-
Narx.
400+
NARX.
401401
402402
coef_init : array-like of shape (n_terms,), default=None
403403
The initial values of coefficients and intercept for optimization.
404-
When `coef_init` is None, the model will be a One-Step-Ahead Narx.
404+
When `coef_init` is None, the model will be a One-Step-Ahead NARX.
405405
When `coef_init` is `one_step_ahead`, the model will be a Multi-Step-Ahead
406-
Narx whose coefficients and intercept are initialized by the a
407-
One-Step-Ahead Narx.
406+
NARX whose coefficients and intercept are initialized by the a
407+
One-Step-Ahead NARX.
408408
When `coef_init` is an array, the model will be a Multi-Step-Ahead
409-
Narx whose coefficients and intercept are initialized by the array.
409+
NARX whose coefficients and intercept are initialized by the array.
410410
411411
.. note::
412412
When coef_init is None, missing values (i.e., np.nan) are allowed.
@@ -477,7 +477,7 @@ def fit(self, X, y, sample_weight=None, coef_init=None, **params):
477477
n_terms = self.poly_ids_.shape[0] + 1
478478

479479
if isinstance(coef_init, (type(None), str)):
480-
# fit a one-step-ahead Narx model
480+
# fit a one-step-ahead NARX model
481481
xy_hstack = np.c_[X, y]
482482
osa_narx = LinearRegression()
483483
time_shift_vars = make_time_shift_features(xy_hstack, self.time_shift_ids_)
@@ -508,7 +508,7 @@ def fit(self, X, y, sample_weight=None, coef_init=None, **params):
508508
)
509509

510510
lsq = least_squares(
511-
Narx._residual,
511+
NARX._residual,
512512
x0=coef_init,
513513
args=(
514514
self._expression,
@@ -578,7 +578,7 @@ def _residual(
578578
coef = coef_intercept[:-1]
579579
intercept = coef_intercept[-1]
580580

581-
y_hat = Narx._predict(expression, X, y[:max_delay], coef, intercept, max_delay)
581+
y_hat = NARX._predict(expression, X, y[:max_delay], coef, intercept, max_delay)
582582

583583
y_masked, y_hat_masked = _mask_missing_value(y, y_hat)
584584

@@ -622,7 +622,7 @@ def predict(self, X, y_init=None):
622622
f"but got {y_init.shape}."
623623
)
624624

625-
return Narx._predict(
625+
return NARX._predict(
626626
self._expression,
627627
X,
628628
y_init,
@@ -639,7 +639,7 @@ def __sklearn_tags__(self):
639639

640640
@validate_params(
641641
{
642-
"narx": [Narx],
642+
"narx": [NARX],
643643
"term_space": [Interval(Integral, 1, None, closed="left")],
644644
"coef_space": [Interval(Integral, 1, None, closed="left")],
645645
"float_precision": [Interval(Integral, 0, None, closed="left")],
@@ -652,12 +652,12 @@ def print_narx(
652652
coef_space=10,
653653
float_precision=3,
654654
):
655-
"""Print a Narx model as a Table which contains Term and Coef.
655+
"""Print a NARX model as a Table which contains Term and Coef.
656656
657657
Parameters
658658
----------
659-
narx : Narx model
660-
The Narx model to be printed.
659+
narx : NARX model
660+
The NARX model to be printed.
661661
662662
term_space: int, default=20
663663
The space for the column of Term.
@@ -671,14 +671,14 @@ def print_narx(
671671
Returns
672672
-------
673673
table : str
674-
The table of terms and coefficients of the Narx model.
674+
The table of terms and coefficients of the NARX model.
675675
676676
Examples
677677
--------
678678
>>> from sklearn.datasets import load_diabetes
679-
>>> from fastcan.narx import print_narx, Narx
679+
>>> from fastcan.narx import print_narx, NARX
680680
>>> X, y = load_diabetes(return_X_y=True)
681-
>>> print_narx(Narx().fit(X, y), term_space=10, coef_space=5, float_precision=0)
681+
>>> print_narx(NARX().fit(X, y), term_space=10, coef_space=5, float_precision=0)
682682
| Term |Coef |
683683
==================
684684
|Intercept | 152 |
@@ -765,7 +765,7 @@ def make_narx(
765765
refine_max_iter=None,
766766
**params,
767767
):
768-
"""Find `time_shift_ids` and `poly_ids` for a Narx model.
768+
"""Find `time_shift_ids` and `poly_ids` for a NARX model.
769769
770770
Parameters
771771
----------
@@ -810,8 +810,8 @@ def make_narx(
810810
811811
Returns
812812
-------
813-
narx : Narx
814-
Narx instance.
813+
narx : NARX
814+
NARX instance.
815815
816816
Examples
817817
--------
@@ -912,4 +912,4 @@ def make_narx(
912912
- 1
913913
)
914914

915-
return Narx(time_shift_ids=time_shift_ids, poly_ids=poly_ids)
915+
return NARX(time_shift_ids=time_shift_ids, poly_ids=poly_ids)

0 commit comments

Comments
 (0)