Precision Standards in Epi-Mapping: Deterministic Coordinate Governance for Surveillance Pipelines
This guide is part of Spatial Epidemiology Fundamentals & Data Standards, and covers how to make coordinate precision a deterministic, tolerance-checked engineering control rather than an incidental property of whatever the geocoder returned. The operational purpose is narrow and consequential: fix the spatial resolution at which case locations enter analysis, so that cluster-detection sensitivity, exposure-surface fidelity, and de-identification thresholds are all set by policy and recorded for audit — not silently inherited from a float column.
Concept & Epidemiological Alignment
Precision in epi-mapping is the retained spatial resolution of a coordinate after ingestion, expressed jointly as decimal-degree places (in a geographic CRS) and a meter-level tolerance band (in a projected CRS). It is distinct from accuracy: a geocoder can return a coordinate to eight decimal places (false precision) that is still 200 m from the true address (low accuracy). A surveillance pipeline must control both, but they fail in opposite directions — over-precision leaks identity, under-precision dissolves signal.
The mapping between decimal-degree precision and ground resolution is non-linear and latitude-dependent. For longitude, the ground distance subtended by one degree contracts toward the poles:
where is latitude, the longitude delta in degrees, and the latitude delta in degrees. The -th decimal place of longitude therefore resolves to meters — roughly 1.1 m at the equator for five decimals, but only ~0.8 m at latitude 45°. This is why a single global “round to N decimals” rule under-specifies the control: the analytical resolution it yields drifts with the study extent’s latitude.
When to enforce precision — and at which stage. Fix precision once, at ingestion, immediately after coordinate extraction and before spatial indexing, joins, or the Coordinate Reference Systems for Public Health reprojection that distance-based methods depend on. Enforcing it later — at export, or inside a Global & Local Moran’s I Implementation or Getis-Ord Gi* Hotspot Detection run — means the over-precise coordinates have already propagated into intermediate artifacts and the audit trail no longer reflects what was analyzed. Precision is an ingestion gate, not an output filter.
Assumptions that must hold in surveillance data. Three conditions make a precision standard defensible. First, the disclosed resolution must be coarser than or equal to the analytical resolution actually used — you may analyze at 5 decimals internally and publish at 3, never the reverse. Second, rounding must be deterministic and topology-preserving: Python’s native round() uses banker’s rounding and operates scalar-by-scalar, which can detach a vertex from a shared boundary; production code rounds vectorized with a fixed mode. Third, the chosen tolerance must exceed the input’s own positional error — rounding to 1.1 m resolution is meaningless when the geocoder’s stated accuracy is ±50 m, and it manufactures an illusion of certainty that biases downstream confidence intervals.
Precision Decision Logic
The first decision is the target decimal-degree precision, driven by the analytical scale and the strictest applicable disclosure rule. The table maps the common bands to their ground resolution at the equator and the surveillance contexts in which each is defensible.
| Decimal places | Resolution (equator) | Defensible use | Disclosure posture |
|---|---|---|---|
| 2 | ~1.1 km | Statewide/regional choropleths, public dashboards | Safe for public release; below address resolution |
| 3 | ~110 m | County and ZIP-level syndromic surveillance | Public release; aligns with HIPAA 3-digit ZIP intent |
| 4 | ~11 m | Census-tract outbreak mapping, neighborhood rates | Controlled access; aggregate before publishing |
| 5 | ~1.1 m | Facility-level analysis, contact tracing under a DUA | Restricted; never published un-aggregated |
| 6+ | ≤0.11 m | Engineering survey precision | Excessive for epi mapping; strip on ingestion |
The second decision is the rounding strategy, which depends on the CRS state at the moment of enforcement:
Round in decimal degrees only while the data are still geographic (EPSG:4326). Once geometries are projected for distance work, switch to meter-grid snapping at the tolerance, because a fixed decimal-degree rule applied to projected meters is meaningless. Detailed threshold-selection patterns and the regulatory crosswalk for each band are developed in Setting Decimal Precision for Disease Coordinate Mapping.
Spatial Data Prerequisites
Before any precision rule can be applied deterministically, the input must satisfy a minimal contract:
- Geometry type: Point geometries for case-level coordinates; the precision control here governs vertex coordinates, so polygon and line inputs are snapped vertex-wise to the same tolerance grid.
- CRS state: A declared, valid CRS. A
GeoDataFramewithcrs is Nonecannot be precision-controlled, because the meaning of a “decimal place” is undefined without knowing whether the axis is degrees or meters. Reject undeclared-CRS inputs at the gate rather than guessing EPSG:4326. - Positional accuracy metadata: Per-record or per-source positional uncertainty (geocoder match type, GPS HDOP, or a fixed source tolerance). Precision must not be set finer than this value.
- Minimum scale coherence: The chosen tolerance should be a single value per analytical layer; mixing 3-decimal regional points with 5-decimal facility points in one layer corrupts any distance computation that spans them.
- Stable identifier: A
case_id(or equivalent) for deterministic ordering and audit reconciliation — rounding must be reproducible row-for-row.
Type enforcement and format normalization that feed this gate are covered in Spatial Data Types & Formats.
Production Implementation
The pipeline below enforces precision at ingestion. It validates the input contract with pydantic, applies deterministic vectorized rounding in the correct units for the CRS state, refuses to over-specify beyond the recorded positional accuracy, and emits a structured audit record for every rejected or coerced row. It is runnable against a pinned stack.
# Pinned: geopandas==0.14.4, pyproj==3.6.1, shapely==2.0.4,
# pandas==2.2.2, numpy==1.26.4, pydantic==2.7.1
import hashlib
import json
import logging
from typing import Optional
import numpy as np
import pandas as pd
import geopandas as gpd
from pyproj import CRS
from pydantic import BaseModel, field_validator, ValidationError
logger = logging.getLogger("epi.precision")
class IngestCoordinate(BaseModel):
"""Validates one case coordinate against the ingestion contract."""
case_id: str
lat: float
lon: float
pos_accuracy_m: float = 50.0 # default geocoder rooftop-miss budget
@field_validator("lat")
@classmethod
def _lat_range(cls, v: float) -> float:
if not -90.0 <= v <= 90.0:
raise ValueError(f"latitude {v} out of range")
return v
@field_validator("lon")
@classmethod
def _lon_range(cls, v: float) -> float:
if not -180.0 <= v <= 180.0:
raise ValueError(f"longitude {v} out of range")
return v
def _degrees_for_accuracy(pos_accuracy_m: float, lat: float) -> int:
"""Coarsest decimal-degree precision whose cell is <= positional accuracy.
Prevents false precision: never retain resolution finer than the input
is actually known to. Returns a decimal-place count in [2, 5].
"""
metres_per_deg_lon = 111_320.0 * np.cos(np.radians(lat))
for decimals in range(2, 6): # 2..5 dp
cell_m = metres_per_deg_lon * (10 ** -decimals)
if cell_m >= pos_accuracy_m:
return decimals
return 5 # floor at 5 dp (~1.1 m)
def enforce_geographic_precision(
df: pd.DataFrame,
policy_decimals: int = 3,
) -> gpd.GeoDataFrame:
"""Round geographic (EPSG:4326) coordinates deterministically at ingestion.
The retained precision is min(policy_decimals, accuracy-bounded decimals):
policy sets the disclosure ceiling, positional accuracy sets the floor of
meaningfulness. Both decisions are logged per record for audit.
"""
df = df.sort_values("case_id", kind="stable").reset_index(drop=True)
rows, rejected = [], []
for rec in df.to_dict("records"):
try:
c = IngestCoordinate(**rec)
except ValidationError as exc:
rejected.append({"case_id": rec.get("case_id"), "error": exc.errors()})
continue
acc_decimals = _degrees_for_accuracy(c.pos_accuracy_m, c.lat)
decimals = min(policy_decimals, acc_decimals)
# Deterministic round-half-to-even via numpy (vectorized-equivalent,
# topology-stable: identical inputs always map to identical outputs).
lat_r = float(np.round(c.lat, decimals))
lon_r = float(np.round(c.lon, decimals))
rows.append({
"case_id": c.case_id,
"lat": lat_r,
"lon": lon_r,
"precision_dp": decimals,
"precision_capped_by": "accuracy" if acc_decimals < policy_decimals else "policy",
})
if rejected:
logger.warning("precision gate rejected %d records", len(rejected))
for r in rejected:
logger.info("[AUDIT][REJECT] %s", json.dumps(r, default=str))
gdf = gpd.GeoDataFrame(
rows,
geometry=gpd.points_from_xy([r["lon"] for r in rows], [r["lat"] for r in rows]),
crs="EPSG:4326",
)
return gdf
def snap_projected_precision(
gdf: gpd.GeoDataFrame,
tolerance_m: float = 10.0,
) -> gpd.GeoDataFrame:
"""Snap projected geometries to a fixed meter grid (precision in a planar CRS).
Use AFTER reprojection to an equal-distance/equal-area CRS. A decimal-degree
rule is meaningless on projected metres; snap to a tolerance grid instead.
"""
if gdf.crs is None or gdf.crs.is_geographic:
raise ValueError("snap_projected_precision requires a projected CRS")
g = gdf.copy()
# shapely 2.x set_precision: deterministic grid snapping, topology-aware.
g["geometry"] = g.geometry.set_precision(grid_size=tolerance_m)
return g
def precision_manifest(gdf: gpd.GeoDataFrame, policy_decimals: int,
tolerance_m: float) -> dict:
"""Serialize the precision configuration with a reproducible config hash."""
cfg = {
"policy_decimals": policy_decimals,
"projected_tolerance_m": tolerance_m,
"input_crs": str(gdf.crs),
"record_count": int(len(gdf)),
"precision_dp_distribution": (
gdf["precision_dp"].value_counts().sort_index().to_dict()
if "precision_dp" in gdf.columns else {}
),
}
cfg["config_sha256"] = hashlib.sha256(
json.dumps(cfg, sort_keys=True).encode()
).hexdigest()
logger.info("[AUDIT][MANIFEST] %s", json.dumps(cfg))
return cfg
The two stages are deliberately separated: enforce_geographic_precision runs while coordinates are still in EPSG:4326 and reasons in decimal places; snap_projected_precision runs after the canonical reprojection and reasons in meters. The precision_capped_by column records, per record, whether the disclosure policy or the input’s own accuracy was the binding constraint — exactly the kind of provenance a reviewer needs to confirm no record was over-specified.
Parameter Selection & Tuning
policy_decimals(disclosure ceiling): Set from the strictest applicable rule for the publication target — 2 for public dashboards, 3 for ZIP/county release, 4–5 only behind a data use agreement. This is a governance decision, not a tuning knob; document it in the manifest and the data dictionary, not in code comments alone.pos_accuracy_m(meaningfulness floor): Source it from the geocoder’s match-type table (rooftop ≈ 5–10 m, interpolated street ≈ 25–50 m, ZIP centroid ≈ 1–5 km) or from GPS HDOP where available. When it is unknown, default conservatively high so the gate cannot manufacture precision the data do not support.tolerance_m(projected grid): Choose the finest operationally meaningful scale for the intended analysis — e.g. 250–500 m for vector-control radii, 10–25 m for facility catchments. It must exceed the residual error of the datum transformation applied during reprojection.- Latitude reference for
_degrees_for_accuracy: For wide study extents, evaluate the accuracy-bounded decimals at the highest-latitude record rather than a single nominal latitude, so the coarsest defensible precision governs the whole layer and no record is over-specified. - IQR multiplier for outlier flags (validation stage): A multiplier of 3.0 flags gross geocoding failures (transposed lat/lon, default-to-(0,0)) without flagging genuine peripheral cases; tighten to 1.5 only when the at-risk extent is known to be compact.
Edge Cases & Failure Modes
- Banker’s rounding detaching shared vertices. Scalar
round()applied independently to a boundary vertex shared by two polygons can round the two copies differently, opening a sliver gap. Round vectorized with a single fixed mode, and for polygon/line inputs preferset_precision(which is topology-aware) over coordinate-wise rounding. - CRS-less input silently assumed EPSG:4326. A feed delivered as bare lat/lon with
crs=Nonewill be treated as geographic by libraries that auto-assume — but if it is actually State Plane feet, every “decimal place” is nonsense. Reject undeclared CRS at the gate; never infer. - Transboundary CRS drift across a multi-zone extent. A multi-state surveillance area spanning UTM zones cannot share one projected tolerance grid without distortion. Snap precision per zone, or reproject the whole extent to a single equal-area CRS before projected snapping, and assert the authority code at ingestion.
- Over-precision re-inflated by a later join. Rounding at ingestion is undone if a downstream join pulls full-precision coordinates from an un-gated reference table. Gate every coordinate source, not just the primary feed, and store only the rounded copy in derived layers.
- Zero-island and degenerate coordinates. Records defaulting to (0.0, 0.0) on geocode failure survive range validation. Flag the null-island coordinate explicitly and quarantine rather than round.
- Memory for N > 50k. Per-row
pydanticvalidation is but object-heavy; for large batches validate in chunks and apply the vectorizednp.round/set_precisionoperations on whole columns rather than row dictionaries, keeping the validation objects out of the hot path.
Automated Validation & Statistical Tolerance
Precision formatting alone does not guarantee a clean layer: a coordinate can be correctly rounded yet topologically invalid or a gross outlier. After the precision gate, run a topology-and-tolerance pass that repairs invalid geometries and flags — but does not silently delete — coordinate outliers for human audit review.
# Pinned: geopandas==0.14.4, shapely==2.0.4, numpy==1.26.4
import numpy as np
import geopandas as gpd
from shapely.validation import make_valid
def run_spatial_validation(gdf: gpd.GeoDataFrame,
tolerance_m: float = 15.0,
iqr_k: float = 3.0) -> gpd.GeoDataFrame:
"""Repair geometry validity and flag coordinate outliers for audit.
Requires a PROJECTED crs so the IQR fences are in metres. Flags are
advisory ('OUTLIER'/'PASS'); records are never auto-deleted, only routed
to review, preserving the immutable-raw guarantee.
"""
if gdf.crs is None or gdf.crs.is_geographic:
raise ValueError("validation requires a projected CRS for metric fences")
g = gdf.copy()
g["geometry"] = g.geometry.apply(make_valid)
cx, cy = g.geometry.centroid.x.to_numpy(), g.geometry.centroid.y.to_numpy()
(q1x, q3x), (q1y, q3y) = np.percentile(cx, [25, 75]), np.percentile(cy, [25, 75])
iqr_x, iqr_y = q3x - q1x, q3y - q1y
out = (
(cx < q1x - iqr_k * iqr_x) | (cx > q3x + iqr_k * iqr_x) |
(cy < q1y - iqr_k * iqr_y) | (cy > q3y + iqr_k * iqr_y)
)
g["validation_flag"] = np.where(out, "OUTLIER", "PASS")
pass_rate = float((g["validation_flag"] == "PASS").mean() * 100)
print(f"[VALIDATION] pass={pass_rate:.1f}% tol={tolerance_m}m k={iqr_k}")
return g
Calibrate tolerance_m and iqr_k against ground-truth geocoding-accuracy reports and historical cluster-detection sensitivity, not against arbitrary defaults — the same flag that catches a transposed coordinate can suppress a genuine peripheral case if the fences are drawn too tight.
Compliance & Audit Controls
Precision governance is defensible only when it is reproducible by a reviewer holding the inputs and the recorded configuration. Four controls make it so:
- Deterministic execution. Sort by a stable
case_idbefore rounding so tie-breaking is byte-reproducible, and use a fixed rounding mode (round-half-to-even) so the same input always yields the same output across runs and platforms. - Immutable raw storage. Persist original, un-rounded coordinates in a read-only staging layer; all precision transformations write to derived layers with explicit lineage. Audit reconstruction requires the raw inputs to still exist.
- Configuration logging with a hash. Emit the
precision_manifest— policy decimals, projected tolerance, input CRS, the per-decimal-place distribution, and a SHA-256 of the config — for every ingestion cycle. The hash lets a reviewer confirm two runs used identical precision policy without diffing the data. - Output schema and metadata. Carry
precision_dp,precision_capped_by, andvalidation_flagas first-class columns, and attach ISO 19115 lineage fields (process step, source positional accuracy, transformation parameters) so the precision decision survives interagency handoff. The regulatory crosswalk that maps each decimal band to HIPAA, GDPR, and state reporting obligations is maintained in Compliance Mapping Frameworks.
Production Precision Checklist
By fixing resolution at ingestion, reasoning about precision in the correct units for each CRS state, and recording every coercion in a hashed manifest, surveillance teams keep geospatial outputs statistically valid, disclosure-compliant, and reproducible under audit.
Related Topics
- Setting Decimal Precision for Disease Coordinate Mapping — threshold selection and the regulatory crosswalk for each decimal band.
- Coordinate Reference Systems for Public Health — projection selection the projected-tolerance stage depends on.
- Spatial Data Types & Formats — type enforcement and format normalization that feed the precision gate.
- Compliance Mapping Frameworks — mapping precision bands to HIPAA, GDPR, and state reporting obligations.
- Spatial Epidemiology Fundamentals & Data Standards — the ingestion architecture this precision gate sits inside.