Spatial Machine Learning Explained: Why Location Breaks the Usual Rules
Problem statement
Every introductory machine learning course assumes rows are independent and identically distributed. Spatial data violates that assumption in a specific, measurable way: nearby rows are similar, so a dataset of 3,000 cells does not contain 3,000 independent observations.
That single fact breaks three things people rely on:
- Cross-validation scores stop meaning what they usually mean β in either direction, depending on what you will predict.
- Feature importance starts rewarding whatever encodes location.
- Transfer to a new area becomes unpredictable from any internal statistic.
Measured on a real problem β predicting vegetation index from terrain over a 10 km window in Snowdonia:
trained and validated inside the sampled area RΒ² +0.56
the same model applied to the adjacent half RΒ² -0.77
The same model, the same features, ground 5 km away. Worse than predicting the mean.
Quick answer
Three habits handle most of it:
# 1. know which task you are doing, and validate that task
# interpolation inside a sampled area -> random k-fold is fine
# extrapolation to new ground -> hold out a real region
# 2. be deliberate about coordinates as features
X = terrain_features # transfers, weakly
X = np.column_stack([terrain, coords]) # fits better, transfers worse
# 3. report where the model is applicable, not just how good it is
applicable = feature_space_distance(X_new, X_train) < threshold
Step-by-step solution
1. Establish what the model is for
There are three spatial prediction tasks, and they are not variations on one problem:
- Interpolation β fill gaps between samples in an area you have surveyed. This is the easy case, and ordinary random cross-validation estimates it well.
- Extrapolation in space β apply a model fitted here to ground over there. This is the hard case, and no internal validation estimates it.
- Extrapolation in time β apply a model fitted on past data to the future. Different again, and it needs a chronological split.
Almost every disappointment in applied spatial ML comes from validating the first and deploying the second.
2. Expect the sample size to be smaller than the row count
Neighbouring cells are highly correlated, so a dense sample contains far fewer independent observations than rows.
The measured consequence: going from 100 to 10,000 training cells improved the true map RΒ² only from +0.413 to +0.599. A hundredfold increase in rows bought 0.19 RΒ².
That is the signature of correlated data. Extra rows from the same neighbourhoods add very little, and any confidence interval computed from the row count is far too narrow.
3. Treat coordinates as a deliberate choice
Adding raw x and y to a random forest improved the in-area map from RΒ² 0.559 to 0.721 β a genuine improvement for interpolation, because coordinates proxy for unmeasured local structure.
The same features under block cross-validation improved much less, 0.221 to 0.324, because a coordinate learned here does not mean anything over there.
Neither "always include coordinates" nor "never include coordinates" is right. Include them for interpolation; drop them, or replace them with transferable covariates, for anything that must move.
4. Expect feature importance to shift
terrain only slope 0.459, elevation 0.282, tpi 0.106,
northness 0.089, eastness 0.065
terrain + x,y slope 0.395, elevation 0.230, x 0.129, y 0.112,
northness 0.055, tpi 0.046, eastness 0.033
Adding coordinates pushed x and y into third and fourth place, and pushed TPI from third down to sixth. The physical story changed because two location proxies absorbed variance that the terrain variables had been explaining.
Importance is a statement about this fitted model on this data, not about the world.
5. Report applicability, not just accuracy
A model's accuracy is a single number about the data it saw. What a user of the map needs is where the prediction is supported β which is a raster, not a number.
Code examples
Example 1 β a spatial train/test split that means something
import numpy as np
from sklearn.cluster import KMeans
def spatial_split(coords, test_fraction=0.3, n_blocks=10, seed=0):
"""Hold out whole blocks, so the test set is genuinely unseen ground."""
blocks = KMeans(n_clusters=n_blocks, random_state=seed,
n_init=10).fit_predict(coords)
rng = np.random.default_rng(seed)
order = rng.permutation(n_blocks)
sizes = np.array([(blocks == b).sum() for b in order])
cumulative = np.cumsum(sizes) / sizes.sum()
held_out = set(order[cumulative <= test_fraction].tolist())
if not held_out:
held_out = {int(order[0])}
test = np.isin(blocks, list(held_out))
print(f" {len(held_out)} of {n_blocks} blocks held out, "
f"{test.mean():.1%} of rows")
return ~test, test
Ten blocks and a 30% hold-out is a reasonable default. Five blocks makes the score unstable β measured standard deviation 0.106 across block layouts β while thirty blocks are small enough that neighbouring folds start sharing information again.
Example 2 β how many independent observations do you have?
import numpy as np
from scipy.spatial import cKDTree
def effective_sample_size(coords, values, range_m):
"""Rows within one autocorrelation range of each other are not independent."""
tree = cKDTree(coords)
neighbours = tree.query_ball_point(coords, r=range_m)
counts = np.array([len(n) for n in neighbours])
effective = float((1.0 / counts).sum())
print(f" {len(coords):,} rows")
print(f" median neighbours within {range_m:.0f} m: {np.median(counts):.0f}")
print(f" effective sample size: {effective:,.0f} "
f"({effective / len(coords):.1%} of the rows)")
return effective
This is a rough estimate, not a theorem, and it is enough to stop you quoting a confidence interval based on 250,000 pixels when the survey has a few hundred independent looks at the landscape.
Example 3 β checking the covariates transfer before you rely on them
import numpy as np
def covariate_shift(X_train, X_new, names):
"""Are the new area's covariates inside the range the model was fitted on?"""
rows = []
for i, name in enumerate(names):
train, new = X_train[:, i], X_new[:, i]
lo, hi = np.percentile(train, [1, 99])
outside = float(((new < lo) | (new > hi)).mean())
shift = float((new.mean() - train.mean()) / (train.std() + 1e-12))
rows.append({"feature": name, "outside_train_range": round(outside, 4),
"mean_shift_sd": round(shift, 3)})
flag = " <-- extrapolating" if outside > 0.1 else ""
print(f" {name:12} {outside:6.1%} outside train range, "
f"mean shifted {shift:+.2f} sd{flag}")
return rows
Run this before applying a model anywhere new. A covariate whose values in the new area are 30% outside the training range guarantees the model is extrapolating, and tree models handle that particularly badly β they cannot predict outside the range of the training target at all.
Explanation
Why correlated rows inflate confidence rather than accuracy
Two cells twenty metres apart have almost the same elevation, slope and vegetation. As training data the second one adds almost nothing; as a count it doubles your apparent sample.
Every statistic computed from the row count is therefore too confident: standard errors too small, confidence intervals too narrow, p-values too significant. Accuracy itself is unaffected β the model is as good as it is β but every statement about how certain you are is wrong.
That is why the measured curve is so flat: 100 to 10,000 training cells moved the true RΒ² from 0.413 to 0.599. The extra 9,900 rows were mostly repeats of information already present.
Why coordinates help and hurt at the same time
A random forest given x and y can carve the study area into regions and fit a different local mean in each. That is a legitimate and powerful way to capture spatial structure the measured covariates miss β soil, land use history, microclimate.
It is also, precisely, a lookup table for this area. Applied elsewhere, the splits are meaningless: the model has learned "north of 5,880,000 is greener", which is a fact about Snowdonia rather than about the world.
The measurement shows both effects. In-area map RΒ² rose from 0.559 to 0.721; block CV, which simulates moving, rose much less, 0.221 to 0.324.
There is a middle path: replace raw coordinates with covariates that travel β distance to coast, elevation, aspect, rainfall β which encode the reason a location differs rather than its identity.
Why tree models fail strangely under extrapolation
A random forest predicts by averaging training targets in a leaf. It therefore cannot predict a value outside the range of the training targets, and for a feature value beyond the training range it simply reuses the most extreme split it learned.
The consequence is peculiar rather than catastrophic-looking: predictions in a new area saturate at a plausible-looking constant. Nothing raises an error, and the map looks flat rather than wrong.
That is visible in the transfer test. With coordinates included, the new-region RΒ² was β0.095 β the model had become nearly constant over the new region, because every eastern x value was outside the western training range. Without coordinates, it confidently applied a wrong relationship and scored β0.771. The more "conservative" model was the one that had been broken into constancy.
Why applicability matters more than accuracy
A single accuracy number describes the average over the data you happened to have. A map is used pixel by pixel, and the pixels vary enormously in how well they are supported.
The useful deliverable is therefore two rasters: the prediction, and a mask of where the covariates resemble the training data closely enough for the prediction to mean anything. See Extrapolation in space explained.
Edge cases or notes
- Row count is not sample size. Estimate the effective sample size from the autocorrelation range.
- Coordinates are a deliberate choice, not a default. Include for interpolation, exclude for transfer.
- Tree models cannot extrapolate the target. Predictions saturate at the training range.
- Linear models extrapolate confidently and wrongly. Different failure, same cause.
- Scale matters. A model fitted on 20 m cells does not apply to 1 km cells; the covariates mean different things.
- Class imbalance is usually spatial. Rare classes cluster, so a random split can put an entire class in one fold.
- Blocks must exceed the autocorrelation range, or spatial CV still leaks.
- Report the CV scheme with the score. A bare RΒ² is not interpretable.
Internal links
- Spatial leakage explained: why random cross-validation lies β the measurements behind this page
- How to do spatial cross-validation in Python β the implementation
- Spatial features explained: turning geometry into columns β covariates that travel
- Extrapolation in space explained: the area of applicability β the mask to ship with the map
- Feature importance says coordinates are the best predictor β the coordinate trap in detail
- My model scores 0.95 in testing and fails in the field β the symptom
- Spatial autocorrelation explained β the structure underneath all of this
- How to evaluate a spatial model honestly β what to report instead of one number
FAQ
What makes machine learning on spatial data different?
Nearby observations are correlated, so rows are not independent. That breaks cross-validation, inflates confidence, and makes transfer to new areas unpredictable from internal statistics.
Should I include coordinates as features?
For predicting inside the sampled area, yes β they raised the true map RΒ² from 0.559 to 0.721 here. For a model that must work elsewhere, no: coordinates are a lookup table for one place.
Why does more training data barely help?
Because correlated rows repeat information. Going from 100 to 10,000 training cells raised the true map RΒ² only from 0.413 to 0.599.
Can I use a random forest for spatial prediction?
Yes, and it is a sensible default. Be aware it cannot predict outside the range of the training target, so in a new area its predictions saturate rather than error.
How do I know if my model will work in a new region?
Test it on a real held-out region. No cross-validation scheme computed from one region can tell you β block CV promised +0.385 where reality was β0.771.
What is the effective sample size?
Roughly, the number of independent neighbourhoods your samples cover, rather than the number of rows. Estimate it from the autocorrelation range before quoting any confidence interval.
Is spatial machine learning just interpolation with more steps?
For the interpolation task, largely yes, and a good interpolator is often competitive. Machine learning earns its place when covariates carry information that geometry alone cannot.