EGS 2405 · Jomo Kenyatta University of Agriculture and Technology

Geostatistics

A course reader for fourth-year Geomatic Engineering and Geospatial Information Systems.

11 teaching weeks 2 h lecture + 2 h practical Python · geopandas · scikit-gstat · pykrige Assessment 30 / 70 Edition 1.0 · Sept 2026

How to use this reader

Each chapter covers one teaching week and has the same five parts: learning outcomes, the reading itself, a practical brief, self-test questions, and further reading. The reading is the lecture in written form — come to the lecture having read it, and the two hours can be spent on the parts that are actually difficult rather than on transcription.

Practicals are written in two tiers. The Core task is what everyone must complete; a scaffolded notebook is supplied so that the geostatistics is the exercise and the file handling is not. The Extension is for those who finish early or who already write Python fluently — it is never assessed separately, but it is where the interesting work is.

Before week 1

Work through Appendix A. It is a self-check, not a test: twelve short Python tasks that tell you — privately — whether you need to spend a weekend on revision before the practicals start. You have all met Python in GIS programming, but the gap between "I have seen a for loop" and "I can write a function that returns a NumPy array" is exactly the gap that makes week 3 painful. Find out now, while it is cheap to fix.

One habit is worth forming immediately. Every figure you produce in this unit should be able to answer the question "how confident are you, and how do you know?" A geostatistician who cannot answer that has produced a picture, not an estimate. The whole unit is arranged around getting you to the point where you can answer it.

Software and data setup

Everything runs in Python. Install with conda (recommended, because it handles the GDAL dependency chain) or pip into a fresh virtual environment:

# conda — recommended
conda create -n geostat python=3.11
conda activate geostat
conda install -c conda-forge geopandas rasterio matplotlib scipy scikit-learn jupyterlab
pip install scikit-gstat pykrige

# check it works
python -c "import geopandas, skgstat, pykrige; print('ok')"

If the lab machines will not cooperate, everything in this unit also runs in Google Colab with !pip install scikit-gstat pykrige geopandas in the first cell. Colab is the fallback, not the default: you should leave this unit able to set up a working environment yourself, because no employer will do it for you.

PackageWhat we use it for
geopandasReading point data, coordinate reference systems, spatial joins
skgstatEmpirical variograms, model fitting, directional variograms
pykrigeOrdinary, universal and regression kriging
numpy / scipyLinear algebra — you will solve the kriging system directly in week 5
scikit-learnRandom forest and cross-validation machinery in week 10

Datasets are listed in Appendix C. Two run through the whole unit: the Meuse floodplain heavy-metal dataset, because the entire literature uses it and you should be able to read that literature; and a Kenyan soil dataset drawn from the Africa Soil Information Service, because a method you have only ever applied to Dutch cadmium is a method you do not yet own.

Notation

Geostatistics has an unusually inconsistent notation across textbooks. This reader uses the following throughout, and where a source uses something different it is flagged.

SymbolMeaning
xA location — a coordinate pair, not a variable value
z(x)The observed value at location x
Z(x)The random function of which z(x) is one realisation
h, hThe lag vector separating two locations, and its length
γ(h)Semivariance at lag h — the variogram
C(h)Covariance at lag h
N(h)Number of point pairs separated by lag h
c0Nugget — variance not resolved at the shortest sampled lag
c1Partial sill — the structured component of variance
aRange — lag beyond which there is no spatial dependence
λiKriging weight assigned to sample i
ψLagrange multiplier in the ordinary kriging system
σ²OKOrdinary kriging variance
ẑ(x0)The estimate at an unsampled location x0
01

Week one

Why spatial data breaks classical statistics

By the end of this week you should be able to
  • State what a regionalised variable is and why it needs a theory of its own
  • Distinguish strict, second-order and intrinsic stationarity, and say what each assumption buys
  • Explain the support of a measurement and why changing it changes the statistics
  • Recognise the modifiable areal unit problem in a real map
  • Load and project a point dataset in GeoPandas and produce a first exploratory map

The problem, stated plainly

You have 155 soil samples from a 4 km × 4 km block in Kiambu. You need a map of soil organic carbon for the whole block. Classical statistics — the statistics you learned in second year — will give you a mean and a standard deviation, and both are useless for the question, because they assume your 155 observations are independent draws from a population. They are not. Two samples 20 m apart are telling you nearly the same thing. Two samples 3 km apart are telling you different things. That structure is not a nuisance to be corrected for; it is the entire signal you need in order to predict at the 3,999,845 locations you did not sample.

This is the founding observation of geostatistics, made by Georges Matheron in the early 1960s while working on the estimation of ore grades in French mines, building on the empirical work of Danie Krige in the South African gold fields. Their problem was commercial and unforgiving: mining a block of ore that turns out to be waste costs money, and so does leaving payable ore in the ground. They needed estimates with a stated error, not just estimates.

Regionalised variable. A variable distributed in space whose values show some structure — neighbouring values are related — but which is too irregular to describe with a deterministic function. Soil carbon, rainfall, ore grade, groundwater fluoride, air pollution: all regionalised.

The insight is to treat such a variable as both deterministic and random. It has structure, so we can predict; it is irregular, so the prediction has error. Neither a purely deterministic model (a polynomial surface) nor a purely statistical one (an i.i.d. sample) captures both.

The random function, and the one-realisation problem

Formally we treat the value at every location x as a random variable Z(x). The collection of all of them across the region is a random function. Our data — the 155 measurements — are one realisation of that random function, denoted z(x1), …, z(x155).

There is an immediate and serious problem with this. In ordinary statistics you estimate the properties of a random variable by taking many independent draws from it. Here we have one draw from Z(x1), one from Z(x2), and so on: one realisation of the whole field. There is only one Kiambu, and we sampled it once. How can you estimate a variance from a single observation?

The answer is to trade an assumption for the data we lack. If we are willing to assume that the statistical properties are the same everywhere in the region, then observations at different locations can substitute for repeated observations at the same location. That assumption is stationarity, and it is the price of admission to the entire subject.

Three grades of stationarity

Stationarity comes in strengths. We want the weakest one that still lets us do the work, because every assumption we make is a claim about reality that could be wrong.

AssumptionWhat it claimsVerdict
StrictThe entire joint distribution is invariant under translation. Everything about the statistics is the same everywhere.Far too strong; unverifiable
Second-orderThe mean is constant, and the covariance between two points depends only on the vector separating them, not on where they are.Usable, but requires a finite variance
IntrinsicThe mean of the difference between two points is zero, and the variance of that difference depends only on the separation.Weakest; what we actually assume

Intrinsic stationarity is weaker in a way that matters. It says nothing about the variance of Z itself — which may not even be finite — only about the variance of differences. Many real phenomena drift upward indefinitely across a region and have no meaningful global variance, but still have well-behaved local differences. This is precisely why the variogram, which is built from differences, became the central tool of the subject rather than the covariance function.

Stated formally, the intrinsic hypothesis is two conditions:

E[Z(x) − Z(x+h)] = 0 Var[Z(x) − Z(x+h)] = 2γ(h)

The second line defines the variogram 2γ(h). The factor of two is why γ is called the semivariogram — a distinction most practitioners now ignore, using "variogram" for γ. This reader does the same.

Where students go wrong

Stationarity is a property you assume about a model, not a property you test in the data. You cannot prove your field is stationary. What you can do is look for evidence that the assumption is untenable — an obvious trend, a variance that visibly changes across the region — and then either restrict the region, remove the trend (week 6), or admit the method does not apply. Deciding this is a judgement call, and it is one of the things you are being trained to make.

Support, and why it changes everything

Support is the size, shape and orientation of the volume on which a measurement is made. A soil sample taken with a 5 cm auger has a different support from a bulked sample composited across a 10 m plot, which has a different support again from a 30 m Landsat pixel.

Support is not a technicality. As support increases, the variance of the measurements decreases — averaging over a larger volume smooths out small-scale variation. A dataset of 5 cm cores and a dataset of 10 m composites from the same field will have genuinely different histograms, different variances, and different variograms, and neither is wrong. This is why mining engineers estimate block grades rather than point grades: the mine extracts blocks, so the block is the relevant support, and the variance of block grades is much lower than the variance of drill-core grades. We return to this in week 6 under change of support.

MAUP — the modifiable areal unit problem. When point data are aggregated into areal units, the results depend on both the size of the units (the scale effect) and where the boundaries are drawn (the zonation effect). Redistricting a county into different wards can change a correlation from strong positive to weak negative without a single underlying value changing.

Every choropleth map of county-level statistics is a MAUP artefact to some degree. Geostatistics does not solve MAUP, but working with point support and explicit change-of-support operations at least makes the problem visible rather than hidden.

What sampling design does to what follows

You will usually inherit a dataset rather than design the survey, but you must be able to read what the design has done to your data. Four patterns recur:

The last point is worth holding on to. A survey designed to estimate a mean and a survey designed to estimate a variogram are different surveys. If you are ever asked to design one, ask which it is.

Practical 1 — Environment, data, and a first look
Core everyone
  1. Complete the environment setup above and confirm all three imports succeed.
  2. Sit the twenty-minute Python diagnostic. It is not graded and does not appear in your record; it exists so that both you and I know where the class is starting from.
  3. Load the Meuse dataset into a GeoDataFrame. Print the CRS, the number of records, and the summary statistics of the zinc column.
  4. Load the Kiambu soil samples (supplied as CSV with longitude/latitude columns). Build a GeoDataFrame, set the CRS to EPSG:4326, then reproject to EPSG:32737 — UTM zone 37S. Explain in one sentence why the reprojection is not optional.
  5. Produce a bubble map: points sized and coloured by soil organic carbon, over county boundaries. Write two sentences on any spatial pattern you can see by eye.
Extension if you have time
  1. Compute the mean nearest-neighbour distance for the Kiambu samples, and compare it to the value expected under complete spatial randomness for the same intensity. Is the survey clustered, random, or regular?
  2. Take the Meuse points and aggregate zinc onto 100 m, 250 m and 500 m grids. Plot the three histograms together. You have just demonstrated the support effect — describe what happened to the variance and say why.
import geopandas as gpd
import pandas as pd
import matplotlib.pyplot as plt

# CSV with columns: lon, lat, soc  (soil organic carbon, g/kg)
df = pd.read_csv("data/kiambu_soc.csv")

gdf = gpd.GeoDataFrame(
    df,
    geometry=gpd.points_from_xy(df.lon, df.lat),
    crs="EPSG:4326",          # degrees — NOT a distance unit
)
gdf = gdf.to_crs("EPSG:32737")   # UTM 37S — metres

gdf["X"] = gdf.geometry.x
gdf["Y"] = gdf.geometry.y

ax = gdf.plot(column="soc", markersize=gdf.soc * 3,
              legend=True, cmap="viridis", figsize=(7, 7))
ax.set_title("Soil organic carbon, Kiambu (g/kg)")
ax.set_axis_off()
plt.show()
The single most common error in this unit

Computing distances in degrees. Every variogram, every range, every kriging neighbourhood in this unit is a distance, and a degree is not a distance — one degree of longitude is about 111 km at the equator and shrinks to nothing at the poles. If you leave your data in EPSG:4326, everything downstream is quietly, plausibly wrong. Project to UTM first, every single time. For Kenya that is EPSG:32736 (zone 36S, west of 36°E) or EPSG:32737 (zone 37S, east of 36°E).

Self-test
  1. Why can we not simply treat 155 soil samples as an independent random sample and use classical confidence intervals?
  2. Give a phenomenon that plausibly satisfies intrinsic but not second-order stationarity, and say why.
  3. Two teams sample the same field: one takes 5 cm cores, the other bulks ten cores into a composite per 10 m plot. Which dataset has the higher variance, and why?
  4. A colleague reports that soil carbon correlates with rainfall at r = 0.81 at ward level and r = 0.34 at point level. Has anyone made an error?
  5. You may take 100 samples and you must estimate a variogram. Sketch your design and defend it in three sentences.

Reading. Webster & Oliver, Geostatistics for Environmental Scientists, ch. 2–3. Isaaks & Srivastava, ch. 1. For the historical account, Cressie (1990), "The origins of kriging", Mathematical Geology 22(3).

02

Week two

Exploratory spatial data analysis

By the end of this week you should be able to
  • Diagnose skew and decide whether a transform is warranted
  • Distinguish an outlier from a genuine extreme value using spatial context
  • Detect and quantify global trend, and decide whether to remove it
  • Recognise preferential sampling and correct for it by declustering
  • Produce a defensible one-page data summary before any modelling

Do not skip this week

There is a strong temptation, once you know how to fit a variogram, to load the data and fit one. Resist it. Almost every failed geostatistical analysis fails here, in the hour before any modelling, because someone did not look at their data. The variogram is a summary statistic and, like every summary statistic, it can be dominated by a handful of values that should never have entered the calculation.

Distributions and transforms

Kriging is a linear estimator and, strictly, it does not require normality. But two things make the distribution matter anyway. First, the variogram estimator is a squared difference, so it is highly sensitive to extreme values — a single erroneous outlier can inflate the entire variogram. Second, the simulation methods in week 8, and the confidence intervals you will want to attach to your predictions, do assume a Gaussian distribution.

So: plot the histogram, plot the Q–Q plot, compute the skewness. As a working rule, a coefficient of skewness beyond about ±1 is worth acting on. Two transforms are standard:

Transform honestly

A transform is a modelling decision that must be reported, and the back-transformation must be stated. "We log-transformed, kriged, and exponentiated" is an incomplete method statement, because it does not say whether the result is a mean or a median, and the difference for a skewed variable can be tens of percent. If you cannot explain what your back-transformed number is an estimate of, do not transform.

Outliers, and the spatial version of the question

Classical outlier detection asks whether a value is extreme relative to the distribution. Spatial data lets you ask a better question: is a value extreme relative to its neighbours? A cadmium concentration of 18 mg/kg might be perfectly ordinary in the distribution and yet be a clear error if every sample within 200 m reads below 2.

The practical tool is the local deviation: for each point, compare its value to the mean of its k nearest neighbours, and standardise. Points with a large standardised deviation are candidates for investigation. Note the word: investigation, not deletion. Delete a value only when you have a reason external to the statistics — a transcription error, a known contaminated sample, a GPS fix that puts the point in a lake. An extreme value that is real is often the most important observation in the dataset, and a hotspot you deleted for tidiness is a hotspot you failed to map.

Trend, and the decision it forces

A trend (or drift) is a systematic change in the mean across the region — soil depth increasing downslope, temperature decreasing with altitude, contamination decreasing with distance from a smelter. Trend violates the constant-mean assumption, and if you ignore it the variogram will not reach a sill: it climbs indefinitely, because points far apart differ systematically rather than randomly.

Detect it by regressing the variable on the coordinates, and by looking at the shape of the empirical variogram at long lags. Then choose:

  1. Restrict the region. If the trend is a large-scale feature and your area of interest is small, work locally where the mean is plausibly constant. The cheapest solution when it is available.
  2. Model the trend and krige the residuals. Fit the trend (as a function of coordinates, or better of a covariate such as elevation), work with what is left over, then add the trend back. This is universal kriging and regression kriging — week 6.
  3. Ignore it. Occasionally defensible if the trend is weak relative to the local variation and your predictions are all short-range interpolations. Say so explicitly if you do.

Preferential sampling and declustering

Real surveys are rarely evenly spread. Geologists drill densely where the ore is; environmental agencies sample intensively where they suspect contamination; field teams sample near roads. The result is that the naive sample mean is biased toward whatever the clustered areas contain — usually the high values, because that is why people clustered there.

Cell declustering is the standard correction. Lay a grid over the region; each occupied cell receives a total weight of one, shared equally among the points inside it. A point alone in its cell gets weight 1; each of ten points crowded into one cell gets weight 0.1. The declustered mean is then the weighted mean.

mdc = 1ncells ∑j 1nj ∑i ∈ cell j z(xi)

where nj is the number of samples in occupied cell j, and ncells the number of occupied cells. Cell size is chosen by plotting the declustered mean against cell size and taking the size at which it is a minimum (for high-value clustering) or a maximum (for low-value clustering).

Practical 2 — A defensible data summary
Core everyone
  1. For Meuse zinc and for Kiambu soil carbon: histogram, Q–Q plot, skewness, and the five-number summary.
  2. Decide, for each variable, whether to transform and which transform. Write one sentence of justification per variable. Implement the transform and re-plot.
  3. Compute the standardised local deviation using the 8 nearest neighbours. Map the five most extreme points. For each, state whether you would investigate, retain, or remove, and why.
  4. Regress each variable on X and Y. Report R² and the fitted coefficients. Map the residuals. Is there a trend worth removing?
  5. Perform cell declustering across a range of cell sizes. Plot declustered mean against cell size, choose a cell size, and report both the naive and the declustered mean.
  6. Write a one-page data summary. This is the deliverable — the plots are supporting evidence, not the product.
Extension if you have time
  1. Implement normal-score transformation and its inverse yourself, using scipy.stats.norm.ppf and interpolation. Verify that the round trip returns the original values.
  2. Simulate a field with a known strong trend, sample it preferentially toward the high end, and show numerically how much bias declustering recovers.
import numpy as np
from scipy import stats
from sklearn.neighbors import NearestNeighbors

def local_deviation(coords, values, k=8):
    """Standardised difference between each value and its k nearest neighbours."""
    nn = NearestNeighbors(n_neighbors=k + 1).fit(coords)
    _, idx = nn.kneighbors(coords)
    nbr_mean = values[idx[:, 1:]].mean(axis=1)   # column 0 is the point itself
    resid = values - nbr_mean
    return resid / resid.std()

coords = gdf[["X", "Y"]].values
gdf["ld"] = local_deviation(coords, gdf.soc.values)

print("skewness:", stats.skew(gdf.soc))
print(gdf.reindex(gdf.ld.abs().sort_values(ascending=False).index).head(5)
         [["X", "Y", "soc", "ld"]])

# --- cell declustering -------------------------------------------------
def declustered_mean(coords, values, cell):
    keys = np.floor(coords / cell).astype(int)
    _, inv = np.unique(keys, axis=0, return_inverse=True)
    counts = np.bincount(inv)
    w = 1.0 / counts[inv]           # each occupied cell contributes weight 1
    return np.average(values, weights=w)

for cell in [100, 200, 400, 800, 1600]:
    print(cell, round(declustered_mean(coords, gdf.soc.values, cell), 3))
print("naive  ", round(gdf.soc.mean(), 3))
Self-test
  1. Why is the variogram estimator unusually sensitive to outliers? Point to the specific term responsible.
  2. You log-transform, krige, and exponentiate. What quantity have you estimated, and how does it differ from the one you wanted?
  3. Your empirical variogram climbs steadily and never flattens. Give two distinct explanations and say how you would tell them apart.
  4. A survey has 200 points, 140 of them within 500 m of a road. Is the sample mean biased? In which direction, and what would you need to know to say?
  5. Under what circumstances is deleting an extreme value the right decision?

Reading. Isaaks & Srivastava, ch. 2–4 — still the best treatment of exploratory work in the subject. Deutsch & Journel, GSLIB, on declustering. Diggle, Menezes & Su (2010), "Geostatistical inference under preferential sampling", JRSS-C 59(2), for the serious version of the problem.

03

Week three

Quantifying spatial dependence

By the end of this week you should be able to
  • Compute semivariance by hand for a small dataset
  • Derive the empirical variogram and state the relationship between γ(h) and C(h)
  • Choose lag width, tolerance and maximum lag, and justify the choices
  • Read a variogram cloud and an h-scatterplot as diagnostics
  • Recognise when an empirical variogram is too unstable to model

From "nearby things are similar" to a number

Last week's claim was that neighbouring values are related. This week we measure it. Take every pair of points separated by roughly the same distance h, square the difference in their values, and average. If nearby things really are similar, that average will be small for small h and grow as h increases.

That is the whole idea. The estimator, due to Matheron, is:

(3.1) γ̂(h) = 12N(h) ∑i=1N(h) [ z(xi) − z(xi+h) ]²

N(h) is the number of pairs separated by approximately h. The 2 in the denominator makes γ a semivariance, which is what makes γ(h) directly comparable to a variance rather than to twice one.

Under second-order stationarity the variogram and the covariance function carry the same information:

(3.2) γ(h) = C(0) − C(h)

C(0) is the variance of the process. Where covariance is high, semivariance is low, and vice versa — the variogram is the covariance function turned upside down. Note that this identity requires C(0) to exist; under the weaker intrinsic hypothesis it may not, and only γ is defined. That is the technical reason the variogram is the primary tool.

Worked example — semivariance by hand

Five samples along a transect at 100 m spacing, values in g/kg:

Position (m)0100200300400
Value1214111716

Lag h = 100 m. Four pairs: (12,14), (14,11), (11,17), (17,16). Squared differences: 4, 9, 36, 1. Sum = 50.
γ̂(100) = 50 / (2 × 4) = 6.25

Lag h = 200 m. Three pairs: (12,11), (14,17), (11,16). Squared differences: 1, 9, 25. Sum = 35.
γ̂(200) = 35 / (2 × 3) = 5.83

Lag h = 300 m. Two pairs: (12,17), (14,16). Squared differences: 25, 4. Sum = 29.
γ̂(300) = 29 / (2 × 2) = 7.25

Lag h = 400 m. One pair: (12,16). Squared difference 16.
γ̂(400) = 16 / (2 × 1) = 8.00

Two lessons are already visible. The variogram is not monotonic — γ̂(200) is below γ̂(100) — because with three or four pairs the estimate is extremely noisy. And the number of pairs collapses as the lag grows, from four to one. This is why we do not trust long lags, and why the rule of thumb exists: at least about 30 pairs per lag class, and no lag beyond half the survey extent. On a real dataset every one of these numbers would be computed from hundreds of pairs.

Binning: the three choices you must make

In two dimensions, no two pairs have exactly the same separation, so we bin. Three decisions follow, and all three change the answer.

The variogram is a modelling choice, not a measurement

Change the bin count and you get a different variogram from the same data. This is not a flaw to be hidden; it is a property to be reported. Two analysts working honestly on the same dataset can produce different variograms and both be defensible. What is not defensible is trying five binnings, picking the prettiest, and reporting it as though it were the only one. Report your choices and show that your conclusions are not fragile to them.

Two diagnostics worth the time

The variogram cloud plots every individual pair — separation on the x-axis, half the squared difference on the y-axis — before any averaging. Points sitting far above the general mass at short lags mark pairs of nearby locations with wildly different values, and tracing them back to specific samples is often how a data error is caught.

The h-scatterplot plots z(x) against z(x+h) for one lag class. At short lags the cloud should hug the 1:1 line; as the lag grows it should fatten toward a circular blob. Watching that sequence is the most intuitive picture of spatial correlation decaying that the subject offers, and it makes the variogram feel less like an incantation.

Practical 3 — Building an empirical variogram
Core everyone
  1. On paper, before you open a laptop: reproduce the five-point worked example above and verify all four values. Then change the value at 300 m from 17 to 12 and recompute γ̂(100). You should be able to explain the change without running anything.
  2. Build an empirical variogram for Meuse log-zinc with skgstat. Use 15 bins and a maximum lag of half the diagonal.
  3. Repeat with 8, 15 and 30 bins. Plot the three on one set of axes. Which features survive all three binnings? Those are the ones you can believe.
  4. Compare the Matheron estimator with the Cressie–Hawkins robust estimator on the same data. Where do they differ most, and why is that where you would expect?
  5. Plot the variogram cloud. Identify the three pairs contributing most at the shortest lag and locate them on a map.
  6. Produce h-scatterplots for three increasing lag classes. Describe the progression in one sentence.
Extension if you have time
  1. Write your own empirical variogram function from scratch — pairwise distances, binning, averaging — and reproduce skgstat's output to three decimal places. This is the best hour you can spend on understanding the tool.
  2. Bootstrap the variogram: resample the points with replacement 200 times and plot the envelope of the resulting variograms. You now have a picture of how uncertain your variogram actually is, which almost no published study shows.
import numpy as np
import skgstat as skg
import matplotlib.pyplot as plt

coords = gdf[["X", "Y"]].values
vals   = np.log(gdf.zinc.values)          # week 2 said transform; we did

# half the diagonal of the bounding box — the sensible maximum lag
xmin, ymin, xmax, ymax = gdf.total_bounds
maxlag = 0.5 * np.hypot(xmax - xmin, ymax - ymin)

V = skg.Variogram(coords, vals,
                  n_lags=15,
                  maxlag=maxlag,
                  estimator="matheron",
                  normalize=False)

print(V)                                   # bins, pair counts, fitted parameters
print("pairs per bin:", V.bin_count)      # check none is below ~30

# sensitivity to binning — never report just one
fig, ax = plt.subplots(figsize=(7, 4.5))
for n in (8, 15, 30):
    Vn = skg.Variogram(coords, vals, n_lags=n, maxlag=maxlag)
    ax.plot(Vn.bins, Vn.experimental, "o-", label=f"{n} bins", alpha=0.8)
ax.set_xlabel("lag h (m)"); ax.set_ylabel("semivariance γ(h)"); ax.legend()
plt.show()
Self-test
  1. Compute γ̂(100) and γ̂(200) by hand for the transect 8, 11, 9, 15, 14 at 100 m spacing.
  2. Why does the pair count fall as lag increases, and what does that imply about which end of the variogram you should trust?
  3. State the relationship between γ(h) and C(h), and explain why geostatistics prefers the variogram.
  4. Your shortest lag class contains 6 pairs. What are your options?
  5. A variogram cloud shows a handful of very high values at the shortest lag. What are the two most likely causes, and how would you distinguish them?

Reading. Webster & Oliver, ch. 4. Isaaks & Srivastava, ch. 7. Cressie & Hawkins (1980) on the robust estimator, Mathematical Geology 12(2). The scikit-gstat documentation is unusually good and should be read as a text, not consulted as a reference.

04

Week four

Variogram modelling

By the end of this week you should be able to
  • Identify nugget, partial sill and range on a variogram and interpret each physically
  • Write down the spherical, exponential, Gaussian and Matérn models
  • Explain why only certain functions may be used, and what happens if you ignore this
  • Detect anisotropy from directional variograms and quantify it
  • Fit a model and defend the fit against a reasonable challenge

Why we cannot use the empirical variogram directly

Kriging needs a semivariance value for every separation that arises in the calculation — including separations that fall between your bins, and separations shorter than anything you sampled. The empirical variogram gives you fifteen points. So we fit a continuous function through them, and from that point on the model, not the data, is what kriging uses.

There is a second and less obvious reason. The kriging system requires that the matrix of semivariances be invertible for any configuration of points. Only functions with a particular mathematical property — conditional negative-definiteness — guarantee this. Fit an arbitrary curve, and you can obtain negative kriging variances, which are meaningless. This is why we choose from a small catalogue of permissible models rather than fitting whatever passes closest through the points.

The anatomy of a variogram

c₀ nugget sill = c₀ + c₁ a range no spatial correlation lag distance h → γ(h) →
The three parameters. The nugget c0 is the jump at the origin — variance present at separations shorter than you sampled, plus measurement error. The sill is the plateau, equal to the variance of the process. The range a is the lag at which the plateau is reached: beyond it, two observations are effectively unrelated.

Each parameter has a physical reading, and you should always give one.

The permissible models

spherical exponential Gaussian sill a lag distance h → γ(h) →
Same nugget, same sill, same practical range — three very different behaviours near the origin. The Gaussian model leaves the nugget with zero slope (parabolic), implying a very smooth phenomenon. The exponential rises steeply and approaches the sill only asymptotically. The spherical reaches its sill exactly at the range. Behaviour near the origin is the part that most affects kriging weights, so this is the region to get right.
(4.1) Spherical   γ(h) = c0 + c1 [ 1.5(h/a) − 0.5(h/a)³ ]   for h ≤ a,   = c0 + c1 for h > a
(4.2) Exponential   γ(h) = c0 + c1 [ 1 − exp(−h/r) ]

Reaches the sill only asymptotically. The practical range — where γ reaches 95% of the sill — is 3r. Software differs on whether the parameter it reports is r or 3r; check, because a factor of three in your range is not a small error.

(4.3) Gaussian   γ(h) = c0 + c1 [ 1 − exp(−h²/r²) ]

Practical range r√3. Implies an extremely smooth, twice-differentiable phenomenon. With a zero nugget it makes the kriging matrix badly conditioned and produces unstable, oscillating estimates — so if you fit a Gaussian model, keep a small nugget.

(4.4) Matérn   γ(h) = c0 + c1 [ 1 − 21−νΓ(ν) ( hr )ν Kν( h/r ) ]

The general case. The smoothness parameter ν controls behaviour at the origin: ν = 0.5 reproduces the exponential, ν → ∞ approaches the Gaussian. Rather than choosing between models you estimate ν from the data, which is the more honest procedure — and the reason the Matérn dominates the modern statistical literature. Kν is the modified Bessel function of the second kind.

Nested structures

A sum of permissible models is itself permissible, and real data often needs one. Soil properties frequently show variation at two scales — a short-range structure from local soil formation processes at 50–200 m, and a long-range structure from geology or topography at several kilometres. Fitting a single model to such a variogram fits neither structure. Fit a nested model: c0 plus a short-range spherical plus a long-range spherical. Each component then has a physical interpretation you can name, which is a useful discipline — if you cannot name the process behind a component, ask whether you are fitting noise.

Anisotropy

So far we have assumed the variogram depends only on the length of the lag vector, not its direction. That is often false. Soil properties along a valley behave differently from soil properties across it; a plume of contamination is elongated in the direction of groundwater flow; rainfall correlates further along a mountain range than across it.

Detect it by computing directional variograms: restrict pairs to those whose separation vector falls within an angular tolerance of a chosen direction. Compute at 0°, 45°, 90° and 135° with ±22.5° tolerance, which covers the plane without overlap. Then read the result:

Report anisotropy as a ratio and an angle: "geometric anisotropy, ratio 2.4, major axis 35°" tells a reader everything they need. And beware the trap — with four directions and typical sample sizes, each directional variogram has a quarter of the pairs of the omnidirectional one, so apparent anisotropy in a small dataset is often noise. Check whether it survives a change of binning before you believe it.

Fitting

Two approaches, and you should know both:

Whichever you use, do not treat fitting as automatic. Plot the fit over the empirical points and look at it. A fit that is excellent at long lags and poor at short ones is a bad fit for kriging purposes, no matter what the objective function says, because short lags are what determine the weights.

Do not accept the software's first answer

Every fitting routine will return parameters. It will return them for pure noise, for a variogram built from six pairs, and for data with an unremoved trend. The routine has no way to tell you that the model is meaningless. You do — by plotting the fit, checking the sill against the sample variance, checking the range against the survey extent, and asking whether the nugget is physically plausible. Get into the habit now, because in week 5 these parameters go straight into a prediction that someone may act on.

Practical 4 — Fit a model and defend it
Core everyone
  1. Fit spherical, exponential and Gaussian models to the Meuse log-zinc variogram. Tabulate c0, c1, a and the RMSE of fit for each.
  2. Plot all three fits over the empirical points on one figure. Which is best at short lags? Is that the same as the one with the lowest RMSE? Comment.
  3. Compute the nugget-to-sill ratio for your preferred model and interpret it in one sentence.
  4. Compute directional variograms at 0°, 45°, 90°, 135° with ±22.5° tolerance. Plot them together. Is there anisotropy? If so, estimate the ratio and the major-axis direction.
  5. Repeat the directional analysis with a different bin count. Does your conclusion survive? Report honestly if it does not.
  6. Write a short model statement: the model, its parameters, its physical interpretation, and the one thing you are least confident about.
Extension if you have time
  1. Fit a Matérn model and examine the estimated ν. Which of the three standard models does your data most resemble?
  2. Fit a nested model — nugget plus two spherical structures — to the Kiambu carbon data and try to attach a physical process to each range.
# --- compare models on the same empirical variogram --------------------
V = skg.Variogram(coords, vals, n_lags=15, maxlag=maxlag)

for m in ["spherical", "exponential", "gaussian", "matern"]:
    V.model = m
    c0 = V.parameters[-1]                 # nugget is reported last
    print(f"{m:12s} range={V.parameters[0]:8.1f}  "
          f"sill={V.parameters[1]:6.3f}  nugget={c0:6.3f}  rmse={V.rmse:6.4f}")

# --- directional variograms -------------------------------------------
fig, ax = plt.subplots(figsize=(7, 4.5))
for az in (0, 45, 90, 135):
    DV = skg.DirectionalVariogram(coords, vals,
                                  azimuth=az,
                                  tolerance=22.5,     # four non-overlapping sectors
                                  n_lags=12,
                                  maxlag=maxlag)
    ax.plot(DV.bins, DV.experimental, "o-", label=f"{az}°", alpha=0.85)
ax.set_xlabel("lag h (m)"); ax.set_ylabel("γ(h)")
ax.legend(title="azimuth"); plt.show()
Self-test
  1. Why may we not fit any smooth curve that passes close to the empirical points?
  2. A variogram has nugget 0.6 and sill 0.85. What does this tell you about the prospects for kriging, and what would you do next?
  3. Your software reports an exponential range of 240 m. What is the practical range, and why does the distinction matter?
  4. Directional variograms at 0° and 90° have similar sills but ranges of 400 m and 950 m. Name the type of anisotropy and say how it is handled.
  5. Why is a Gaussian model with zero nugget numerically dangerous?

Reading. Webster & Oliver, ch. 5–6. Chilès & Delfiner, Geostatistics: Modeling Spatial Uncertainty, ch. 2, for the mathematics of permissible models. Stein, Interpolation of Spatial Data (1999), ch. 2, on why the Matérn deserves its status.

05

Week five · CAT 1 this week

Ordinary kriging

By the end of this week you should be able to
  • Derive the ordinary kriging system from the unbiasedness and minimum-variance conditions
  • Solve a three-point kriging system by hand and interpret the weights
  • Explain why kriging weights are not a function of distance alone
  • State exactly what the kriging variance does and does not measure
  • Produce paired prediction and standard-error surfaces in Python

What we are asking for

We want an estimate at an unsampled location x0, formed as a weighted average of the samples:

(5.1) ẑ(x0) = ∑i=1n λi z(xi)

Every interpolation method you have met is of this form. Nearest neighbour sets one weight to 1 and the rest to 0. Inverse distance weighting sets λi ∝ 1/dip. What distinguishes kriging is that it does not assert the weights; it derives them, from two requirements.

Unbiasedness. On average the estimate should equal the truth, E[ẑ(x0) − Z(x0)] = 0. With an unknown but constant mean, this forces the weights to sum to one.

Minimum variance. Among all weight sets satisfying that constraint, choose the one minimising the estimation variance Var[ẑ(x0) − Z(x0)].

This is a constrained optimisation, handled with a Lagrange multiplier ψ. Differentiating and setting to zero gives a linear system of n+1 equations:

(5.2) ∑j=1n λj γ(xi, xj) + ψ = γ(xi, x0)    for i = 1 … n ∑j=1n λj = 1

In matrix form, Γλ = γ0, where Γ is the (n+1)×(n+1) matrix of semivariances between samples bordered by ones, and γ0 holds the semivariances between each sample and the target, with a 1 appended.

Having solved for the weights, the minimised variance itself is available:

(5.3) σ²OK(x0) = ∑i=1n λi γ(xi, x0) + ψ

Look carefully at the left-hand matrix in (5.2). It contains γ between samples — how redundant the samples are with each other — and the right-hand side contains γ between samples and the target — how relevant each sample is. Kriging balances relevance against redundancy. That is the entire idea, and the worked example below makes it concrete.

Worked example — solving a three-point system by hand
P₁ (0, 100) z = 10 P₂ (30, 95) z = 11 P₃ (0, −100) z = 6 x₀ — estimate here 100 m 99.6 m 100 m 30.4 m apart All three samples are ~100 m from the target

Model: spherical, nugget c0 = 2, partial sill c1 = 8 (total sill 10), range a = 250 m. Evaluating equation (4.1) at each separation:

SeparationDistance (m)γ
P₁–P₂30.43.452
P₁–P₃200.09.552
P₂–P₃197.39.504
P₁–x₀100.06.544
P₂–x₀99.66.528
P₃–x₀100.06.544

The system, from (5.2), is:

0·λ₁ + 3.452·λ₂ + 9.552·λ₃ + ψ = 6.544 3.452·λ₁ + 0·λ₂ + 9.504·λ₃ + ψ = 6.528 9.552·λ₁ + 9.504·λ₂ + 0·λ₃ + ψ = 6.544 λ₁ + λ₂ + λ₃ = 1

P₁ and P₂ are nearly interchangeable — the same distance from the target and the same distance from P₃ — so set λ₁ = λ₂ = λ and λ₃ = 1 − 2λ. Substituting into the first and third equations gives −15.652λ + ψ = −3.008 and 19.056λ + ψ = 6.544. Subtracting: 34.708λ = 9.552, so λ = 0.2752 and ψ = 1.299.

λ₁ = 0.275   λ₂ = 0.275   λ₃ = 0.450

The estimate is ẑ = 0.275(10) + 0.275(11) + 0.450(6) = 8.48 g/kg, and from (5.3) the kriging variance is 0.275(6.544) + 0.275(6.528) + 0.450(6.544) + 1.299 = 7.84, giving a standard error of 2.80 g/kg.

Now look at what happened. All three samples sit the same distance from the target. Inverse distance weighting would therefore give each exactly one third, producing an estimate of 9.00. Kriging gives P₃ — the lone sample on the far side — a weight of 0.45, almost as much as P₁ and P₂ combined. It has recognised that P₁ and P₂, only 30 m apart, are largely telling it the same thing, and has declustered them automatically. No interpolator that looks only at distance to the target can do this. That is what you are paying for.

What drives the weights

The kriging variance, and the trap in it

Equation (5.3) yields a variance at every location, and mapping it alongside the prediction is one of the most useful things geostatistics gives you. But read (5.3) again and notice what is not in it: the data values. The kriging variance is built entirely from the variogram model and the geometry of the sampling configuration. Move all your sample values around at random, keeping the locations fixed, and the kriging variance map does not change by a single unit.

What the kriging variance actually is

It is a measure of configuration quality — how well-surrounded a location is by samples, given an assumed variogram. It is not a measure of how wrong your estimate is at that location. It cannot detect that your variogram model is poor, that your data contain an error, or that the phenomenon is more variable in one part of the region than another.

Presenting a kriging variance map captioned "uncertainty" or "error" is one of the most common misrepresentations in applied geostatistics, and one you will be marked down for here. Say "kriging variance" and explain what it measures. If you need a genuine picture of uncertainty in the values, you need simulation — week 8.

Search neighbourhoods

Using all n samples for every prediction means inverting an n×n matrix at every grid node, which is impractical beyond a few thousand points and unnecessary anyway: samples beyond the range contribute essentially nothing. So we use a moving neighbourhood — typically the nearest 16 to 32 samples, sometimes with quadrant or octant constraints forcing the search to draw from all directions rather than taking 20 points from one dense cluster.

The choice is a real one. Too few samples gives noisy, discontinuous maps with visible artefacts at the boundaries where the neighbourhood changes composition. Too many is slow for no benefit. Sixteen to twenty-four with quadrant balancing is a reasonable default; report what you used.

Practical 5 — Kriging, by hand and by machine
Core everyone
  1. On paper first. Reproduce the worked example above. Then move P₂ to (80, 60) — still roughly 100 m from the target but no longer clustered with P₁ — recompute the three semivariances that change, and re-solve. Predict, before you calculate, what will happen to λ₃.
  2. Assemble and solve the same system in NumPy with np.linalg.solve. Confirm you get the same weights. You have now written kriging yourself.
  3. Krige the Meuse log-zinc data onto a 40 m grid using your week 4 model. Produce two maps side by side: prediction and kriging standard error.
  4. Overlay the sample locations on the standard-error map. Describe the relationship in one sentence.
  5. Re-run with the nugget set to zero and then to 80% of the sill. Show all three prediction maps. What happens at the sample locations in each case, and why?
  6. Re-run with neighbourhood sizes of 6, 16 and 48. Comment on artefacts and on run time.
Extension if you have time
  1. Construct a configuration where one sample receives a negative weight, and demonstrate it numerically.
  2. Verify the exactness property: krige at a location where a sample already exists, with a zero-nugget model, and confirm the estimate returns the observed value and the variance returns zero. Then add a nugget and show that both properties break. Explain why.
import numpy as np

def spherical(h, c0=2.0, c1=8.0, a=250.0):
    h = np.asarray(h, dtype=float)
    g = np.where(h <= a,
                 c0 + c1 * (1.5 * h / a - 0.5 * (h / a) ** 3),
                 c0 + c1)
    return np.where(h == 0, 0.0, g)      # γ(0) = 0 exactly

pts  = np.array([[0., 100.], [30., 95.], [0., -100.]])
z    = np.array([10., 11., 6.])
x0   = np.array([0., 0.])
n    = len(pts)

# Γ: semivariances between samples, bordered with ones for the constraint
D = np.linalg.norm(pts[:, None, :] - pts[None, :, :], axis=-1)
G = np.ones((n + 1, n + 1))
G[:n, :n] = spherical(D)
G[n, n]   = 0.0

# γ₀: semivariances between each sample and the target
g0 = np.ones(n + 1)
g0[:n] = spherical(np.linalg.norm(pts - x0, axis=1))

sol    = np.linalg.solve(G, g0)
w, psi = sol[:n], sol[n]

print("weights   ", np.round(w, 4))          # [0.2755 0.2749 0.4496]
print("estimate  ", round(float(w @ z), 3))    # 8.475
print("variance  ", round(float(w @ g0[:n] + psi), 3))
print("naive mean", round(z.mean(), 3))       # 9.0 — what IDW would give
from pykrige.ok import OrdinaryKriging

gridx = np.arange(xmin, xmax, 40.0)
gridy = np.arange(ymin, ymax, 40.0)

OK = OrdinaryKriging(
    coords[:, 0], coords[:, 1], vals,
    variogram_model="spherical",
    variogram_parameters={"sill": 0.62, "range": 900.0, "nugget": 0.05},
    nlags=15, enable_plotting=False,
)

zhat, var = OK.execute("grid", gridx, gridy)
se = np.sqrt(var)

fig, (a1, a2) = plt.subplots(1, 2, figsize=(12, 5))
a1.imshow(zhat, origin="lower", cmap="viridis"); a1.set_title("OK prediction, log-zinc")
a2.imshow(se,   origin="lower", cmap="magma");   a2.set_title("kriging standard error")
a2.scatter(*((coords.T - [[xmin], [ymin]]) / 40.0), s=5, c="white")
plt.show()
Self-test
  1. Why do the ordinary kriging weights sum to one? Which requirement forces it?
  2. Three samples lie at equal distances from a target; two are 10 m apart and one is isolated. Rank the weights and justify the ranking without calculating.
  3. What does a negative kriging weight mean, and when is it a problem?
  4. Two locations have identical kriging variances. Does that mean the estimates are equally reliable? Answer in full.
  5. Your kriged map is almost flat and close to the global mean everywhere. Give the most likely cause and the diagnostic that would confirm it.

Reading. Isaaks & Srivastava, ch. 12 — the clearest derivation in print, worked with actual numbers. Webster & Oliver, ch. 8. Goovaerts, ch. 5.

06

Week six

When the mean is not constant

By the end of this week you should be able to
  • Choose between simple, ordinary, universal and external-drift kriging for a given problem
  • Use a covariate to improve prediction, and know when it will not help
  • Implement regression kriging and explain its relationship to KED
  • Explain change of support and why block estimates have lower variance
  • State when co-kriging is worth its cost

The assumption we have been making

Ordinary kriging assumes the mean is constant but unknown, at least within the search neighbourhood. Often it is not. Rainfall increases with altitude. Soil depth increases downslope. Contamination decreases away from a source. When the mean varies systematically, ordinary kriging is inefficient at best and biased at worst, and the variogram will not reach a sill.

The family of methods below all decompose the variable into a deterministic part and a stochastic part:

(6.1) Z(x) = m(x) + ε(x)

m(x) is the trend or drift — the structured, explainable part — and ε(x) is the spatially correlated residual. The methods differ only in what they assume about m(x).

MethodAssumption about m(x)Use when
Simple krigingConstant and knownRarely — but it is the building block for simulation in week 8
Ordinary krigingConstant, unknownThe default; no covariate available
Universal krigingA polynomial function of the coordinatesA smooth regional gradient with no physical covariate
KED / external driftA linear function of a covariate known everywhereThe best case: a covariate that is physically related and mapped densely
Regression krigingAny regression model, fitted separatelySeveral covariates, or a non-linear relationship

Kriging with an external drift

This is the method to reach for, because it exploits the situation you are usually in: a sparse set of expensive point measurements, and a cheap covariate available everywhere. The pairings are familiar — rainfall with elevation, temperature with elevation, soil organic carbon with NDVI or with terrain wetness index, groundwater level with topography.

KED does everything at once: it estimates the trend coefficients and the spatial prediction within a single system, using the residual variogram. Two conditions must hold, and both are frequently violated in student work.

The critical technical point: the variogram used in KED is the variogram of the residuals, not of the raw variable. Fit the trend, take the residuals, compute and model their variogram, and use that. The residual variogram will typically have a shorter range and a lower sill than the raw one — that difference is precisely the structure the covariate has explained.

Regression kriging

Regression kriging is the same idea run as three explicit steps, which makes it easier to understand, easier to debug, and more flexible:

  1. Fit a regression of z on the covariates at the sample locations, by any method — OLS, GLM, or in week 10, random forest.
  2. Compute the residuals, model their variogram, and krige the residuals across the region.
  3. Add the kriged residual surface to the regression prediction surface.

With OLS in step 1 and consistent variogram estimation, regression kriging and KED are theoretically equivalent. In practice RK is preferred whenever you want several covariates, a non-linear model, or simply to see the two components separately — and being able to map the trend surface and the residual surface individually is genuinely informative about where your covariate is working.

The circularity trap

Fitting the trend by OLS and then computing a variogram from those residuals is slightly circular: OLS assumed independent errors, which is exactly what you are about to demonstrate is false. The residual variogram is therefore mildly biased, typically underestimating the range. For teaching purposes and most applied work this is acceptable and universally done — but you should know it, and say so. The principled fix is generalised least squares iterated with the variogram estimation, or REML, which estimates both jointly.

Change of support: block kriging

Everything so far has predicted a value at a point. Often the decision is about an area: the mean grade of a 10 m mining block, the mean soil carbon of a one-hectare field, the mean rainfall over a catchment. Block kriging estimates the average over a block V directly, by replacing the point-to-point semivariances on the right-hand side of the kriging system with point-to-block average semivariances.

The consequence to remember: block estimates have substantially lower kriging variance than point estimates, because averaging over an area removes short-scale variability. This is not a trick — the block mean genuinely is more predictable than any individual point within it. It also means that comparing a point-kriging standard error with a block-kriging standard error is meaningless unless you say which is which.

Co-kriging, briefly

When the secondary variable is also sparsely sampled — not available everywhere — KED is unavailable and co-kriging is the formally correct method. It requires modelling the variogram of each variable plus the cross-variogram between them, all under the linear model of coregionalisation, which constrains the models to share ranges and produces a positive-definite system.

It is a lot of machinery. Be honest about when it pays: co-kriging helps materially only when the secondary variable is markedly more densely sampled than the primary and strongly correlated with it. If both are sampled at the same locations, the gain is usually negligible and not worth the modelling burden. Know that it exists, know the condition under which it is worth doing, and reach for KED or RK first.

Practical 6 — Bringing in a covariate
Core everyone
  1. Load the rainfall station dataset and the SRTM DEM. Extract elevation at each station.
  2. Scatterplot rainfall against elevation. Fit an OLS regression and report R² and the slope in mm per 100 m.
  3. Compute and model the variogram of the raw rainfall and of the OLS residuals on the same axes. Report both sets of parameters and explain the difference in sill.
  4. Implement regression kriging: krige the residuals, add to the regression surface, map the result.
  5. Run KED with pykrige.UniversalKriging using elevation as the drift term. Map the difference between the RK and KED surfaces and comment on its magnitude.
  6. Krige the same data with ordinary kriging. Produce all three maps together with a common colour scale, and identify where the covariate made the biggest difference. Explain why it was there.
  7. Block-krige to a 1 km block support. Compare the standard-error map with the point-kriging one and state the ratio.
Extension if you have time
  1. Add slope, aspect and distance-to-coast as further covariates in the regression step. Does prediction improve under cross-validation, or have you overfitted? Week 7 gives you the tools to answer this properly — come back to it.
  2. Demonstrate the circularity problem: simulate data with a known trend and known residual variogram, fit by OLS, and measure how much the estimated residual range is biased.
from sklearn.linear_model import LinearRegression
from pykrige.uk import UniversalKriging

# ---- regression kriging, done as three explicit steps -----------------
X = stations[["elev"]].values
y = stations["rain"].values

lm = LinearRegression().fit(X, y)
print(f"R² = {lm.score(X, y):.3f}   slope = {lm.coef_[0]*100:.1f} mm per 100 m")

resid = y - lm.predict(X)

# the variogram that matters is the RESIDUAL variogram, not the raw one
Vr = skg.Variogram(stations[["X", "Y"]].values, resid,
                   n_lags=12, model="exponential")
print("residual variogram:", Vr.parameters)

OKr = OrdinaryKriging(stations.X, stations.Y, resid,
                      variogram_model="exponential",
                      variogram_parameters={"sill": Vr.parameters[1],
                                            "range": Vr.parameters[0],
                                            "nugget": Vr.parameters[2]})
resid_grid, _ = OKr.execute("grid", gridx, gridy)

trend_grid = lm.predict(dem_grid.reshape(-1, 1)).reshape(dem_grid.shape)
rk_grid    = trend_grid + resid_grid      # the two components, added back

# ---- the same thing in one step, as KED -------------------------------
UK = UniversalKriging(stations.X, stations.Y, y,
                      variogram_model="exponential",
                      drift_terms=["specified"],
                      specified_drift=[stations.elev.values])
ked_grid, ked_var = UK.execute("grid", gridx, gridy,
                               specified_drift_arrays=[dem_grid])
Self-test
  1. Your variogram climbs without reaching a sill. Name three responses and the circumstances favouring each.
  2. Why must the covariate in KED be available at every prediction location? What method applies if it is not?
  3. Whose variogram do you model in KED, and why is it not the variogram of the raw variable?
  4. Block kriging returns a smaller standard error than point kriging on the same data. Is this a genuine improvement in knowledge? Explain.
  5. Under what specific condition is co-kriging worth the extra modelling effort?

Reading. Hengl, Heuvelink & Stein (2004), "A generic framework for spatial prediction of soil variables based on regression-kriging", Geoderma 120 — the standard reference for RK. Goovaerts, ch. 6. Webster & Oliver, ch. 9.

07

Week seven

Validation, and honesty about error

By the end of this week you should be able to
  • Run leave-one-out and k-fold cross-validation and interpret the full diagnostic set
  • Use the MSDR to test whether your variogram model is producing honest variances
  • Explain why random cross-validation is optimistic on spatial data, and apply spatial blocking
  • Describe the smoothing effect and identify it in a map
  • Benchmark kriging against simpler methods and accept the result

The question this week answers

You have a map. Someone is going to spend money on the basis of it. How good is it, and how do you know?

The kriging variance will not answer this — week 5 established that it never saw your data values. The only honest route is to withhold observations, predict them, and compare. That is cross-validation, and it is the closest thing this subject has to a truth serum.

The diagnostics, and what each one catches

Leave-one-out cross-validation removes each observation in turn, predicts it from the remainder, and collects the errors ei = z(xi) − ẑ(xi). Four statistics follow, and you should report all four, because each catches a different failure.

(7.1) ME = 1n ∑ ei     RMSE = √( 1n ∑ ei² )

ME should be near zero — it tests unbiasedness. A large ME means systematic over- or under-prediction and usually points to an unhandled trend. RMSE measures accuracy and is the number to compare between methods; it has the units of your variable, which makes it interpretable.

(7.2) MSDR = 1n ∑ [ z(xi) − ẑ(xi) ]²σ̂²(xi)

The mean squared deviation ratio is the diagnostic most students skip and the one that matters most. It divides each squared error by the kriging variance predicted at that location. If the variogram model is honest, MSDR should be close to 1. Substantially above 1 means your model is overconfident — errors are larger than it claims. Substantially below 1 means it is over-cautious.

MSDR is the only one of the four that tests the variance model rather than the prediction. A model can have an excellent RMSE and an MSDR of 3, which means the map is reasonably accurate but the error bars you would quote from it are wrong by a factor of nearly two. If you are going to attach uncertainty to a map — and this unit insists you do — this is the number that says whether that uncertainty is honest.

Random cross-validation lies to you on spatial data

Standard k-fold cross-validation assigns observations to folds at random. On spatially autocorrelated data this means almost every held-out point has a near neighbour in the training set — often a few metres away. The model is not being asked to predict; it is being asked to interpolate between points it has already seen. The resulting RMSE can be optimistic by 30% or more, and the effect grows with how clustered your sampling is.

The fix is spatial block cross-validation: partition the region into blocks larger than the variogram range and hold out whole blocks. Now the model must genuinely predict into unsampled ground, which is what it will be asked to do in production. Expect your error to increase. That increase is not a regression in your model; it is the removal of a lie. This becomes even more important in week 10, when machine learning gives you many more ways to leak information between folds.

The smoothing effect

Kriging minimises estimation variance, and one consequence is that the resulting map is smoother than reality: high values are under-estimated, low values over-estimated, and the variance of the kriged surface is lower than the variance of the data. Plotting predicted against observed makes it obvious — the regression line has a slope below one, fanning away from the 1:1 line at both ends.

This is not a bug. It is the correct behaviour for minimising squared error, and it is unavoidable in any optimal linear estimator. But it matters enormously for a particular class of question. If you are asked "what fraction of this district exceeds the WHO fluoride guideline?", counting cells above the threshold on a kriged map will give you a systematically wrong answer, because the map has flattened exactly the extremes you are counting. Kriging is the right tool for "what is the value here"; it is the wrong tool for "how much of the area exceeds a threshold". That is a simulation question, and it is next week.

Benchmark honestly

Kriging is not automatically the best method. Compare it, on identical folds, against inverse distance weighting, nearest neighbour and a thin-plate spline. There are real situations where the simpler methods win: a very high nugget-to-sill ratio leaves little structure for kriging to exploit; very dense, evenly spaced sampling makes almost any interpolator adequate; small datasets make the variogram too unstable to be worth the trouble.

Report the comparison whichever way it comes out. A study that concludes "IDW performed as well as kriging here, and is simpler, so we used it" is a good study. A study that reports only the kriging results because they were expected is not.

Practical 7 — Build a validation harness
Core everyone
  1. Write a leave-one-out cross-validation function returning ME, RMSE, MAE and MSDR. You will reuse it for the rest of the unit, so write it properly.
  2. Run it on your week 5 ordinary kriging model. Report all four. Interpret the MSDR in one sentence.
  3. Re-run with a deliberately wrong nugget — double it, then halve it. Show how RMSE and MSDR each respond. Which is more sensitive, and why does that make MSDR the better diagnostic for the variance model?
  4. Implement spatial block cross-validation with blocks about twice the variogram range. Compare its RMSE with the random k-fold RMSE on the same data and quantify the optimism.
  5. Benchmark OK, KED and IDW (with p = 1, 2, 3) on identical spatial folds. Produce one clean comparison table.
  6. Plot predicted against observed for your best model with a 1:1 line and a fitted line. Estimate the smoothing by reporting the ratio of the variance of predictions to the variance of observations.
Extension if you have time
  1. Plot RMSE from spatial block CV as a function of block size, from small to very large. Explain the shape of the curve you get.
  2. Investigate whether the errors are themselves spatially correlated by computing a variogram of the cross-validation residuals. What would a structured residual variogram tell you?
import numpy as np
from sklearn.model_selection import KFold

def loo_cv(coords, values, model, params):
    """Leave-one-out CV returning the four diagnostics that matter."""
    n = len(values)
    pred = np.empty(n); var = np.empty(n)
    for i in range(n):
        keep = np.arange(n) != i
        ok = OrdinaryKriging(coords[keep, 0], coords[keep, 1], values[keep],
                             variogram_model=model, variogram_parameters=params)
        z, s = ok.execute("points", coords[i:i+1, 0], coords[i:i+1, 1])
        pred[i], var[i] = z[0], s[0]

    e = values - pred
    return {
        "ME":   e.mean(),                    # → 0 if unbiased
        "RMSE": np.sqrt((e ** 2).mean()),     # accuracy, in data units
        "MAE":  np.abs(e).mean(),             # less outlier-sensitive
        "MSDR": ((e ** 2) / var).mean(),      # → 1 if the variance model is honest
    }

def spatial_blocks(coords, block_size):
    """Fold labels from a spatial grid — hold out whole blocks, not points."""
    keys = np.floor(coords / block_size).astype(int)
    _, fold = np.unique(keys, axis=0, return_inverse=True)
    return fold

# the comparison that matters: same data, two ways of splitting it
fold_random  = KFold(10, shuffle=True, random_state=0)
fold_spatial = spatial_blocks(coords, block_size=2 * fitted_range)
# ... score both; the gap between them is your optimism
Self-test
  1. Your model has RMSE 4.2 and MSDR 2.8. What is wrong, and what would you change?
  2. Why does random k-fold cross-validation overestimate performance on spatial data? Be specific about the mechanism.
  3. How would you choose the block size for spatial block cross-validation?
  4. Explain why a kriged map is a poor basis for estimating the area exceeding a threshold.
  5. IDW beats kriging on your data under spatial CV. Give two plausible reasons and say what you would report.

Reading. Webster & Oliver, ch. 8 §8.4. Roberts et al. (2017), "Cross-validation strategies for data with temporal, spatial, hierarchical or phylogenetic structure", Ecography 40(8) — the definitive treatment. Meyer & Pebesma (2022) on area of applicability, Methods in Ecology and Evolution 12(9).

08

Week eight

Simulation instead of interpolation

By the end of this week you should be able to
  • Explain why the best single map is the wrong tool for a risk question
  • State the sequential Gaussian simulation algorithm step by step
  • Distinguish a realisation from the E-type estimate and know when each is appropriate
  • Post-process a set of realisations into a probability map
  • Propagate uncertainty through a downstream calculation

Two different questions

Compare these:

The reason is last week's smoothing effect. A kriged map has less variance than reality: it has flattened the peaks and filled in the troughs. Counting cells above a threshold on such a map systematically undercounts the area at risk, and the undercount is worst exactly where the threshold is far from the mean — which is usually where the policy question sits.

Simulation gives up optimality at each individual location in exchange for getting the statistical properties of the whole field right. A simulated realisation is not the best estimate anywhere; it is a plausible version of reality that reproduces the correct histogram, the correct variogram, and honours the data at the sample points. Generate many of them and you have a picture of what the field might be, not just a single flattened best guess.

Conditional simulation. The generation of multiple equally probable realisations of a random field, each of which (i) reproduces the observed values exactly at sampled locations, (ii) reproduces the sample histogram, and (iii) reproduces the modelled variogram.

Each realisation is less accurate than the kriged map at any single location. The set of realisations is far more informative than the kriged map, because the spread between them at each location is a genuine, data-aware measure of uncertainty — unlike the kriging variance.

Sequential Gaussian simulation

SGS is the standard algorithm and it is simpler than it sounds. The key insight is that under a multivariate Gaussian model, the conditional distribution at any location — given the data and the previously simulated values — is normal, with mean equal to the simple kriging estimate and variance equal to the simple kriging variance. So you can build a realisation one node at a time:

  1. Normal-score transform the data, so the marginal distribution is exactly standard normal. (This is why week 2 mattered.)
  2. Model the variogram of the transformed data.
  3. Define a random path visiting every grid node exactly once.
  4. At each node in turn: simple-krige from the original data and all previously simulated nodes to obtain a mean and a variance; draw one random value from the resulting normal distribution; add that drawn value to the conditioning dataset.
  5. When the path is complete, back-transform the whole realisation through the inverse normal-score table.
  6. Repeat from step 3 with a different random path and different random draws, for as many realisations as you need — 100 for exploratory work, 500 or more for stable tail probabilities.

Two details are worth pausing on. Step 4's phrase "and all previously simulated nodes" is what makes the realisation reproduce the variogram — each drawn value becomes data for its neighbours, propagating spatial structure across the field. And the random path in step 3 matters: a systematic path would introduce directional artefacts, which is why it must be random and different for each realisation.

Reading a set of realisations

You now have, say, 200 values at every grid node — one from each realisation. That is an empirical distribution, and everything useful follows from it:

Never map a single realisation as though it were an estimate

A single realisation looks more realistic than a kriged map — it has the right texture and roughness, and it is seductive for exactly that reason. It is also wrong nearly everywhere, and it is no more likely to be right than any of the other 199. Present realisations only in sets, to show variability. If you need one map, present the E-type or the kriged map, and say which it is.

Practical 8 — From a map to a probability
Core everyone
  1. Normal-score transform the groundwater fluoride dataset and model the variogram of the scores.
  2. Generate 100 conditional realisations on the prediction grid.
  3. Display four realisations side by side with a common colour scale. Describe how they differ and how they agree, and relate both to the sample locations.
  4. Compute the E-type estimate and place it beside the ordinary kriging map with a common scale. Compare their variances numerically and explain the direction of the difference.
  5. Produce a map of P(fluoride > 1.5 mg/L). Compare the area it identifies as at risk with the area obtained by thresholding the kriged map. State the discrepancy as a percentage and explain its cause.
  6. For each realisation, compute the total area exceeding the guideline. Plot the histogram of those 100 numbers, and report the median with a 90% interval. This is the answer a public health officer actually needs.
Extension if you have time
  1. Repeat with 500 realisations. How many are needed before the tail probabilities stabilise? Demonstrate rather than assert.
  2. Verify the algorithm: check that the realisations reproduce the sample histogram and the modelled variogram, and confirm that each honours the observed value at every sample location.
import numpy as np
import gstools as gs        # pip install gstools — the practical SGS route in Python

model = gs.Exponential(dim=2, var=0.85, len_scale=420.0, nugget=0.15)

krige = gs.krige.Ordinary(model, cond_pos=(x_obs, y_obs), cond_val=ns_values)
cond  = gs.CondSRF(krige)

N = 100
sims = np.empty((N, gridy.size, gridx.size))
for i in range(N):
    cond.set_pos((gridx, gridy), "structured")
    sims[i] = cond(seed=i)                       # one realisation

sims = back_transform(sims)                      # inverse normal score

etype  = sims.mean(axis=0)                      # → converges toward the kriged map
cstd   = sims.std(axis=0)                       # depends on DATA, unlike kriging variance
p_exc  = (sims > 1.5).mean(axis=0)             # P(exceeding the guideline)

# uncertainty in an aggregate — the number a decision-maker can use
cell_km2  = 0.04
area_risk = (sims > 1.5).sum(axis=(1, 2)) * cell_km2
print(f"area above guideline: {np.median(area_risk):.1f} km² "
      f"(90% interval {np.percentile(area_risk, 5):.1f}–"
      f"{np.percentile(area_risk, 95):.1f})")
Self-test
  1. Why does a kriged map underestimate the area exceeding a high threshold?
  2. What three properties must a conditional realisation reproduce?
  3. Why must the simulation path be random and differ between realisations?
  4. In what sense is a single realisation worse than the kriged map, and in what sense is a set of them better?
  5. Contrast the conditional variance from simulation with the kriging variance. Which responds to the data values, and why does that matter?

Reading. Goovaerts, ch. 8 — the standard treatment. Deutsch & Journel, GSLIB, on sgsim. Chilès & Delfiner, ch. 7. The gstools documentation for the Python implementation.

09

Week nine

Non-Gaussian and categorical variables

By the end of this week you should be able to
  • Apply the indicator transform and explain what it buys
  • Perform indicator kriging and interpret the output as a probability
  • Recognise and repair order-relation violations
  • Krige categorical variables such as soil class or land cover
  • Convert a probability map into a decision map with a stated risk tolerance

When the Gaussian assumption will not hold

Simulation last week required a normal-score transform, which requires a continuous variable with a well-defined distribution. Plenty of real problems do not fit: heavily skewed data with a long tail no transform tames; data with a spike of values at a detection limit; and variables that are not numeric at all — soil class, land cover, rock type, presence or absence.

The indicator approach handles all of these by giving up on estimating the value and estimating a probability instead.

The indicator transform

Pick a threshold zk and replace every observation with a 0 or a 1:

(9.1) i(x; zk) = 1 if z(x) ≤ zk,   0 otherwise

This is a remarkable trick, because the expected value of the indicator is the probability of being below the threshold. So if you krige the indicator variable, the estimate you get back — a number between 0 and 1 — is a direct estimate of P(Z(x0) ≤ zk), with no distributional assumption at all.

Two further benefits come free. Extreme values lose their leverage: a wildly high outlier becomes a 0, exactly like a moderately high value, so the indicator variogram is far more robust than the raw one. And you can use a different variogram at each threshold, which lets the spatial structure of the high values differ from that of the low ones — often physically true, and something ordinary kriging cannot express.

Building a full local distribution

Repeat the procedure at a series of thresholds — typically the nine deciles of the sample distribution — and krige each indicator separately. At every prediction location you now have nine estimated probabilities, which together trace out the local cumulative distribution function. From that you can read the median, any quantile, an interquartile range as an uncertainty measure, and the probability of exceeding any threshold you care about.

Order-relation violations

Because each threshold is kriged independently, nothing forces the results to be a valid CDF. You will regularly find estimated probabilities that decrease as the threshold increases, or that fall outside [0, 1]. These are order-relation violations and they are a normal, expected feature of indicator kriging, not evidence you have made a mistake.

The standard repair is to average the results of an upward and a downward pass through the thresholds, then truncate to [0, 1] and enforce monotonicity. Report how many violations occurred: a large number signals too few data per threshold, or thresholds set too far into the tails where almost every indicator is the same value.

Categorical variables

For a categorical variable with K classes, define one indicator per class and krige each. At every location you obtain K probabilities, which after correction sum to one. Two products follow: assign each location its most probable class for a map, and map the probability itself for a picture of where the classification is confident and where it is not. That second map is usually the more useful of the two, and it is the sort of thing that would improve a great many published soil and land-cover maps.

Sequential indicator simulation follows the same logic as SGS but draws from the indicator-derived local CDF instead of a Gaussian one — which is how you produce realisations of a categorical field, such as alternative plausible soil maps, each internally consistent.

From probability to decision

A probability map is not yet a decision. Turning it into one requires a threshold on the probability, and choosing that threshold is a value judgement about the relative cost of the two errors, not a statistical one.

Consider a fluoride exceedance map used to prioritise borehole testing. Flagging every location with P > 0.1 casts a wide net: few contaminated boreholes missed, many clean ones tested unnecessarily. Flagging at P > 0.7 tests far fewer, but misses more contaminated sites. The right answer depends on the cost of a test against the cost of a child drinking fluoride-contaminated water for a year, and that is a decision for the public health authority, not the analyst.

Your job is to make the trade-off visible: present the probability map, state the consequences of several candidate thresholds, and let the decision-maker choose with their eyes open. This is what it means to communicate uncertainty rather than merely compute it, and it is a skill this unit is assessing.

Practical 9 — A decision map with a stated tolerance
Core everyone
  1. Indicator-transform the fluoride data at the 1.5 mg/L guideline. Report the proportion of ones.
  2. Compute and model the indicator variogram. Compare it with the raw variogram from week 4 and comment on the difference in robustness.
  3. Krige the indicator and map P(fluoride > 1.5 mg/L). Compare this map with the simulation-derived probability map from week 8. Where do the two methods disagree, and which do you trust?
  4. Repeat at the nine deciles. Build the full local CDF at three chosen locations and plot them. Count and report the order-relation violations, then correct them.
  5. From the corrected CDFs, map the median and the interquartile range.
  6. Produce three decision maps at P > 0.1, 0.3 and 0.7. For each, report the number of locations flagged. Write a short paragraph addressed to a county water officer explaining the trade-off in plain language — no jargon, no equations.
Extension if you have time
  1. Krige a categorical soil-class variable by the indicator method and produce both a most-probable-class map and a confidence map.
  2. Implement sequential indicator simulation and generate ten alternative plausible soil-class maps.
# --- indicator kriging at a regulatory threshold -----------------------
threshold = 1.5                                 # mg/L, WHO guideline
ind = (gdf.fluoride.values > threshold).astype(float)
print(f"proportion above guideline: {ind.mean():.3f}")

Vi = skg.Variogram(coords, ind, n_lags=12, model="spherical")

OKi = OrdinaryKriging(coords[:, 0], coords[:, 1], ind,
                      variogram_model="spherical",
                      variogram_parameters={"sill": Vi.parameters[1],
                                            "range": Vi.parameters[0],
                                            "nugget": Vi.parameters[2]})
prob, _ = OKi.execute("grid", gridx, gridy)
prob = np.clip(prob, 0, 1)                       # kriging does not respect [0,1]

# --- make the decision trade-off explicit ------------------------------
for p in (0.1, 0.3, 0.7):
    flagged = (prob > p).sum()
    print(f"P > {p:.1f}: flag {flagged:5d} cells "
          f"({100*flagged/prob.size:.1f}% of the area)")
Self-test
  1. Why is the expected value of an indicator variable a probability?
  2. Give two reasons the indicator variogram is more robust than the raw variogram.
  3. What is an order-relation violation, why does it occur, and how is it repaired?
  4. Indicator kriging returns 1.08 at a location. What has happened and what do you do?
  5. Who should choose the probability threshold for a decision map, and what information do they need from you in order to choose it well?

Reading. Goovaerts, ch. 7. Journel (1983), "Nonparametric estimation of spatial distributions", Mathematical Geology 15(3). Deutsch & Journel, GSLIB, on ik3d and sisim.

10

Week ten · CAT 2 this week

Machine learning meets geostatistics

By the end of this week you should be able to
  • Position regression kriging as the bridge between the two traditions
  • Explain why a plain random forest on coordinates is a poor spatial predictor, and what fixes it
  • Identify feature leakage in a spatial modelling workflow
  • Compare a machine-learning and a geostatistical model under a fair protocol
  • State what machine learning gains and what it gives up

The honest framing

Most operational mapping in soils, ecology and Earth observation is now done with machine learning rather than kriging. SoilGrids, which produces global soil property maps at 250 m, is built on gradient-boosted trees and random forests, not on variograms. It would be dishonest to teach this unit as though that had not happened.

It would be equally dishonest to present it as a replacement. The two traditions answer overlapping but different questions, and the sharpest way to see the difference is this: a random forest gives you a prediction; kriging gives you a prediction and a model of the error. Where a stated uncertainty is required — a resource statement, a regulatory exceedance assessment, anything that will be challenged — that difference decides the matter.

Why a random forest on coordinates fails

Give a random forest X and Y as features and it will fit the training data well and generalise badly. Trees partition space with axis-parallel splits, producing blocky, rectangular artefacts; the model has no notion of distance and cannot interpolate smoothly; and outside the convex hull of the training data it predicts a constant, because trees cannot extrapolate at all.

Two established fixes:

Feature leakage: the failure that inflates published accuracies

Machine learning gives you far more ways to leak information between training and test sets than kriging does, and spatial data makes all of them worse. Three to watch for.

Random splits. Week 7's lesson, now more damaging: with many features a flexible model can effectively memorise locations, and a randomly held-out point sitting 20 m from a training point is nearly free to predict. Always use spatial blocking.

Preprocessing before splitting. Scaling, imputing, or selecting features on the full dataset before splitting leaks test information into training. Fit every transformation inside the fold.

Tuning on the test set. If you choose hyperparameters by looking at test performance, the test set is now part of training and your reported accuracy is optimistic. Use a separate inner loop for tuning — nested cross-validation.

What each side gives up

KrigingRandom forest
UncertaintyExplicit variance from the model; simulation gives full distributionsNone natively; quantile forests and conformal methods approximate it
CovariatesFew, and linear unless you work at itMany, non-linear, with interactions, essentially free
ExtrapolationDegrades gracefully toward the mean with growing varianceFails silently — predicts a constant with no warning
InterpretabilityThree parameters, each physically meaningfulVariable importance and partial dependence, both easy to over-read
Small samplesWorkable from about 50–100 pointsNeeds several hundred at minimum

The practical conclusion is unglamorous and correct: use both, compare them under a fair protocol, and report what you find. A candidate who can say "random forest with kriged residuals beat plain kriging by 8% RMSE under spatial block cross-validation, but I used kriging for the exceedance assessment because the client needed confidence intervals" is demonstrating exactly the judgement this unit exists to build.

Practical 10 — A fair comparison
Core everyone
  1. Assemble a covariate stack for the Kiambu soil data: elevation, slope, aspect, topographic wetness index, mean NDVI, mean annual rainfall. Extract values at the sample points.
  2. Fit a random forest on the covariates alone. Report out-of-bag R² and variable importance.
  3. Fit a second forest with X and Y added. Map both predictions. Identify the blocky artefacts in the second and explain their cause.
  4. Implement random forest regression kriging: forest on covariates, variogram and kriging of the residuals, sum the two surfaces. Check whether the residuals have spatial structure left — if they do not, say so, because that is an informative result.
  5. Score four models — OK, KED, RF, RF+RK — under identical spatial block folds. Produce one comparison table with RMSE, ME and, where available, MSDR.
  6. Score the same four under random k-fold. Report both tables side by side and quantify the optimism induced by random splitting.
  7. Write 200 words recommending one model for a specified purpose, including why you rejected the others.
Extension if you have time
  1. Implement RFsp with distance features and add it to the comparison.
  2. Use quantile regression forests to obtain prediction intervals, and compare their coverage with kriging's — do 90% intervals actually contain the truth 90% of the time?
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import GroupKFold

# Spatial blocks become GROUPS — GroupKFold then never splits a block.
groups = spatial_blocks(coords, block_size=2 * fitted_range)
gkf    = GroupKFold(n_splits=5)

results = []
for train, test in gkf.split(covars, y, groups=groups):
    rf = RandomForestRegressor(n_estimators=500, min_samples_leaf=3,
                               random_state=0, n_jobs=-1)
    rf.fit(covars[train], y[train])                # fit INSIDE the fold

    # --- RF + regression kriging of the residuals ----------------------
    res = y[train] - rf.predict(covars[train])
    ok  = OrdinaryKriging(coords[train, 0], coords[train, 1], res,
                          variogram_model="exponential")
    res_hat, _ = ok.execute("points", coords[test, 0], coords[test, 1])

    pred_rf   = rf.predict(covars[test])
    pred_rfrk = pred_rf + res_hat

    results.append({
        "rf":   np.sqrt(((y[test] - pred_rf)   ** 2).mean()),
        "rfrk": np.sqrt(((y[test] - pred_rfrk) ** 2).mean()),
    })

import pandas as pd
print(pd.DataFrame(results).mean().round(3))
Self-test
  1. Why does a random forest given only coordinates produce blocky maps?
  2. Name three distinct forms of feature leakage in a spatial ML workflow and the fix for each.
  3. A colleague reports R² = 0.92 from random k-fold CV on 300 clustered soil samples. What is your first question?
  4. When would you choose kriging over a better-scoring random forest, and on what grounds?
  5. Your residuals after the RF step show no spatial structure. What does this tell you, and what would you do?

Reading. Hengl et al. (2018), "Random forest as a generic framework for predictive modeling of spatial and spatio-temporal variables", PeerJ 6:e5518. Meyer & Pebesma (2021) on the area of applicability. Ploton et al. (2020), Nature Communications 11, on spatial validation and overoptimism in the ecological literature.

11

Week eleven

Applications and synthesis

By the end of this week you should be able to
  • Select an appropriate method for an unfamiliar applied problem and justify the choice
  • Write a geostatistical report that a non-specialist can act on
  • Present and defend an analysis under questioning

Four fields, four different pressures

Digital soil mapping. The largest current application. Sparse expensive samples, abundant cheap covariates from remote sensing and terrain analysis — the ideal setting for regression kriging and its machine-learning descendants. AfSIS and SoilGrids are the reference programmes; both publish their methods, and reading one properly is worth a week of lectures.

Mineral resource estimation. Where the subject began, and the one field where the reporting is legally constrained. Codes such as JORC and SAMREC govern how a resource may be stated publicly, and a Competent Person signs off personally. Block kriging on the mining block support, careful treatment of the high-grade tail, and conditional simulation for grade-tonnage uncertainty are all standard. This is geostatistics at its most rigorous, because being wrong is actionable.

Environmental and public health. Contamination, air quality, disease incidence. Almost always a threshold question rather than a value question, which means indicator methods and simulation rather than plain kriging. The audience is a regulator or a health officer, so communication of uncertainty is half the work.

Climate and agriculture. Rainfall and temperature surfaces from station networks, almost always with elevation as a covariate, feeding drought indices and yield models. Kenya's station network is sparse and unevenly distributed, which makes declustering and honest error reporting matter more here than in the textbooks.

What a defensible report contains

Before it is a map, an analysis is a set of decisions someone else must be able to check. Every one of these belongs in the report:

  1. The question, and the support and units of the answer.
  2. The data: source, count, sampling design, date, and known quality problems.
  3. Exploratory findings: distribution, transform applied and its justification, outliers investigated and the decision on each, trend, declustering.
  4. The variogram: estimator, binning choices, model, parameters, a plot of the fit, and directional analysis with the anisotropy conclusion.
  5. The prediction method, with the reason it was chosen over the alternatives.
  6. Validation: the full diagnostic set, under spatial blocking, with a comparison against at least one simpler method.
  7. Uncertainty: how it was quantified and what it does and does not cover.
  8. The limitations, stated plainly — including where the map should not be used.

Point 8 separates a professional from a technician. Every map has regions where the data do not support the prediction: outside the sampling envelope, in areas of a covariate range never observed, in unsampled corners. Say where they are. A report that identifies its own limits is far more likely to be trusted than one that does not.

Practical 11 — Project presentation and defence
Format

Ten minutes of presentation, five minutes of questions, in pairs. Questions come from the class and from me, and answering them is a substantial part of the assessment — the reasoning behind your choices is what is being examined, not the appearance of your maps.

You must be able to answer
  • Why this variogram model, and what would change if I halved the nugget?
  • What is your MSDR, and what does its value tell you about your error bars?
  • Show me where on this map your prediction should not be trusted, and say why.
  • You used spatial block cross-validation — what block size, and how did you choose it?
  • What would a competent colleague criticise first in this analysis?
Deliverable

A report of no more than eight pages covering the eight points above, submitted with your notebook. The notebook must run start to finish on a clean machine. Include the AI-use declaration described in the unit outline: which tools, which prompts, what they produced, and what you changed and why.

The one thing to carry out of this unit

You will forget the spherical model's formula; it is in every textbook and in Appendix B. What should stay with you is the habit the whole unit has been arranged around: every prediction carries an uncertainty, that uncertainty can be quantified, and quoting a number without one is not a professional act.

Software will keep making the computation easier. It has never made the judgement easier — deciding whether the data support the question, whether the model is defensible, whether the map should be trusted where someone is about to act on it. That judgement is what you were doing all semester, and it is what you are being hired for.

Reading. Hengl & MacMillan, Predictive Soil Mapping with R (open access). Rossiter (2018), "Past, present and future of information technology in pedometrics", Geoderma 324. The JORC Code (2012) for the mineral reporting context.

A

Appendix A

Python refresher and self-check

You met Python in GIS programming. This appendix tells you — privately, before anyone is watching — whether that is enough for what follows. Work through the twelve tasks below without help of any kind: no notes, no search, no AI assistant. The point is to find out what you actually have, and using help defeats the entire purpose.

Scoring yourself honestly
  • 10–12 comfortably — you are fine. Go straight to the Extension tasks in the practicals; the Core will not stretch you.
  • 6–9 — the usual position, and perfectly workable. Spend a weekend on the resources at the end of this appendix, particularly NumPy array indexing, which is where week 3 will hurt.
  • Below 6 — this is fixable, but not by hoping. Come and see me in week 1. There is a supported route through this unit that does not require you to become a fluent programmer, and taking it early is far better than discovering the problem in week 5.

The twelve tasks

  1. Read samples.csv into a pandas DataFrame and print the number of rows and the column names.
  2. Select all rows where soc is greater than 20 and depth is less than 30.
  3. Add a column soc_pct equal to soc divided by 10.
  4. Write a function euclid(x1, y1, x2, y2) that returns the distance between two points. Test it on a case whose answer you know.
  5. Given lists xs and ys, use a loop to build a list of distances from every point to the point (0, 0).
  6. Do the same with NumPy and no loop at all.
  7. Create a 5×5 NumPy array of random numbers. Extract the third column. Extract the bottom-right 2×2 corner.
  8. Given a 2D array D of pairwise distances, return the mean of all values strictly greater than 0 and less than 100.
  9. Plot a scatter of x against y, coloured by soc, with a colourbar and axis labels.
  10. This code raises IndexError: index 10 is out of bounds for axis 0 with size 10. Explain why, and fix it:
    for i in range(len(v) + 1): print(v[i])
  11. Write a function that takes a NumPy array and returns a new array with values above the 95th percentile replaced by the 95th percentile. Do not modify the input.
  12. Explain in one sentence what the following does, and why it is preferable to a loop:
    np.linalg.norm(pts[:, None, :] - pts[None, :, :], axis=-1)

Tasks 1–5 are the floor. Tasks 6–9 are the working level for this unit. Tasks 10–12 are what the Extension work assumes — task 12 in particular is the broadcasting idiom that appears in week 5's kriging code, and understanding it is worth more than memorising any formula in this reader.

Five things that will actually bite you

Where to go if you scored low

On using AI assistants in this unit

They are permitted, and declared. Use them for what they are good at — explaining an error message, drafting a plotting routine, reminding you of an argument name. Do not use them for the four tasks marked on paper first in the practicals, and do not use them for the self-check above.

The reason is not integrity policing; it is that these tools produce geostatistical code that runs and is wrong, in ways that are hard to see. A model will happily hand you random k-fold cross-validation on autocorrelated data, or a variogram fitted in degrees, or a kriging variance map captioned "uncertainty". Each of those has appeared in this unit as an error you are being trained to catch. If you cannot catch them, the tool is a liability rather than an asset — and the paper exercises are what build the ability to catch them.

B

Appendix B

Formula sheet

This sheet is supplied in the CATs and in the final examination. You are not expected to memorise these; you are expected to know which one to reach for, and what each symbol means.

Structure

γ̂(h) = 12N(h) ∑i=1N(h) [ z(xi) − z(xi+h) ]² γ(h) = C(0) − C(h)

Permissible models

Spherical  γ(h) = c0 + c1[1.5(h/a) − 0.5(h/a)³], h ≤ a  |   c0+c1, h > a Exponential  γ(h) = c0 + c1[1 − exp(−h/r)],   practical range 3r Gaussian  γ(h) = c0 + c1[1 − exp(−h²/r²)],   practical range r√3 Power  γ(h) = c0 + b·hα,   0 < α < 2   (unbounded — no sill)

Ordinary kriging

ẑ(x0) = ∑i λi z(xi),    ∑i λi = 1 ∑j λj γ(xi, xj) + ψ = γ(xi, x0)   ∀i σ²OK(x0) = ∑i λi γ(xi, x0) + ψ

Validation

ME = 1n ∑ ei    RMSE = √( 1n ∑ ei² )    MAE = 1n ∑ |ei| MSDR = 1n ∑ ei² / σ̂²(xi)   → 1 if the variance model is honest

Indicators and declustering

i(x; zk) = 1 if z(x) ≤ zk, else 0    E[I] = P(Z ≤ zk) mdc = 1ncells ∑j 1nj ∑i ∈ j z(xi)

Rules of thumb

Maximum lag≤ ½ the survey diagonal
Pairs per lag class≥ 30
First lag width≈ mean nearest-neighbour distance
Angular tolerance, directional±22.5°
Kriging neighbourhood16–24 points, quadrant-balanced
Nugget / sill > 0.5weak structure — reconsider
CV block size≥ 2 × variogram range
Realisations for tail probabilities≥ 500
Kenya UTM zonesEPSG:32736 (W of 36°E) · EPSG:32737 (E of 36°E)
C

Appendix C

Datasets

DatasetUsed inNotes
Meuse — 155 topsoil samples, heavy metals, Dutch floodplain1–5, 7The canonical teaching set. Ships with geopandas examples and appears throughout the literature, so you can compare your results with published ones.
Kiambu soil — soil organic carbon, pH, texture1–2, 10Compiled from AfSIS. Real Kenyan sampling, with real gaps and clustering.
Kenya rainfall stations — long-term annual means with elevation6Sparse, unevenly distributed, strongly related to elevation. The natural KED dataset.
SRTM 30 m DEM, clipped to the study areas6, 10The covariate for KED and the source of slope, aspect and wetness index.
Groundwater fluoride — Rift Valley boreholes8–9Skewed, threshold-driven, genuinely consequential. The WHO guideline is 1.5 mg/L.
Covariate stack — terrain derivatives, mean NDVI, CHIRPS rainfall10Pre-extracted at the sample points and supplied as a raster stack.
Synthetic fields — simulated from known variograms3–4, 7Generated for this unit. Because the true variogram is known, you can measure how close your fitted model came — which no real dataset allows.

All datasets are distributed through the unit's shared folder as CSV and GeoTIFF, with a README giving the CRS, units and provenance of each. If you use them outside this unit, check the licence file — AfSIS and CHIRPS are openly licensed with attribution requirements, and the Kenyan borehole data is redistributed for teaching only.

EGS 2405 Geostatistics · Course reader, edition 1.0 · Jomo Kenyatta University of Agriculture and Technology · September 2026. Corrections and suggestions are welcome and will be credited in the next edition.