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 aggregates all point pairs whose separation falls in an interval , and its semivariance estimate has variance roughly proportional to , where 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
pip install "scikit-gstat>=1.0" "numpy>=1.23" "scipy>=1.9"
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
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
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
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
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
# 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
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
- Empirical Variogram Estimation — the estimation stage this tuning belongs to
- Fitting Empirical Variograms with SciKit-GStat — the end-to-end Variogram object walkthrough
- Detecting & Modeling Geometric Anisotropy in Python — where tolerance and bandwidth matter most
← 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.