Qu

Machine learning

AR model forecast

classical AR(2) autoregressive fitting via the Yule-Walker equations, recovering KNOWN generating coefficients from a synthetic series, then forecasting beyond the observed data.

catalog/qu_ar_model_forecast.qu

The synthetic series uses the exact generating formula from the engine's own regression test (`ar_model_recovers_known_ar2_coefficients_from_a_ synthetic_series` in engine/crates/qu-interp/src/lib.rs): x[t] = c1*x[t-1] + c2*x[t-2] + 0.1*randn(), c1=0.6, c2=-0.3 a stationary AR(2) pair (characteristic roots outside the unit circle). That test measured (while writing it) that n=20000 recovers c1 within ~0.015 of the true 0.6 -- ordinary Yule-Walker finite-sample noise, not a fitting bug -- so this demo uses the same n and the same tolerance logic.

What it exercises

How it works

Generate a synthetic AR(2) series with KNOWN coefficients.

seed(12345)
n = 20000
c1 = 0.6
c2 = -0.3
x = zeros(n)
x[0] = randn()
x[1] = randn()
for t = 2 to n - 1
    x[t] = c1 * x[t - 1] + c2 * x[t - 2] + 0.1 * randn()
end for

Fit an AR(2) model and compare recovered coefficients to the truth.

m = ar_model(x, 2)
c1_hat = m.coef[0]
c2_hat = m.coef[1]
print("True coefficients:      c1={c1:.3f}, c2={c2:.3f}")
print("Yule-Walker recovered:  c1={c1_hat:.4f}, c2={c2_hat:.4f}")
print("Fitted mean: {m.mean:.4f} (series mean, series is zero-mean by construction)")

if abs(c1_hat - c1) < 0.05 then
    print("c1 recovered within the test suite's own 0.05 tolerance.")
else
    print("MISMATCH: c1_hat is further than 0.05 from the true c1!")
end if
if abs(c2_hat - c2) < 0.05 then
    print("c2 recovered within the test suite's own 0.05 tolerance.")
else
    print("MISMATCH: c2_hat is further than 0.05 from the true c2!")
end if

Forecast 20 steps beyond the observed series.

horizon = 20
future = m.predict(horizon)
print("Last 5 observed values:   {x[n-5:end]}")
print("Next {horizon} forecast values: {future}")

Plot the tail of the observed series plus the forecast.

tail = 100
history_x = (n - tail) to (n - 1)
future_x = n to (n + horizon - 1)
plot(history_x, x[n-tail:end], label="observed", color="blue")
plot(future_x, future, label="AR(2) forecast", color="red")
xlabel("t")
ylabel("x")
title("AR(2) Yule-Walker fit: history and forecast")
legend()
show plot

Results

printed
True coefficients:      c1=0.600, c2=-0.300
Yule-Walker recovered:  c1=0.5848, c2=-0.2929
Fitted mean: 0.0003 (series mean, series is zero-mean by construction)
c1 recovered within the test suite's own 0.05 tolerance.
c2 recovered within the test suite's own 0.05 tolerance.
Last 5 observed values:   [0.161765, 0.006374, 0.121317, 0.089219, -0.043995]
Next 20 forecast values: [-0.051665, -0.017132, 0.005309, 0.008318, 0.003504, -0.000192, -0.000944, -0.000301, ... (20 elements)]
-0.25 -0.20 -0.15 -0.10 -0.05 0 0.05 0.10 0.15 0.20 0.25 0.30 0.35 1.990 1.992 1.994 1.996 1.998 2.000 2.002 ×104 AR(2) Yule-Walker fit: history and forecast t x observed AR(2) forecast
Drawn by running the program above.

The whole program

Runnable as it stands: qu run catalog/qu_ar_model_forecast.qu

# qu_ar_model_forecast.qu -- classical AR(2) autoregressive fitting via the

# Yule-Walker equations, recovering KNOWN generating coefficients from a

# synthetic series, then forecasting beyond the observed data.

#

# The synthetic series uses the exact generating formula from the engine's

# own regression test (`ar_model_recovers_known_ar2_coefficients_from_a_

# synthetic_series` in engine/crates/qu-interp/src/lib.rs):

#   x[t] = c1*x[t-1] + c2*x[t-2] + 0.1*randn(),  c1=0.6, c2=-0.3

# a stationary AR(2) pair (characteristic roots outside the unit circle).

# That test measured (while writing it) that n=20000 recovers c1 within

# ~0.015 of the true 0.6 -- ordinary Yule-Walker finite-sample noise, not a

# fitting bug -- so this demo uses the same n and the same tolerance logic.

#

# Constructs exercised (all real, verified-working builtins):

#   seed(n)               -- fixes the interpreter's persistent RNG stream

#   ar_model(x, order)     -- Yule-Walker AR(p) fit; fields coef/order/mean/

#                              history/fitted/residuals

#   model.predict(n_ahead) -- recursive multi-step-ahead forecast


backend auto

#%% Generate a synthetic AR(2) series with KNOWN coefficients.

seed(12345)
n = 20000
c1 = 0.6
c2 = -0.3
x = zeros(n)
x[0] = randn()
x[1] = randn()
for t = 2 to n - 1
    x[t] = c1 * x[t - 1] + c2 * x[t - 2] + 0.1 * randn()
end for

#%% Fit an AR(2) model and compare recovered coefficients to the truth.

m = ar_model(x, 2)
c1_hat = m.coef[0]
c2_hat = m.coef[1]
print("True coefficients:      c1={c1:.3f}, c2={c2:.3f}")
print("Yule-Walker recovered:  c1={c1_hat:.4f}, c2={c2_hat:.4f}")
print("Fitted mean: {m.mean:.4f} (series mean, series is zero-mean by construction)")

if abs(c1_hat - c1) < 0.05 then
    print("c1 recovered within the test suite's own 0.05 tolerance.")
else
    print("MISMATCH: c1_hat is further than 0.05 from the true c1!")
end if
if abs(c2_hat - c2) < 0.05 then
    print("c2 recovered within the test suite's own 0.05 tolerance.")
else
    print("MISMATCH: c2_hat is further than 0.05 from the true c2!")
end if

#%% Forecast 20 steps beyond the observed series.

horizon = 20
future = m.predict(horizon)
print("Last 5 observed values:   {x[n-5:end]}")
print("Next {horizon} forecast values: {future}")

#%% Plot the tail of the observed series plus the forecast.

tail = 100
history_x = (n - tail) to (n - 1)
future_x = n to (n + horizon - 1)
plot(history_x, x[n-tail:end], label="observed", color="blue")
plot(future_x, future, label="AR(2) forecast", color="red")
xlabel("t")
ylabel("x")
title("AR(2) Yule-Walker fit: history and forecast")
legend()
show plot