Aligning Spatial Metadata with ISO 19157
Your dataset passes its internal checks, but a partner agency asks a harder question: can you prove, in standard-conformant metadata, that the layer meets a stated quality level? ISO (International Organization for Standardization) 19157 is the standard that answers this — it defines a vocabulary of data-quality elements and measures, and it prescribes how to report the results of evaluating them. This page maps each ISO 19157 element to a concrete, runnable validation measure, gives you a conformance checklist, and shows how to encode the outcome as ISO 19115/19139 metadata so the quality statement travels with the data. It extends the compliance framework alignment workflow from generic rule-writing to producing formal, machine-readable quality reports.
Prerequisites
- A clear data quality scope. ISO 19157 requires every quality report to state what it applies to — the whole dataset, a feature type, or a single attribute. Decide the scope before measuring; a completeness ratio is meaningless without knowing the population it counts against.
- An authoritative expected count or reference. Completeness and positional/thematic accuracy need a comparison basis: an expected feature count from a source-of-truth register, or surveyed control points. Without a reference, these elements can only be reported as “not evaluated”.
- Python 3.10+ with GeoPandas 0.14+, Shapely 2.0+, and lxml 5.0+ — the first three compute measures,
lxmlbuilds the metadata XML. - Awareness of the encoding version. ISO 19139 is the older XML encoding; ISO 19115-3:2016 is its successor. Confirm which your catalogue (GeoNetwork, pycsw, CKAN) ingests before you commit to element names — the namespaces differ.
- Gotcha — measures need units. Every ISO 19157 quantitative measure carries a
valueUnit. A positional accuracy of “0.4” is unusable; “0.4 metre” is conformant. Record units at the point of measurement, not at encoding time.
Step-by-Step Procedure
Step 1 — Select the applicable quality elements and their measures
ISO 19157 defines six data-quality elements. Map each to a standard measure with an explicit unit and acceptance level. Not every element applies to every dataset — record the ones you evaluate and mark the rest “not evaluated” rather than omitting them.
# iso19157_plan.py — the measurement plan for one dataset scope
QUALITY_PLAN = {
"scope": "cadastral_parcels (feature type)",
"measures": [
{"element": "completeness_omission", "measure": "rate_of_missing_items",
"unit": "ratio", "accept": {"op": "<=", "value": 0.02}},
{"element": "logical_consistency_topological", "measure": "invalid_geometry_count",
"unit": "count", "accept": {"op": "==", "value": 0}},
{"element": "logical_consistency_domain", "measure": "value_domain_nonconformance_rate",
"unit": "ratio", "accept": {"op": "<=", "value": 0.0}},
{"element": "positional_accuracy_absolute", "measure": "rmse_horizontal",
"unit": "metre", "accept": {"op": "<=", "value": 0.5}},
{"element": "temporal_quality_currency", "measure": "max_record_age_days",
"unit": "day", "accept": {"op": "<=", "value": 365}},
],
}
Verification: Confirm each measure has a unit and a numeric accept threshold. An element without an acceptance level can produce a DQ_QuantitativeResult but not a DQ_ConformanceResult — decide deliberately whether that is acceptable for the scope.
Step 2 — Compute the completeness and logical-consistency measures
These elements are computable from the data alone. The function returns raw measure values keyed to match the plan, ready for threshold evaluation.
import geopandas as gpd
def measure_intrinsic(gdf: gpd.GeoDataFrame, expected_count: int,
domain: dict[str, set]) -> dict:
"""Compute completeness, topological, and domain measures from the data itself."""
n = len(gdf)
# Completeness — omission relative to an authoritative expected count
missing_rate = max(expected_count - n, 0) / max(expected_count, 1)
# Logical consistency — topological (OGC validity via GEOS)
invalid_geometry_count = int((~gdf.geometry.is_valid).sum())
# Logical consistency — domain (controlled-vocabulary conformance)
domain_violations = 0
for col, allowed in domain.items():
if col in gdf.columns:
domain_violations += int((~gdf[col].isin(allowed)).sum())
domain_rate = domain_violations / max(n, 1)
return {
"rate_of_missing_items": round(missing_rate, 4),
"invalid_geometry_count": invalid_geometry_count,
"value_domain_nonconformance_rate": round(domain_rate, 4),
}
Verification: On a dataset whose expected count you know exactly, rate_of_missing_items should be 0.0, and invalid_geometry_count should agree with a SELECT count(*) ... WHERE NOT ST_IsValid(geom) in PostGIS, because both use GEOS.
Step 3 — Compute the positional accuracy measure against a reference
Positional accuracy needs external truth. This step joins measured features to surveyed control points by identifier and computes horizontal root-mean-square error (RMSE) in the layer’s projected CRS.
import numpy as np
def measure_positional_rmse(gdf: gpd.GeoDataFrame, control: gpd.GeoDataFrame,
key: str) -> dict:
"""Horizontal RMSE between measured points and surveyed control points (same CRS)."""
if gdf.crs != control.crs:
raise ValueError("Reproject both layers to a common projected CRS before measuring RMSE")
merged = gdf.merge(control[[key, "geometry"]], on=key, suffixes=("", "_ref"))
dx = merged.geometry.x.values - merged["geometry_ref"].x.values
dy = merged.geometry.y.values - merged["geometry_ref"].y.values
rmse = float(np.sqrt(np.mean(dx**2 + dy**2)))
return {"rmse_horizontal": round(rmse, 3), "sample_size": len(merged)}
Verification: sample_size must be large enough to be representative — ISO 19157 measures reference a sampling method, so record how many control points were used. If it is zero, the identifier join failed and the measure must be reported as “not evaluated”, never as 0.0.
Step 4 — Evaluate every measure against its acceptance level
Combine the measured values and apply each threshold to produce the conformance verdicts. This mirrors the checklist an auditor would fill in by hand.
import operator
OPS = {"==": operator.eq, "<=": operator.le, ">=": operator.ge}
def evaluate(plan: dict, measured: dict) -> list[dict]:
rows = []
for m in plan["measures"]:
value = measured.get(m["measure"])
if value is None:
rows.append({**m, "value": None, "conforms": None, "note": "not evaluated"})
continue
conforms = OPS[m["accept"]["op"]](value, m["accept"]["value"])
rows.append({
"element": m["element"], "measure": m["measure"], "unit": m["unit"],
"value": value, "threshold": f'{m["accept"]["op"]} {m["accept"]["value"]}',
"conforms": conforms,
})
return rows
Verification: Print the rows as a table and confirm every element from the plan appears exactly once, each with either a conforms verdict or an explicit not evaluated note. Silent absence of an element is the most common conformance-report defect.
Step 5 — Encode the results as ISO 19115/19139 metadata
Serialize each evaluated element into a DQ_Element with both a DQ_QuantitativeResult (the value) and a DQ_ConformanceResult (the verdict). The snippet below builds the data-quality fragment with lxml; embed it under MD_Metadata/dataQualityInfo.
from lxml import etree
GMD = "http://www.isotc211.org/2005/gmd"
GCO = "http://www.isotc211.org/2005/gco"
NSMAP = {"gmd": GMD, "gco": GCO}
def build_dq_element(row: dict) -> etree._Element:
el = etree.Element(f"{{{GMD}}}report")
# Element type is chosen per ISO 19157 category; DQ_DomainConsistency shown as example
dq = etree.SubElement(el, f"{{{GMD}}}DQ_DomainConsistency")
# Quantitative result: the measured value with its unit
qr = etree.SubElement(etree.SubElement(dq, f"{{{GMD}}}result"),
f"{{{GMD}}}DQ_QuantitativeResult")
val = etree.SubElement(etree.SubElement(qr, f"{{{GMD}}}value"),
f"{{{GCO}}}Record")
val.text = str(row["value"])
# Conformance result: the pass/fail verdict against the stated threshold
cr = etree.SubElement(etree.SubElement(dq, f"{{{GMD}}}result"),
f"{{{GMD}}}DQ_ConformanceResult")
passed = etree.SubElement(cr, f"{{{GMD}}}pass")
etree.SubElement(passed, f"{{{GCO}}}Boolean").text = str(bool(row["conforms"])).lower()
return el
def build_data_quality(rows: list[dict]) -> bytes:
root = etree.Element(f"{{{GMD}}}DQ_DataQuality", nsmap=NSMAP)
reports = etree.SubElement(root, f"{{{GMD}}}report") if False else root
for row in rows:
if row.get("conforms") is not None:
root.append(build_dq_element(row))
return etree.tostring(root, pretty_print=True, xml_declaration=True, encoding="UTF-8")
Verification: Validate the emitted XML against your catalogue’s schema (GeoNetwork accepts an upload and reports schema errors). Confirm each DQ_ConformanceResult/pass value is true or false and that no evaluated measure is missing from the document.
Interpreting Results
A conformance report is not a single pass/fail — it is a per-element statement. Read it element by element:
| DQ element | Automatable? | Typical measure | Conformance question |
|---|---|---|---|
| Completeness (omission/commission) | Yes | Rate of missing/excess items | Is the feature count within tolerance of the authoritative source? |
| Logical consistency — topological | Yes | Invalid geometry count | Are all geometries OGC-valid? |
| Logical consistency — domain | Yes | Domain nonconformance rate | Do all coded values fall in the controlled vocabulary? |
| Positional accuracy | Semi (needs control points) | Horizontal RMSE | Is displacement within the stated metre tolerance? |
| Temporal quality | Yes | Max record age | Is every record current within the SLA window? |
| Thematic accuracy | Semi (needs ground truth) | Misclassification rate | Do classes match verified truth? |
The value of encoding both a quantitative and a conformance result is that a downstream consumer with a stricter tolerance can re-apply their own threshold to your measured value without re-running the evaluation. When an element is genuinely not evaluated — no control points, for instance — say so explicitly in the lineage. A silent omission reads to an auditor as a hidden failure. For the broader mapping of these same elements to executable rules across INSPIRE and FGDC, see compliance framework alignment, and cross-reference the sibling guidance on aligning local GIS data with INSPIRE standards where the same measures feed INSPIRE’s data-quality reporting.
Gotchas & Edge Cases
“Not evaluated” is a valid, required outcome. ISO 19157 explicitly supports reporting that an element was not assessed. Reporting 0 or pass for an element you never measured is a false statement in a compliance artifact. Always distinguish “measured and conformant” from “not measured”.
Units are part of the measure, not decoration. A positional accuracy measured in degrees (EPSG:4326) and one measured in metres (a projected CRS) are not comparable. Compute distance and area measures only after reprojecting to a suitable projected CRS, and record that CRS in the lineage.
Domain conformance depends on a versioned vocabulary. If your controlled vocabulary changes, a value that conformed last quarter may not conform now. Pin the vocabulary version in the metadata so the conformance verdict is reproducible against the list that was in force at evaluation time.
ISO 19139 and ISO 19115-3 element names differ. Copying element paths from a 19139 example into a 19115-3 pipeline produces namespace errors. Decide the target encoding first; do not mix the two in one document.
Completeness needs a defined population. A “rate of missing items” is only meaningful against a stated expected count. If the authoritative register itself is incomplete, your completeness measure inherits that uncertainty — document the source of the expected count in the lineage, as covered under defining spatial data quality policies.
When to Escalate
The single-dataset approach here suits a periodic conformance report. Escalate to a heavier process when:
- You must report at scale across a catalogue. Hand-built XML per dataset does not scale to hundreds of layers. Generate metadata programmatically and load it into GeoNetwork or pycsw, driving the measures from a rule engine as in building rule engines with GeoPandas.
- Regulatory audits require signed lineage. Encoding results is not the same as certifying who ran the evaluation and against which data version. Wire the evaluation into a role-gated pipeline as described in compliance framework alignment.
- Positional and thematic accuracy need statistical rigour. RMSE from a handful of control points is indicative, not defensible. A formal accuracy assessment needs a designed sampling plan and confidence intervals — engage a surveyor and record the sampling method in the metadata.
Frequently Asked Questions
What is the difference between ISO 19157 and ISO 19115?
ISO 19157 defines the data-quality model — the six quality elements, the concept of a quality measure, and how to report evaluation results. ISO 19115 defines the broader geographic metadata schema that describes a dataset, and it contains the data-quality section (DQ_DataQuality) where ISO 19157 results are recorded. ISO 19139 (now largely superseded by ISO 19115-3) is the XML encoding that serializes that metadata to a file. In practice you use ISO 19157 to decide what to measure and ISO 19115/19139 to write those measurements down.
Which ISO 19157 quality elements can be validated automatically?
Completeness (omission and commission), logical consistency (topological, domain, format, conceptual), and temporal quality are almost entirely automatable from the data itself using GeoPandas, Shapely, PostGIS, or GDAL. Positional accuracy and thematic accuracy usually require an external reference — surveyed control points or a ground-truth classification — so they are semi-automatable: the comparison is scripted, but acquiring the reference is a manual or survey step. Record which elements were evaluated automatically versus by inspection in the metadata lineage.
Do I need a DQ_ConformanceResult and a DQ_QuantitativeResult for every measure?
Not always, but the pairing is best practice. A DQ_QuantitativeResult records the raw measured value (for example, a completeness ratio of 0.982) while a DQ_ConformanceResult records the pass/fail verdict against a stated acceptance threshold. The quantitative value lets a downstream user apply their own threshold; the conformance flag gives an immediate answer. Recording both makes the metadata reusable across consumers with different quality requirements.
Related:
- Compliance Framework Alignment — the parent workflow for mapping standards to executable rules
- Aligning Local GIS Data with INSPIRE Standards — the same quality measures feeding INSPIRE reporting
- Defining Spatial Data Quality Policies — setting the acceptance thresholds these measures are evaluated against
Back to Compliance Framework Alignment