Datum Shifts and Why Coordinates Move by Metres
A datum fixes where the coordinate origin sits relative to the Earth and what shape the Earth is assumed to have. Two datums make different choices, so the same physical point has two different coordinate pairs — commonly tens to hundreds of metres apart, and the difference is not a constant offset you can subtract. That non-constancy is the whole reason transformations exist, and why a three-parameter shift is an approximation rather than an answer.
This page belongs to the Datum Transformations & Projection Accuracy guide.
Why This Matters
Datum errors are the quietest class of spatial defect. A projection mistake moves data to the wrong ocean and is noticed in a minute. A datum mistake moves it a few metres — inside the error people attribute to GPS, invisible at any sensible map scale, and consequential exactly where it matters: a boundary, a setback, a utility clearance.
The confusion is understandable, because both a datum and a projection are described by the same object in most software. EPSG:4326 and EPSG:4258 both look like “lat/long, WGS 84-ish” and differ by a datum whose coordinates diverge by centimetres today and by a growing amount over the years. EPSG:4277 and EPSG:4326 differ by well over a hundred metres in places.
Prerequisites
pyproj3.4+ and a workingproj.db- Knowing which datum your source data is actually on — not just which projection
- A projected reference system for measuring the shift in metres
- The parent guide’s distinction between datum, projection and transformation
Primary Method
# Measure the datum shift across an extent, in metres
from pyproj import Transformer, Geod
def shift_field(source_crs: str, target_crs: str, samples: list[tuple[float, float]]):
"""
For each (lon, lat) sample, report how far the point moves when converted
between two geographic datums. Distances are geodesic, so they are metres.
"""
t = Transformer.from_crs(source_crs, target_crs, always_xy=True)
geod = Geod(ellps="WGS84")
out = []
for lon, lat in samples:
lon2, lat2 = t.transform(lon, lat)
_, _, metres = geod.inv(lon, lat, lon2, lat2)
out.append((lon, lat, metres))
return out
for lon, lat, metres in shift_field(
"EPSG:4277", "EPSG:4326", # OSGB36 -> WGS 84
[(-5.0, 50.0), (-2.0, 52.0), (0.0, 54.0), (-4.0, 57.0)],
):
print(f"({lon:6.2f}, {lat:5.2f}) shift {metres:7.2f} m")
Run that over a handful of points spread across the extent and the key property is visible immediately: the shift is different at every one. It is a field, not an offset.
Step-by-Step Walkthrough
1. Separate the datum from the projection
Ask pyproj what the datum is, rather than inferring it from the projection’s name.
from pyproj import CRS
for code in (4326, 4258, 4277, 27700, 3857):
crs = CRS.from_epsg(code)
print(f"EPSG:{code:<6} {crs.name}")
print(f" datum: {crs.datum.name}")
print(f" projected: {crs.is_projected}")
Two projected systems can share a datum — a UTM zone and a national grid over the same country often do not — and two geographic systems can differ only by datum, which is the case that catches people.
2. Understand what makes the shift vary
A datum is realised by a network of physical control points, and those points were surveyed with the instruments of their era. The resulting reference frame is not a perfect rigid body relative to a modern satellite-based one: it stretches and rotates slightly, differently in different regions, because the original survey accumulated error across the country.
That is why the shift is a field. A three-parameter transformation models it as pure translation; a seven-parameter one adds rotation and scale; a grid-based one stores the residual at every node and interpolates. Each step models more of the real deformation.
3. Find out which model your environment will use
The route PROJ picks depends on what data it can find, which means the accuracy you get is a property of the machine rather than of your code.
from pyproj.transformer import TransformerGroup
tg = TransformerGroup("EPSG:27700", "EPSG:4326")
print("will use:", tg.transformers[0].description)
print("accuracy:", tg.transformers[0].accuracy, "m")
for missing in tg.unavailable_operations:
print("UNAVAILABLE (grid not installed):", missing.name)
4. Check whether the shift matters for your data
The honest test is to compare the shift against the accuracy of the measurements. A handheld receiver giving three metres paired with a transformation accurate to three metres roughly doubles the error budget — noticeable but often acceptable. The same transformation paired with centimetre-grade survey data throws away everything the equipment provided.
CAPTURE_ACCURACY_M = 0.02 # survey grade
route_accuracy = tg.transformers[0].accuracy or 999
if route_accuracy > CAPTURE_ACCURACY_M * 5:
raise RuntimeError(
f"transformation accurate to {route_accuracy} m, but capture is "
f"{CAPTURE_ACCURACY_M} m — install the grid or record the degradation"
)
5. Record which datum the data is on
Most of the damage from datum shifts comes from data whose datum was never recorded and had to be guessed. A container that states its reference system precisely — including the datum, which the WKT definition carries — can always be transformed correctly later. One that says “lat/long” cannot.
Verification
# Round-tripping through another datum must return to the start
t_out = Transformer.from_crs("EPSG:4277", "EPSG:4326", always_xy=True)
t_back = Transformer.from_crs("EPSG:4326", "EPSG:4277", always_xy=True)
lon, lat = -2.0, 52.0
lon2, lat2 = t_out.transform(lon, lat)
lon3, lat3 = t_back.transform(lon2, lat2)
geod = Geod(ellps="WGS84")
_, _, residual = geod.inv(lon, lat, lon3, lat3)
assert residual < 0.01, f"round trip lost {residual:.4f} m"
print(f"round trip closes to {residual * 1000:.2f} mm")
A round trip that does not close means the two directions took different routes — usually because a grid is available in one direction and not the other, which is a real and confusing configuration. Testing it costs nothing and catches an environment problem that would otherwise show up as a slow drift across repeated processing.
Alternative Approaches or Edge Cases
Datums that move over time. Plate-fixed datums such as ETRS89 are tied to a continent and drift relative to global ITRF-based frames by a couple of centimetres a year. For survey-grade work over a decade that is not negligible, and the correct handling involves an epoch as well as a datum — which most file formats, including GeoPackage, have no field for.
Data with no recorded datum. Where the datum is unknown, the only sound approach is to determine it empirically from control points whose coordinates you have in a known system. Guessing WGS 84 because the numbers look like degrees is how a systematic hundred-metre error enters a dataset and stays.
Small extents. Over a site of a few hundred metres, the shift is effectively constant, and a single offset really does capture it. That is why local engineering grids work, and why the “just subtract the difference” instinct is right at site scale and wrong at national scale.
Troubleshooting
Everything is offset by a similar amount in a similar direction
Cause: A datum mismatch — the data is on one datum and being interpreted as another. Fix: Identify the source datum, then transform rather than translate. A consistent offset is the fingerprint; the residual variation across the extent is what tells you it is a datum and not a simple coordinate error.
The offset changed after a library upgrade
Cause: A newer PROJ preferred a different route, or the published transformation was revised. Fix: Compare the two routes with projinfo, decide which one your published data should use, and pin it explicitly — see the parent guide for the pinning pattern.
Two datasets of the same area do not line up, and both claim EPSG:4326
Cause: One of them is not really on WGS 84 — most often it was converted with a three-parameter shift while the other used a grid, so both are labelled 4326 and sit a metre or two apart. Fix: This is unresolvable from the files alone; it needs the provenance of each. It is the strongest argument for recording the transformation operation alongside the container.
Frequently Asked Questions
Is WGS 84 a datum or a coordinate system?
Both names get used for both things, which is much of the confusion. WGS 84 is a datum; EPSG:4326 is a geographic coordinate reference system that uses it. Adding to the difficulty, the WGS 84 datum has itself been realised several times, and the realisations differ by centimetres — so “WGS 84” is precise enough for metre-level work and ambiguous below that.
How big can a datum shift get?
Hundreds of metres. Datums defined before satellite geodesy were fitted to a region rather than the globe, so their origin can sit a long way from the Earth’s centre of mass. Shifts of one to two hundred metres between a mid-twentieth-century national datum and WGS 84 are ordinary, and some older datums differ by considerably more.
Does a datum shift affect area and distance?
Only very slightly. A shift moves every point in a small area by nearly the same amount, so the relative geometry — and therefore area, distance and shape — is almost unchanged. What changes is absolute position, which is what matters for anything that must line up with the ground or with another dataset. This is why an area report can be right while every boundary in it is in the wrong place.
Related
- Datum Transformations & Projection Accuracy — parent guide: making the route deterministic
- How to Bundle PROJ Grid Files with an Application — shipping the data the accurate route needs
- Verifying a Reprojection with Known Control Points — measuring the residual rather than trusting the label
- Choosing Between EPSG:4326 and a Projected CRS — the separate question of which system to store in