Ordinary vs Universal Kriging: Which to Use
Use ordinary kriging when the mean is constant but unknown across your area; use universal kriging (kriging with drift) when a systematic large-scale trend is present — temperature falling with elevation, a plume from a point source. Decide by testing for a trend: regress the target on coordinates or covariates. If the drift is significant, model it with
UniversalKriging(..., drift_terms=['regional_linear']); otherwiseOrdinaryKrigingis simpler and just as good.
Why This Matters
Choosing the wrong kriging variant quietly corrupts a surface. Fit ordinary kriging to data with a real trend and the variogram never reaches a sill, the weights misbehave, and predictions are biased in exactly the regions you care about. Fit universal kriging to trend-free data and you add parameters that overfit and inflate variance. This decision guide sits within the Ordinary & Universal Kriging workflow and the wider Kriging, Interpolation & Surface Generation Techniques pillar, and it gives you a test-driven way to pick — not a coin flip.
The Core Distinction
Both estimators predict an unsampled value as a weighted linear combination of neighbours, . They differ in what they assume about the mean.
Ordinary kriging assumes a constant but unknown mean:
with a single unbiasedness constraint . Universal kriging replaces the constant mean with a deterministic drift expressed as a sum of basis functions :
so the mean varies smoothly across the domain. The are typically low-order polynomials of the coordinates (regional linear or quadratic drift) or external covariates (external drift, KED). Universal kriging adds one unbiasedness constraint per drift term, solved jointly with the weights inside the kriging system.
The residual must be second-order stationary in both cases. Ordinary kriging assumes that of the raw variable; universal kriging assumes it only of the detrended residual — which is why a trend that breaks ordinary kriging can be absorbed cleanly by universal kriging.
Comparison Table
| Criterion | Ordinary kriging | Universal kriging (KED) |
|---|---|---|
| Mean assumption | Constant, unknown | Deterministic drift (trend) |
| Best when | No large-scale trend | Clear coordinate/covariate gradient |
| Extra parameters | None (just variogram) | Drift coefficients per term |
| Variogram fitted on | Raw values | Detrended residuals (ideally) |
| Extrapolation behaviour | Reverts to sample mean | Follows the drift beyond samples |
| Overfitting risk | Low | Higher if drift is spurious |
| Covariate support | No | Yes (external drift) |
| PyKrige class | OrdinaryKriging |
UniversalKriging |
| Typical domains | Homogeneous fields, soil pockets | Elevation-driven climate, plumes |
A subtle trap: universal kriging should fit its variogram on the residuals after removing the drift, not on the raw data. PyKrige’s automatic fit does not always make that explicit, which is one reason many practitioners prefer the two-stage transparency of regression kriging when covariates are involved.
A Decision Framework
The workflow is: test, choose, validate. Never pick on intuition alone — a mild trend that looks obvious on a map may not improve out-of-sample predictions, and the cross-validation step is what settles it.
Running the Trend Test
Before choosing, quantify the drift. Regress the target on the coordinates; a significant slope or a meaningful signals a trend that ordinary kriging cannot represent.
import numpy as np
import statsmodels.api as sm
# x, y: projected coordinates (metres); z: target values
X = sm.add_constant(np.column_stack([x, y])) # intercept + linear drift
ols = sm.OLS(z, X).fit()
print(ols.summary().tables[1]) # coefficient significance
print(f"Trend R^2: {ols.rsquared:.3f}")
trend_present = ols.f_pvalue < 0.05 and ols.rsquared > 0.10
print("Use universal kriging" if trend_present else "Ordinary kriging is fine")
A low and non-significant coordinate terms point to ordinary kriging. A clear gradient points to universal kriging. Confirm the detrended residuals look stationary — the constant-mean-of-residuals assumption — using the methods in Testing for Second-Order Stationarity in Python.
Code: Ordinary vs Universal Kriging in PyKrige
The two estimators share almost all their configuration; universal kriging just adds drift_terms.
from pykrige.ok import OrdinaryKriging
from pykrige.uk import UniversalKriging
gridx = np.linspace(x.min(), x.max(), 150)
gridy = np.linspace(y.min(), y.max(), 150)
# --- Ordinary kriging: constant unknown mean ---
ok = OrdinaryKriging(
x, y, z,
variogram_model="spherical",
nlags=15, weight=True, verbose=False,
)
z_ok, ss_ok = ok.execute("grid", gridx, gridy)
# --- Universal kriging: linear drift in the coordinates ---
uk = UniversalKriging(
x, y, z,
variogram_model="spherical",
drift_terms=["regional_linear"], # first-order polynomial trend in x, y
nlags=15, weight=True, verbose=False,
)
z_uk, ss_uk = uk.execute("grid", gridx, gridy)
print(f"OK surface range: {z_ok.min():.2f} to {z_ok.max():.2f}")
print(f"UK surface range: {z_uk.min():.2f} to {z_uk.max():.2f}")
For kriging with external drift — a covariate raster explaining the trend — pass the covariate instead of a coordinate polynomial:
# external_drift is a 2D array on (gridy, gridx); external_drift_x/y are its axes
uk_ked = UniversalKriging(
x, y, z,
variogram_model="spherical",
drift_terms=["external_Z"],
external_drift=covariate_grid, # covariate sampled on the grid
external_drift_x=grid_cov_x,
external_drift_y=grid_cov_y,
verbose=False,
)
z_ked, ss_ked = uk_ked.execute("grid", gridx, gridy)
When the trend is driven by covariates rather than raw coordinates, the two-stage regression kriging approach is often clearer and lets you use non-linear trend models.
Validating the Choice
The decisive test is out-of-sample error. Cross-validate both estimators and keep the drift only if it lowers held-out RMSE.
from sklearn.model_selection import LeaveOneOut
def loocv(estimator_cls, x, y, z, **kw):
loo, errs = LeaveOneOut(), []
idx = np.arange(len(z))
for i in idx:
tr = idx != i
est = estimator_cls(x[tr], y[tr], z[tr], variogram_model="spherical",
nlags=12, weight=True, verbose=False, **kw)
pred, _ = est.execute("points", x[i:i+1], y[i:i+1])
errs.append(float(pred[0]) - z[i])
return np.sqrt(np.mean(np.square(errs)))
rmse_ok = loocv(OrdinaryKriging, x, y, z)
rmse_uk = loocv(UniversalKriging, x, y, z, drift_terms=["regional_linear"])
print(f"OK RMSE={rmse_ok:.4f} | UK RMSE={rmse_uk:.4f}")
If universal kriging does not beat ordinary kriging in cross-validation, the trend was not worth modelling — prefer the simpler estimator.
Interpreting the Comparison
- UK wins clearly: a genuine drift exists and is well captured. Confirm the detrended residual variogram reaches a sill.
- OK and UK tie: the trend is weak; choose ordinary kriging for simplicity and stability.
- UK worse than OK: the drift is spurious or overfitted, or the residuals are non-stationary. Revisit the trend test and residual diagnostics.
- Variogram never plateaus under OK: a hallmark of an unmodelled trend — move to universal kriging.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| OK variogram keeps rising, no sill | Unmodelled large-scale trend | Switch to universal kriging or detrend first |
| UK adds parameters but RMSE worsens | Spurious/overfitted drift | Drop drift terms; use ordinary kriging |
external_Z drift errors in PyKrige |
Covariate grid axes not aligned with external_drift_x/y |
Match the drift array shape and axes to the covariate grid |
| UK residuals still show a gradient | Drift order too low | Add regional_quadratic or a covariate drift term |
| Both estimators biased at edges | Extrapolation beyond sample hull | Clip the grid to the convex hull of samples |
| Trend test insignificant but map looks trended | Trend is short-range, not global | Treat as autocorrelation; ordinary kriging is appropriate |
Next Steps
For the mechanics of running each estimator end to end, see Step-by-Step Ordinary Kriging with PyKrige. When the trend comes from covariates and you want a flexible, inspectable model, move to Regression Kriging.
Frequently Asked Questions
When should I use universal kriging instead of ordinary kriging?
Use universal kriging when the variable has a systematic large-scale trend — a spatial drift — that a constant mean cannot represent, such as temperature falling with elevation or a pollutant gradient from a source. Ordinary kriging assumes the mean is constant (though unknown) across the domain; a real trend violates that assumption, biases the variogram, and inflates prediction error. If a regression of the target on coordinates or covariates is significant, universal kriging (or regression kriging) is the better choice.
How do I test whether my data has a trend?
Fit an ordinary least squares regression of the target on the spatial coordinates (and any candidate covariates) and check the coefficients’ significance and the R-squared. A significant coordinate term or a clear directional gradient in a scatter of value against easting/northing indicates drift. You can also inspect whether the variogram keeps rising without reaching a sill, which is a symptom of an unmodelled trend.
What are drift_terms in PyKrige UniversalKriging?
drift_terms tells PyKrige how to model the trend. ‘regional_linear’ fits a linear function of the x and y coordinates; ‘point_logarithmic’ adds logarithmic point drift; ‘external_Z’ uses an external covariate raster (kriging with external drift); and ‘specified’/‘functional’ accept custom drift arrays or functions. You pass the corresponding data — for example external_drift and its grid — alongside the drift_terms list.
Related
- Step-by-Step Ordinary Kriging with PyKrige — full ordinary kriging implementation
- Regression Kriging — the two-stage alternative when covariates drive the trend
- Testing for Second-Order Stationarity in Python — verify the assumptions behind each estimator
← Back to Ordinary & Universal Kriging