Choosing Lag Bins and Bandwidth for Variograms

TL;DR: Set maxlag to about half the domain diameter, then choose n_lags so every bin holds at least 30 to 50 point pairs. Pick bin_func="even" for regular sampling, "uniform" when density falls off with distance, and "kmeans" for strongly clustered data. Verify with np.bincount(V.lag_groups()) and confirm the nugget, sill, and range stay stable across binnings.

Why This Matters

The lag binning is the one modelling choice in empirical variogram estimation that you make by hand, and it silently determines the nugget, sill, and range that flow into every downstream kriging model across the wider variogram modeling and semivariance analysis workflow. Two analysts can hand the same data to scikit-gstat and read off ranges that differ by a factor of two purely because they binned differently. Understanding the levers, the number of lags, the maximum lag, the binning function, and the tolerance, turns variogram estimation from guesswork into a repeatable procedure.

Each lag class kk aggregates all point pairs whose separation falls in an interval [dk1,dk)[d_{k-1}, d_k), and its semivariance estimate has variance roughly proportional to 1/N(hk)1/N(h_k), where N(hk)N(h_k) is the pair count. This is the statistical reason behind the pairs-per-bin rule: halving the pairs in a bin nearly doubles the uncertainty of its semivariance.

Environment

bash
pip install "scikit-gstat>=1.0" "numpy>=1.23" "scipy>=1.9"
python
import numpy as np
from scipy.spatial.distance import pdist
from skgstat import Variogram

Step-by-Step Implementation

Step 1 — Measure your domain and pairwise distances

python
rng = np.random.default_rng(11)
coords = rng.uniform(0, 800, size=(180, 2))
values = 5 + 0.006 * coords[:, 0] + rng.normal(0, 1.0, size=len(coords))

dists = pdist(coords)                 # all pairwise separation distances
domain_diameter = dists.max()
half_domain = domain_diameter / 2.0
print(f"domain diameter : {domain_diameter:.1f} m")
print(f"suggested maxlag: {half_domain:.1f} m")

The largest reliable lag is bounded by geometry: beyond half the domain, only a handful of far-corner pairs contribute, and their semivariance is meaningless.

Step 2 — Set the maximum lag distance

python
V = Variogram(coords, values, maxlag=half_domain, n_lags=12, model="spherical")

maxlag accepts a float distance, the strings "median" or "mean" (of the pairwise distances), or a fraction in (0, 1) interpreted as a proportion of the maximum distance. maxlag=0.5 is a convenient shorthand for half the maximum separation.

Step 3 — Choose the number of lags against the pairs rule

python
def pairs_per_lag(v):
    return np.bincount(v.lag_groups(), minlength=v.n_lags)

for n in (8, 12, 20, 30):
    V.n_lags = n                       # reassigning triggers a re-fit
    counts = pairs_per_lag(V)
    print(f"n_lags={n:2d}  min pairs/bin={counts.min():4d}  "
          f"range={V.parameters[0]:7.1f}  sill={V.parameters[1]:5.2f}  "
          f"nugget={V.parameters[2]:5.2f}")

Watch the min pairs/bin column. Increase n_lags only while every bin still clears roughly 30 pairs. The printed range, sill, and nugget show directly how binning perturbs the model.

Step 4 — Select a binning function

python
for bf in ("even", "uniform", "kmeans"):
    V.bin_func = bf
    counts = pairs_per_lag(V)
    print(f"bin_func={bf:8s}  pairs/bin={counts}  range={V.parameters[0]:.1f}")

"even" gives equal-width distance bins; "uniform" sizes bins so each holds a similar pair count; "kmeans" clusters the pairwise distances so bin edges follow the natural spacing of the data. Use "uniform" or "kmeans" whenever sampling density changes with distance.

Step 5 — Control lag tolerance and directional bandwidth

python
# Tolerance and bandwidth matter for directional variograms:
from skgstat import DirectionalVariogram

DV = DirectionalVariogram(
    coords, values,
    azimuth=0, tolerance=22.5, bandwidth=60.0,
    n_lags=12, maxlag=half_domain,
)
print("directional range:", DV.parameters[0])

tolerance is the angular half-width (in degrees) of the search cone around each azimuth, and bandwidth caps the perpendicular offset a pair may have from the search line. Wider tolerance and bandwidth include more pairs (more stable, less directional resolution); narrower values sharpen direction at the cost of pair counts.

Step 6 — Confirm parameter stability

python
V.bin_func = "even"
V.n_lags = 15
base = V.parameters
V.bin_func = "uniform"
alt = V.parameters
print("even   :", np.round(base, 2))
print("uniform:", np.round(alt, 2))

If the range, sill, and nugget barely move between sensible binnings, the model is robust. Large swings mean the fit is being driven by binning artefacts rather than by spatial structure.

Interpreting the Output

The three fitted parameters respond to binning in predictable ways. Too few lags oversmooth the near-origin behaviour: the first bin averages over short and medium lags together, pushing the intercept up and inflating the nugget while blurring the range. Too many lags fragment the cloud into sparse, jumpy bins that destabilize the sill. The pairs-per-bin count is your early warning: any bin under 30 pairs is a candidate cause of instability.

Lever scikit-gstat argument Effect if too large Effect if too small
Max lag maxlag Noisy tail, inflated range Truncates the sill, range underestimated
Lag count n_lags Sparse, jumpy bins Oversmoothed nugget and range
Binning bin_func Mismatched to point spacing
Tolerance tolerance Direction washed out Too few pairs per direction
Bandwidth bandwidth Cross-direction contamination Sparse directional bins

Critical Best Practices

Treat 30 pairs as a floor, not a target

The 30-pairs-per-bin rule is a minimum for a usable estimate; 50 or more is what you want for anything you will publish or put into production. Always print np.bincount(V.lag_groups()) and inspect the smallest bin before trusting the fit.

Never bin past half the domain

Semivariance beyond half the domain diameter rests on the few point pairs spanning opposite corners. Those estimates are dominated by sampling noise and, if included, drag the fitted range upward. Cap maxlag and let the well-sampled short lags carry the model.

Match bin_func to your sampling geometry

Regular grids and evenly spread samples suit "even". Transect or nested designs, where nearby pairs vastly outnumber distant ones, are better served by "uniform" or "kmeans", which prevent the crowded short-lag bins from dominating.

Tune tolerance and bandwidth together

When you move to directional variograms, a narrow tolerance with a generous bandwidth, or the reverse, can leave far lags empty. Adjust both while watching pair counts, so each direction retains enough pairs to be meaningful. This becomes central when you study anisotropy and directional variograms.

Report your binning choices

Because binning is a modelling decision, record n_lags, maxlag, and bin_func alongside the fitted parameters. A range without its binning context is not reproducible.

Troubleshooting

Symptom Likely cause Fix
Fitted range jumps between runs Sparse far bins from too many lags Lower n_lags; enforce 30+ pairs per bin
Nugget larger than expected Too few lags oversmoothing the origin Increase n_lags and shrink the first bin
Empty or single-pair bins maxlag too large or clustered data Reduce maxlag; try bin_func="kmeans"
Sill unstable across binnings Outliers plus small bins Use a robust estimator and raise pair counts
Directional bins nearly empty Tolerance or bandwidth too narrow Widen tolerance/bandwidth or use fewer lags
maxlag fraction ignored Passed a value above 1 as a fraction Use a value in (0, 1) or an explicit distance

Next Steps

Once your binning is stable, carry the experimental cloud into fitting spherical, exponential and Gaussian variogram models, or revisit the estimation mechanics in fitting empirical variograms with SciKit-GStat.


Related

← Back to Empirical Variogram Estimation

Frequently Asked Questions

How many point pairs should each variogram lag contain?

Aim for at least 30 pairs per lag class, and prefer 50 or more for a publication-grade fit. Below roughly 30 pairs the semivariance estimate for that lag has too much sampling variance to trust, which is why the far tail of a variogram is usually the least reliable part.

What is the difference between bin_func ‘even’ and ‘uniform’ in scikit-gstat?

"even" splits the lag axis into equal-width distance bins, so bins near a clustered scale may hold very different pair counts. "uniform" instead sizes the bins so each holds a similar number of pairs, which stabilizes the estimate when sampling density varies with distance.

How does the number of lags change the nugget, sill and range?

Too few lags oversmooth the short-lag behaviour and inflate the apparent nugget while blurring the range. Too many lags produce sparse, noisy bins that destabilize the sill. The right count keeps each bin populated while resolving the rise from nugget to sill.