Skip to content

Predicting new rows

result.predict(new_data) is available on GaussianResult, LaplaceResult, and INLAResult (i.e. every result produced by LGM.fit). It scores rows that were not passed to fit by rebuilding their design and reusing the already-fitted latent posterior — no refit. It returns an immutable Prediction with predictive_mean, predictive_variance, fitted_mean, and keys (new_data's own index), plus .to_frame(), a DataFrame indexed like new_data with columns predictive_mean, predictive_sd, and fitted_mean.

new_data must carry every column the fitted model reads — each effect's index column, every variable used in a Fixed formula, and the offset column when the model declares one — but not the response column.

Which array do I want?

Compare against observed responses (counts, 0/1, y) with fitted_mean — it is on the response scale (exp of the linear predictor for a Poisson log link, logit⁻¹ for Bernoulli, identity for Gaussian). predictive_mean/predictive_variance are on the linear-predictor (η) scale. See "scale conventions" below.

import pandas as pd
from pylgm import Fixed, Gaussian, IID, LGM

frame = pd.DataFrame({
    "claims": [0.5, 1.5, 2.5, 3.5],
    "x": [1.0, 2.0, 3.0, 4.0],
    "region": ["a", "b", "a", "b"],
})
model = LGM(
    response="claims",
    predictor=Fixed("1 + x") + IID("region_effect", index="region", precision=2.0),
    likelihood=Gaussian(sigma=0.5),
)
result = model.fit(frame)

# new_data carries "x" (the Fixed formula variable) and "region" (the IID
# effect's index), but not "claims" (the response).
new_scenarios = pd.DataFrame({"x": [10.0, -3.0], "region": ["b", "a"]})
prediction = result.predict(new_scenarios)
print(prediction.to_frame())
#    predictive_mean  predictive_sd  fitted_mean
# 0         9.499999       1.764396     9.499999
# 1        -3.499999       1.288687    -3.499999

predict reuses the fitted posterior; it cannot create a new latent column. An index level absent from the fitted model — a region predict never saw at fit time — raises ValueError naming both the offending block and the unrecognized level(s), rather than silently falling back to a prior or a zero contribution. The one exception is a spatial effect: it is keyed on graph nodes, so a node present in the effect's graph but not referenced by any fitted row already has a posterior and predicts without error.

Forecasting genuinely new levels — a future time point, a region never seen at all — is a different operation, and it already worked before predict existed: include those rows at fit time with a NaN response. They contribute no likelihood but still receive a latent column (and, for a structured effect such as RW1, an extrapolated posterior) plus a full prediction, right alongside the observed rows.

import numpy as np
import pandas as pd
from pylgm import Fixed, Gaussian, IID, LGM, RW1

n_hist = 30
history = pd.DataFrame({
    "y": 1.0 + 0.2 * np.sin(np.arange(n_hist) / 3.0),
    "region": (["a", "b"] * (n_hist // 2))[:n_hist],
    "t": range(n_hist),
})
# These 4 future periods never appeared at fit time. Give them a NaN
# response instead of calling predict() on them.
future = pd.DataFrame({
    "y": [np.nan, np.nan, np.nan, np.nan],
    "region": ["a", "b", "a", "b"],
    "t": [n_hist, n_hist + 1, n_hist + 2, n_hist + 3],
})
frame = pd.concat([history, future], ignore_index=True)

model = LGM(
    response="y",
    predictor=Fixed("1") + IID("region", index="region", precision=2.0)
    + RW1("trend", index="t", precision=2.0),
    likelihood=Gaussian(sigma=0.2),
)
result = model.fit(frame)

# The last 4 rows are the future periods; predictive_mean/_variance already
# cover them, in the caller's row order (same arrays fit() always returns).
print(np.round(result.predictive_mean[-4:], 3))    # [0.957 0.957 0.957 0.957]
print(np.round(result.predictive_variance[-4:], 3))  # [0.557 1.037 1.557 2.037]

An RW1/RW2 forecast extrapolates flat from the last fitted level — these examples show it exactly, because the region effect nets out to ~0 by symmetry — and its variance strictly increases with each additional step ahead, converging toward growth of 1/τ per step as the fitted history grows long relative to the forecast horizon (the finite-sample deviation comes from the effect's sum-to-zero identifiability constraint, which couples the whole chain including the unobserved tail).

need use
new covariate values / scenarios on known index levels result.predict(new_data)
new time points, regions, or groups NaN-response rows at fit time

Scale conventions

predictive_variance and fitted_mean follow exactly the same conventions predict mirrors from the fit-row outputs: predictive_variance is the linear-predictor variance Var(eta), for every engine — see "The predictive_variance convention" above for the Gaussian reconstruction of the response-scale value. fitted_mean is identity for Gaussian, the exact lognormal expectation exp(μ + σ²_η/2) for the Poisson log link, and the documented point estimate logit⁻¹(μ) (ignoring linear-predictor variance) for the Bernoulli logit link — see "Non-Gaussian likelihoods (Laplace)" above.

One exception is worth knowing about. Under hyperparameters="integrate" with a non-linear link, the integrated result.fitted_mean mixes the transformed per-hyperparameter values (Σₖ wₖ·g(μₖ, σ²ₖ)), while predict transforms the integrated moments (g(Σₖ wₖμₖ, Var)). Jensen's inequality separates the two by an amount that grows with the hyperparameter uncertainty, so predict().fitted_mean is a moment-matched approximation of result.fitted_mean there rather than an exact match (reproducing it exactly would mean retaining every grid point's latent covariance). predictive_mean and predictive_variance are exact, as is fitted_mean for the identity link and for all plug-in and empirical-Bayes fits.

predict works on a result fitted from either Pandas or a Spark DataFrame, but new_data itself must always be a Pandas DataFrame — Spark new_data is not supported. Not shipped: a prior-based fallback for unseen levels, predictive quantiles or simulation, response-scale predictive variance for non-Gaussian links, and an automatic future-frame construction helper (the NaN-response rows above are built by hand).