Estimating Nugget, Sill & Range Parameters

TL;DR: In scikit-gstat, set use_nugget=True, fit the model, and read V.parameters as [range, sill, nugget]. The partial sill is sill - nugget. Constrain implausible fits with the bounded solver fit_method="trf", and grade spatial dependence with the nugget-to-sill ratio: below 0.25 is strong, 0.25 to 0.75 moderate, above 0.75 weak.

Why This Matters

The nugget, sill, and range are the three numbers that summarize spatial continuity, and they are the payload every kriging system consumes. Reading them correctly is what separates a defensible model from a curve that merely looks plausible, and it is the central skill in working with theoretical variogram models inside the wider variogram modeling and semivariance analysis workflow. A misplaced nugget inflates prediction variance; a range fitted to a sparse tail smooths your surface into meaninglessness. This page shows how to constrain, read, and sanity-check each parameter.

A variogram model decomposes total variance into structured and unstructured parts. With nugget c0c_0, partial sill cc, and total sill c0+cc_0 + c:

γ(h)=c0+cg ⁣(ha),nugget-to-sill ratio=c0c0+c\gamma(h) = c_0 + c \cdot g\!\left(\frac{h}{a}\right), \qquad \text{nugget-to-sill ratio} = \frac{c_0}{c_0 + c}

where g()g(\cdot) is the model shape (spherical, exponential, or Gaussian) and aa is the effective range. The ratio c0/(c0+c)c_0/(c_0+c) is the fraction of variance that is spatially unstructured.

Environment

bash
pip install "scikit-gstat>=1.0" "numpy>=1.23"
python
import numpy as np
from skgstat import Variogram

Step-by-Step Implementation

Step 1 — Fit with a free nugget

python
rng = np.random.default_rng(23)
coords = rng.uniform(0, 600, size=(210, 2))
vals = (
    14.0
    + 5.0 * np.sin(coords[:, 0] / 120.0)
    + rng.normal(0, 1.1, size=len(coords))   # injects nugget-scale noise
)

V = Variogram(coords, vals, model="spherical", n_lags=14,
              maxlag="median", use_nugget=True)

use_nugget=True adds the nugget as a free parameter. Without it, the model is forced through the origin and any measurement error is wrongly absorbed into the sill and range.

Step 2 — Read the fitted parameters

python
effective_range, sill, nugget = V.parameters
partial_sill = sill - nugget

print(f"nugget        : {nugget:6.3f}")
print(f"partial sill  : {partial_sill:6.3f}")
print(f"total sill    : {sill:6.3f}")
print(f"effective range: {effective_range:6.1f}")
print(V.describe())     # full dict including estimator and fit metrics

V.parameters returns [range, sill, nugget] where sill is the total sill. Subtract the nugget to obtain the partial sill, the spatially structured share of variance.

Step 3 — Constrain the fit with a bounded solver

python
# The sample variance is the natural anchor for a plausible sill.
sample_var = float(np.var(vals))
print(f"sample variance: {sample_var:.2f}")

# fit_method="trf" runs a bounded trust-region least-squares fit that
# automatically keeps range, sill and nugget non-negative and within the
# lag domain, instead of the unconstrained Levenberg-Marquardt ("lm") solver.
V = Variogram(
    coords, vals, model="spherical", n_lags=14, maxlag="median",
    use_nugget=True,
    fit_method="trf",
)
print("bounded fit:", np.round(V.parameters, 3))

fit_method="trf" swaps the unconstrained Levenberg-Marquardt solver ("lm") for the trust-region-reflective one, which holds every parameter to a physically sensible, non-negative range bounded by the lag geometry. That alone stops the optimiser from chasing a sparse tail into an absurd range or returning a negative nugget. Compare the fitted sill against sample_var as your plausibility check.

Step 4 — Weight the fit toward well-sampled lags

python
V.fit_sigma = "linear"     # down-weight far, sparse lags in the fit
V.fit()
print("distance-weighted fit:", np.round(V.parameters, 3))

fit_sigma controls how much each experimental point influences the least-squares fit. "linear", "exp", or "sqrt" give near-origin lags more weight, which matters because those lags are the best sampled and most physically meaningful.

Step 5 — Grade spatial dependence with the ratio

python
effective_range, sill, nugget = V.parameters
ratio = nugget / sill
if ratio < 0.25:
    grade = "strong spatial dependence"
elif ratio < 0.75:
    grade = "moderate spatial dependence"
else:
    grade = "weak spatial dependence"

print(f"nugget-to-sill ratio: {ratio:.2f} -> {grade}")

Interpreting the Output

Each parameter carries a distinct physical meaning. The nugget is the semivariance extrapolated to zero lag; it captures measurement error plus variation at scales finer than your sampling interval. The partial sill is the variance that is genuinely spatially structured. The total sill is their sum, and for a stationary variable it should sit close to the sample variance. The range is the distance beyond which pairs are effectively uncorrelated.

Parameter scikit-gstat source Interpretation
Nugget V.parameters[2] Micro-scale variance plus measurement error
Partial sill sill - nugget Spatially structured variance
Total sill V.parameters[1] Full variance plateau ≈ sample variance
Range V.parameters[0] Correlation distance (effective range)
Nugget-to-sill nugget / sill Spatial-dependence strength grade

The nugget-to-sill ratio is the headline diagnostic. A ratio under 0.25 signals strong spatial structure that kriging can exploit sharply; between 0.25 and 0.75 is moderate; above 0.75 warns that most variance is unstructured, and kriging will barely outperform the global mean. A total sill far above the sample variance usually means a residual trend, a cue to revisit stationarity and trend analysis.

Critical Best Practices

Prefer a free nugget over a forced zero

Real sampling carries measurement error and micro-scale variation. use_nugget=True gives that variance a home; forcing the model through the origin smears it into the range and sill and produces overconfident kriging variances. Force a zero nugget only with a strong physical justification.

Anchor the sill to the sample variance

For a stationary variable the fitted total sill should land near np.var(vals). If it drifts far above, suspect a trend; far below, suspect that maxlag truncates the plateau. Fitting with fit_method="trf" keeps the sill non-negative and anchored, so a wild sill points to a data problem rather than a solver one.

Weight the fit toward the origin

The short lags are both the best sampled and the ones kriging weights most heavily. Setting fit_sigma to "linear" or "exp" prevents a noisy far tail from dragging the range and nugget away from the values that actually govern local prediction.

Report the ratio, not just the numbers

The nugget-to-sill ratio communicates spatial-dependence strength in a single figure that is comparable across variables and studies. Always report it alongside the raw parameters so readers can judge whether spatial modelling is even warranted, or whether a simpler spatial regression model would suffice.

Bound the range within the domain

A fitted range longer than your study area is meaningless: you cannot observe correlation beyond the extent you sampled. The trust-region solver bounds the range by the lag geometry set through maxlag, so keep maxlag at or below half the domain and treat a range that pins to that ceiling as a signal to revisit binning.

Troubleshooting

Symptom Likely cause Fix
Negative nugget in the fit Unbounded optimiser overshoot Use fit_method="trf" with a non-negative nugget bound
Total sill far above sample variance Residual trend or non-stationarity Detrend before fitting; check np.var(vals)
Range longer than the domain Fit driven by sparse far lags Lower maxlag; fit with fit_method="trf"
Nugget-to-sill ratio near 1 Weak or no spatial structure Confirm data quality; reconsider spatial modelling
Range or nugget goes negative Unconstrained solver overshoot Set fit_method="trf" for a bounded fit
Sill below the experimental plateau maxlag truncates the curve Increase maxlag toward half the domain

Next Steps

With the parameters interpreted, compare which model family fits your cloud best in fitting spherical, exponential and Gaussian variogram models, and check whether direction changes the range in detecting and modeling geometric anisotropy in Python.


Related

← Back to Theoretical Variogram Models

Frequently Asked Questions

What is the difference between the sill and the partial sill?

The total sill is the semivariance plateau the model reaches at large lags and roughly equals the sample variance of a stationary variable. The partial sill is that plateau minus the nugget, meaning the portion of variance that is spatially structured rather than attributed to nugget effect.

How do I interpret the nugget-to-sill ratio?

The nugget-to-sill ratio grades spatial dependence: below about 0.25 indicates strong spatial structure, between 0.25 and 0.75 moderate structure, and above 0.75 weak structure where most variance is unstructured noise or fine-scale variation your sampling cannot resolve.

Should I always let scikit-gstat fit the nugget freely?

Usually yes: set use_nugget=True so measurement error and micro-scale variation land in the nugget instead of distorting the range and sill. Force a zero nugget only when you have strong physical grounds to believe the variable is perfectly continuous at the origin.