Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

GIS & Geospatial Data (QGIS, GeoPandas, PostGIS)  Module 12 – Professional practice

Data quality, metadata and reproducible workflows

How to state a dataset's quality by ISO 19157's elements, write ISO 19115 metadata with lineage, pin versions so results repeat, and check data before release.

  • Intermediate
  • 15 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Assess a dataset's completeness, logical consistency, positional, thematic and temporal quality
  • Write an ISO 19115-style metadata record with lineage, accuracy, CRS and licence
  • Configure a pinned environment so that a geospatial analysis gives the same numbers next year
  • Run automated quality checks before publishing data or maps

Before you start

What you’ll learn · Before you start

What you will learn

  • Assess a dataset's completeness, logical consistency, positional, thematic and temporal quality
  • Write an ISO 19115-style metadata record with lineage, accuracy, CRS and licence
  • Configure a pinned environment so that a geospatial analysis gives the same numbers next year
  • Run automated quality checks before publishing data or maps

Before you start

On this page

A ward layer passes from a partner to your team, and a year later nobody can say why ward W06 is missing, which CRS its areas were measured in, or why the same script now prints a slightly different area. Three habits prevent that: state the data’s quality, describe it in metadata, and make every run repeatable.

Here you check a layer by the elements of ISO 19157, write its ISO 19115-1 record the way Survey of India describes its boundaries, find why numbers change between machines, and build the checks that run before anything is published.

Quality is stated element by element, with how it was measured

“Good data” says nothing. ISO 19157-1 sets out “the principles for describing the quality of geographic data”, and leaves the acceptable levels to each product’s specification (ISO/TC 211, ISO 19157-1). Its schema names the elements a quality report uses (ISO 19157-2 schema):

Element The question it answers For the ward layer
completeness (data quality): The ISO 19157 data quality element that asks whether features, attributes or relationships are missing (omission) or present in excess (commission), against what the data should hold. Is anything missing or extra? Are all 12 wards there, once each?
logical consistency: The ISO 19157 data quality element that asks whether data obeys the rules of its own structure: its concepts, value domains, format and topology, such as unique ids and closed rings. Does the data obey its own rules? Unique ids, no negative population, closed rings that do not cross themselves
positional accuracy: The ISO 19157 data quality element that states how close positions are to their true values, absolutely or relative to each other, for example as a horizontal RMSE at check points. How far are positions from the truth? The RMSE at surveyed check points
Thematic accuracy Are the classes and values right? Populations close to the census count
Temporal quality Are dates right, in order and valid? A survey year that has happened
Usability Does it fit a stated purpose? Fit for ward-level planning

Each element splits further: completeness into commission and omission, logical consistency into conceptual, domain, format and topological consistency, positional accuracy into absolute, relative and gridded data. A good report names the element, the measure, the result and how it was measured. Survey of India’s boundary database is a model: its metadata reports an absolute external positional accuracy, “Horizontal RMSE ±12.5 m” (ABDB metadata). India’s own standard for data quality, IS 18565 Part 1, is the counterpart of ISO 19157-1 (Survey of India, standards report).

One accuracy figure for everything

“Accuracy: good” or a single number hides which element it measures. Report the element, the measure and the method: “absolute positional accuracy, horizontal RMSE 2.1 m at 24 check points”, not “accurate to 2 m”.

Checkpoint

Check: Quality, element by element

  1. Question 1 of 2 Two wards of a layer share the id W05. Under which ISO 19157 element does that fault belong?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Logical consistency, because the layer breaks its own rule that ids are unique

    A duplicate id is a logical consistency fault, and here it hides a completeness fault too: W06 is missing because its id was typed as W05.

  2. Question 2 of 2 A ward's survey year reads 2031, a year that has not come. Which element does the fault belong to?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Temporal quality, as a fault of temporal validity

    ISO 19157's temporal quality covers the accuracy of a time measurement, temporal consistency and temporal validity. qa.py reports this ward under temporal validity.

Metadata says what the data is, where it came from and how far to trust it

metadata (geospatial): Data that describes a dataset for its users: what it is, its extent, quality, CRS, lineage, constraints and distribution. ISO 19115-1 defines a schema for it. is the dataset’s description for the next user. ISO 19115-1 “defines the schema required for describing geographic information and services by means of metadata”: the identification, the extent, the quality, the spatial and temporal aspects, the content, the spatial reference, the portrayal and the distribution (ISO/TC 211, ISO 19115-1). India’s national metadata standard, IS 16439, is its equivalent. Survey of India writes its boundary database’s sheets in ISO 19115-1:2014: an identifier with the edition, an abstract and a purpose, a scale of 1:50,000, the reference system “LCC- WGS84; EPSG7755”, a lineage: The part of a metadata record that tells a dataset's history: the sources it came from and the steps applied to them, from which a user judges what it is fit for. from its 1:50,000 topographic database, the RMSE, copyright constraints and the formats.

Most layers arrive with far less. This program holds the record of the town’s ward layer as the partner sent it, checks it against ten elements a user needs, and then prints the completed record:

record.py: the ward layer's metadata, checked and completed Python · meta/record.py
"""A metadata record for the town's ward layer, written with ISO 19115-1's elements, the way Survey of India's
boundary database describes itself, and a check that every element a user needs is filled in."""

REQUIRED = {  # element (ISO 19115-1 class and property): what a user decides with it
    "MD_DataIdentification.citation.title": "which dataset this is",
    "MD_DataIdentification.abstract": "what it holds",
    "MD_DataIdentification.purpose": "what it was made for",
    "MD_ReferenceSystem.referenceSystemIdentifier": "the CRS to assign",
    "MD_Resolution.equivalentScale": "the largest scale it holds at",
    "LI_Lineage.statement": "where it came from and what was done to it",
    "DQ_AbsoluteExternalPositionalAccuracy": "how far to trust a position",
    "MD_LegalConstraints.useConstraints": "what you may do with it",
    "MD_DataIdentification.credit": "the credit to show",
    "MD_Distribution.distributionFormat": "how to read it",
}

received = {
    "MD_DataIdentification.citation.title": "Wards of the town",
    "MD_DataIdentification.abstract": "12 ward polygons with population",
    "MD_ReferenceSystem.referenceSystemIdentifier": "EPSG:32643",
    "MD_Distribution.distributionFormat": "GeoPackage",
}
completed = received | {
    "MD_DataIdentification.purpose": "Ward-level planning of clinics and schools",
    "MD_Resolution.equivalentScale": "1:10,000",
    "LI_Lineage.statement": "Digitised from the town's ward map; corners checked by GNSS; the faults qa.py found corrected",
    "DQ_AbsoluteExternalPositionalAccuracy": "Horizontal RMSE 2.1 m at 24 check points",
    "MD_LegalConstraints.useConstraints": "CC BY 4.0",
    "MD_DataIdentification.credit": "Town planning office",
}

for name, record in (("as received", received), ("completed", completed)):
    missing = [element for element in REQUIRED if not record.get(element)]
    print(f"{name}: {len(REQUIRED) - len(missing)} of {len(REQUIRED)} elements")
    for element in missing:
        print(f"  missing {element}: {REQUIRED[element]}")
print()
for element, value in completed.items():
    print(f"{element}: {value}")

Output

as received: 4 of 10 elements
  missing MD_DataIdentification.purpose: what it was made for
  missing MD_Resolution.equivalentScale: the largest scale it holds at
  missing LI_Lineage.statement: where it came from and what was done to it
  missing DQ_AbsoluteExternalPositionalAccuracy: how far to trust a position
  missing MD_LegalConstraints.useConstraints: what you may do with it
  missing MD_DataIdentification.credit: the credit to show
completed: 10 of 10 elements

MD_DataIdentification.citation.title: Wards of the town
MD_DataIdentification.abstract: 12 ward polygons with population
MD_ReferenceSystem.referenceSystemIdentifier: EPSG:32643
MD_Distribution.distributionFormat: GeoPackage
MD_DataIdentification.purpose: Ward-level planning of clinics and schools
MD_Resolution.equivalentScale: 1:10,000
LI_Lineage.statement: Digitised from the town's ward map; corners checked by GNSS; the faults qa.py found corrected
DQ_AbsoluteExternalPositionalAccuracy: Horizontal RMSE 2.1 m at 24 check points
MD_LegalConstraints.useConstraints: CC BY 4.0
MD_DataIdentification.credit: Town planning office

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 record.py

The received record fills 4 of 10: it says what the layer is and how to read it, but not what it was made for, how it was made, how accurate it is, or what you may do with it. Those six are the ones a careful user asks for first. The lineage matters most: “digitised from the town’s ward map; corners checked by GNSS” tells a reader both the source and the check, and so how far the RMSE can be believed.

JSON Validator Check that a metadata record kept as JSON is valid, and list every field it holds, before you publish it.

Checkpoint

Check: An ISO 19115-1 record

  1. One question Which elements did the received record of record.py lack? Choose every one.

    Choose every answer that is right.

    Show the answer: One question

    Answer:

    • The use constraints, its licence
    • The lineage
    • The purpose
    • The positional accuracy

    The received record filled 4 of 10 elements: title, abstract, reference system and format. The six it lacked, purpose, scale, lineage, accuracy, licence and credit, are the ones that decide whether and how to use it.

The same script gives other numbers when the libraries change

A geospatial script is only partly your code. The geometry is done by GEOS, the coordinate operations by PROJ, with its own copy of the EPSG dataset, and the files by GDAL, and each environment carries its own versions. Take two environments of this track: the Python wheels behind its GeoPandas lessons, and QGIS 4.2.3 from conda-forge, which its QGIS lessons ran on. Their version reports differ:

reports.py: the versions two environments carried Python · repro/reports.py
"""Version reports of two environments of this track: the Python wheels behind its GeoPandas lessons and QGIS 4.2.3 from
conda-forge, which its QGIS lessons ran on: the geometry (GEOS), projection (PROJ, with its copy of the EPSG dataset)
and file (GDAL) libraries that each one carried."""

REPORTS = {
    "wheels": {"GEOS": "3.13.1", "PROJ": "9.8.1", "EPSG": "v12.029", "GDAL": "3.12.4"},  # shapely 2.1.2, pyproj 3.8.0, pyogrio 0.13.0
    "QGIS 4.2.3": {"GEOS": "3.14.1", "PROJ": "9.9.0", "EPSG": "v13.102", "GDAL": "3.13.3"},  # from conda-forge
}
Which libraries differ, and what a difference can change Python · repro/drift.py
"""Which libraries differ between the environments of reports.py, and what a difference can change in the results."""
from reports import REPORTS

CHANGES = {
    "GEOS": "overlay, buffers and validity: vertex order, extra vertices, last digits of areas",
    "PROJ": "which transformation is chosen, and so the coordinates",
    "EPSG": "which CRSs and transformations exist at all",
    "GDAL": "how files are read and written: drivers, field types, defaults",
}
names = list(REPORTS)
print("library " + "".join(f"{n:>14s}" for n in names))
for lib in CHANGES:
    values = [REPORTS[n][lib] for n in names]
    print(f"{lib:8s}" + "".join(f"{v:>14s}" for v in values) + ("" if len(set(values)) == 1 else "  differs"))
print()
for lib, effect in CHANGES.items():
    if len({REPORTS[n][lib] for n in names}) > 1:
        print(f"{lib}: {effect}")

Output

library         wheels    QGIS 4.2.3
GEOS            3.13.1        3.14.1  differs
PROJ             9.8.1         9.9.0  differs
EPSG           v12.029       v13.102  differs
GDAL            3.12.4        3.13.3  differs

GEOS: overlay, buffers and validity: vertex order, extra vertices, last digits of areas
PROJ: which transformation is chosen, and so the coordinates
EPSG: which CRSs and transformations exist at all
GDAL: how files are read and written: drivers, field types, defaults

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 drift.py

All four libraries differ between the two environments. Even their EPSG copies differ, v12.029 against v13.102, because each PROJ release ships its own version of the dataset: PROJ 9.6.0 carries v12.004, 9.8.1 went back to v12.029, and 9.9.0 carries v13.102 (PROJ news); pyproj 3.8.0’s wheels carry PROJ 9.8.1 (pyproj). A transformation or a CRS definition can differ between such copies, and a new GEOS can return the same polygon from another start vertex, in the other direction, or with an extra vertex on a straight edge.

Key idea

A geospatial result belongs to the libraries that made it: name GEOS, PROJ with its EPSG version, and GDAL beside every number you publish.

Checkpoint

Check: Why numbers change between environments

  1. One question One script transforms the same point in the "wheels" and the "QGIS 4.2.3" environments of reports.py and gets two slightly different answers. What is the likely reason?

    Choose one answer.

    Show the answer: One question

    Answer: Their PROJ releases carry different copies of the EPSG dataset, v12.029 and v13.102, so the chosen operation can differ

    The drift report shows all four libraries differ. PROJ and its EPSG copy decide transformations, so name both beside every coordinate you publish.

Pinned environments and tests of the shape make results repeat

Repeatable results take two habits. The first is to pin the environment, so next year’s run uses this year’s libraries:

  • Python: lock the dependencies. uv writes a lockfile from your project’s dependencies, and uv sync --locked refuses to run when the lockfile is out of date (uv); pip’s hash-checking mode installs only pinned requirements whose files match their recorded hashes, dependencies included (pip).
  • QGIS: keep heavy work in one pinned environment or a pinned container image, run its steps with qgis_process, whose --json option prints results in a structured form, and keep each workflow as a model in a .model3 file (QGIS).
  • Every run: print the versions of GEOS, PROJ, EPSG and GDAL into its log, seed anything random, and sort outputs that have no order.

The second habit is to test what matters. A file’s bytes change with library versions even when the data does not, so compare the shape: counts, areas and corners. This program compares two exports of ward W01 that differ as text:

Two exports of one ward: different text, the same shape Python · repro/same_shape.py
"""Two exports of ward W01 that a byte-for-byte test calls different and a test of the shape calls equal."""

A = "POLYGON ((500000 2004000, 502000 2004000, 502000 2006000, 500000 2006000, 500000 2004000))"
B = "POLYGON ((502000 2006000, 502000 2004000, 500000 2004000, 500000 2006000, 501000 2006000, 502000 2006000))"


def ring(wkt):
    """The ring's vertices as (x, y) floats, closing point dropped."""
    body = wkt[wkt.index("((") + 2:wkt.index("))")]
    pts = [tuple(float(v) for v in p.split()) for p in body.split(",")]
    return pts[:-1]


def area(pts):
    return abs(sum(x1 * y2 - x2 * y1 for (x1, y1), (x2, y2) in zip(pts, pts[1:] + pts[:1]))) / 2


def corners(pts):
    """Vertices where the boundary turns: a vertex in the middle of a straight edge adds nothing to the shape."""
    keep = []
    for i, (x, y) in enumerate(pts):
        (ax, ay), (bx, by) = pts[i - 1], pts[(i + 1) % len(pts)]
        if (x - ax) * (by - y) - (y - ay) * (bx - x) != 0:
            keep.append((x, y))
    return set(keep)


print(f"same text: {A == B}")
print(f"areas: {area(ring(A)):,.0f} and {area(ring(B)):,.0f} m²")
print(f"same corners, whatever the start, direction or extra vertices: {corners(ring(A)) == corners(ring(B))}")

Output

same text: False
areas: 4,000,000 and 4,000,000 m²
same corners, whatever the start, direction or extra vertices: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 same_shape.py

The second export starts at another corner, runs the other way and carries an extra vertex in the middle of an edge, so a byte-for-byte test fails. Both enclose 4,000,000 m² and have the same four corners, so a test of the shape passes, and would still catch a real change.

Checkpoint

Check: Pinning and testing the shape

  1. Question 1 of 2 What does uv sync --locked do when the lockfile no longer matches the project's dependencies?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It raises an error instead of updating the lockfile

    uv's documentation: with --locked, uv raises an error instead of updating the lockfile; with --frozen it uses the lockfile without checking whether it is up to date.

  2. Question 2 of 2 Which test of an exported polygon survives a library upgrade but still catches a real change?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Compare its area, within a tolerance, and its set of corners

    same_shape.py prints "same text: False" and the same area and corners for two exports of ward W01. Test what matters for the result: counts, areas, corners, class breaks.

Automated checks run before anything is published

Checks a person must remember get skipped. Write them as a program and run it on every release, element by element:

wards.py: the ward layer as received, with its faults Python · qa/wards.py
"""The ward layer of the track's town as a partner sent it: 12 wards of 2 km in UTM zone 43N, with five faults planted
for the quality checks to find. All values are made up."""

CRS = "EPSG:32643"


def square(col, row):
    """The ring of the ward in column col and row row, counter-clockwise, closed."""
    x, y = 500_000 + col * 2_000, 2_000_000 + row * 2_000
    return [(x, y), (x + 2_000, y), (x + 2_000, y + 2_000), (x, y + 2_000), (x, y)]


WARDS = []
for i in range(12):
    col, row = i % 4, 2 - i // 4
    WARDS.append({"id": f"W{i + 1:02d}", "ring": square(col, row), "population": 1_800 + 97 * i, "survey_year": 2024})

WARDS[5]["id"] = "W05"  # a second W05: W06's id was typed wrong
WARDS[7]["ring"] = WARDS[7]["ring"][:-1]  # W08: the ring is not closed
r = WARDS[9]["ring"]
WARDS[9]["ring"] = [r[0], r[2], r[1], r[3], r[0]]  # W10: two corners swapped, a ring that crosses itself
WARDS[2]["population"] = -120  # W03: a value no ward can have
WARDS[10]["survey_year"] = 2031  # W11: a survey year that has not happened
qa.py: the layer's quality report, as JSON and as Markdown Python · qa/qa.py
"""Quality checks of the ward layer before its release, by ISO 19157's elements: completeness, logical consistency
(conceptual, domain and topology) and temporal validity. The report is printed as JSON and as Markdown."""
import json

from wards import CRS, WARDS

EXPECTED = [f"W{i:02d}" for i in range(1, 13)]


def crosses(p, q, r, s):
    """True when segment p-q crosses segment r-s at a point inside both."""
    def turn(a, b, c):
        return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
    return turn(p, q, r) * turn(p, q, s) < 0 and turn(r, s, p) * turn(r, s, q) < 0


def ring_problem(ring):
    if len(ring) < 4 or ring[0] != ring[-1]:
        return "ring not closed"
    edges = list(zip(ring, ring[1:]))
    for i, a in enumerate(edges):
        for b in edges[i + 2:]:
            if a[0] != b[1] and crosses(*a, *b):
                return "ring crosses itself"
    return None


problems = []
ids = [w["id"] for w in WARDS]
problems += [{"element": "completeness", "id": i, "problem": "missing"} for i in EXPECTED if i not in ids]
problems += [{"element": "logical consistency", "id": i, "problem": "duplicate id"} for i in sorted(set(ids)) if ids.count(i) > 1]
if CRS != "EPSG:32643":
    problems.append({"element": "logical consistency", "id": "layer", "problem": f"CRS {CRS}, expected EPSG:32643"})
for w in WARDS:
    if (p := ring_problem(w["ring"])):
        problems.append({"element": "logical consistency", "id": w["id"], "problem": p})
    if w["population"] < 0:
        problems.append({"element": "logical consistency", "id": w["id"], "problem": f"population {w['population']}"})
    if not 2011 <= w["survey_year"] <= 2025:
        problems.append({"element": "temporal validity", "id": w["id"], "problem": f"survey year {w['survey_year']}"})

report = {"layer": "wards", "crs": CRS, "features": len(WARDS), "problems": len(problems), "ok": not problems}
print(json.dumps(report, sort_keys=True))
print("\n| Element | Ward | Problem |\n| --- | --- | --- |")
for p in sorted(problems, key=lambda p: (p["element"], p["id"])):
    print(f"| {p['element']} | {p['id']} | {p['problem']} |")

Output

{"crs": "EPSG:32643", "features": 12, "layer": "wards", "ok": false, "problems": 6}

| Element | Ward | Problem |
| --- | --- | --- |
| completeness | W06 | missing |
| logical consistency | W03 | population -120 |
| logical consistency | W05 | duplicate id |
| logical consistency | W08 | ring not closed |
| logical consistency | W10 | ring crosses itself |
| temporal validity | W11 | survey year 2031 |

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 qa.py

Five faults were planted, and the report lists six problems, because one slip, W06’s id typed as W05, makes both a duplicate and a missing ward. The JSON line is for machines: a release script stops when ok is false. The table is for people, sorted by element. Step through the whole path from a received layer to a published one:

Interactive · stage by stage The ward layer on its way to publication Step through the checks that stand between a received layer and a published one, with what each found.
The ward layer on its way to publication, step 7 of 7. Showing Layer received, Quality checks (0 problems), Fix or flag, Metadata record (10 of 10 elements), Version report, Publish.run the checks againLayer received12 wards, 4 of 10 elementsQuality checks0 problemsFix or flagthe faults, one by oneMetadata record10 of 10 elementsVersion reportGEOS, PROJ, EPSG, GDALPublishdata with its recorddata and record

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. What arrived. A partner sends the ward layer of the town, 12 features in UTM zone 43N, with a metadata record that fills 4 of the 10 elements a user needs.
  2. Checks by element. The quality checks find 6 problems, one or more for each kind of fault planted in the layer, and file each under its ISO 19157 element, from completeness to temporal validity.
  3. Fix what the data proves. A duplicate W05 and a missing W06 are one typing slip; the open ring, the crossing ring, the negative population and the survey year of 2031 go back to the partner, who knows the truth.
  4. Check again. Every fix is followed by the same checks, so a correction that breaks something else is caught at once. The layer moves on only when the report is clean.
  5. Write the record. The metadata record gets its missing elements, among them the purpose, the scale, the lineage, the positional accuracy and the licence, so it fills 10 of 10.
  6. Note the versions. The run that made the layer prints its library versions. The two environments of reports.py differ in all four libraries, so the report says which one produced these numbers.
  7. Publish. The data goes out with its record and its version report, and a reader can now tell what the layer is, where it came from, how far to trust it and how to rebuild it.

Fix only what the data itself proves, such as the mistyped id, and send the rest back to the partner. Then run the same checks again, because a fix can break something else.

Checkpoint

Check: Automated checks before release

  1. Question 1 of 2 Five faults were planted in the ward layer, but qa.py reports six problems. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: W06's id was typed as W05, which makes both a duplicate W05 and a missing W06

    The report lists completeness (W06 missing), logical consistency (W03's negative population, the duplicate W05, W08 open and W10 crossing) and temporal validity (W11's year).

  2. Question 2 of 2 Which part of qa.py's output should a release script read, and what should it do with it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The JSON line: stop the release whenever "ok" is false

    Write the checks so that their result is one machine-readable verdict, and make the release depend on it. Then fix, and run the same checks again.

Practise: the checks of a ward layer

Write four of the checks yourself; the sample tests use three wards of the track’s town:

Exercise · Easy · Python

Write the quality checks for a ward layer

Write problems(wards, expected) in checks.py. wards is a list of dicts with an id, a ring (a list of (x, y) pairs) and a population; expected is the list of ids the layer should hold. Return a sorted list of (id, problem) pairs, one for each fault:

  • (id, "missing") for every expected id that no ward has (completeness);
  • (id, "duplicate id") once for every id that two or more wards share (logical consistency);
  • (id, "ring not closed") for a ring with fewer than 4 points, or whose last point is not its first;
  • (id, "negative population") for a population below 0 (logical consistency: a value outside its domain).

A layer without faults gives []. The sample tests use three wards of the track's town.

Starter code · checks.py

def problems(wards, expected):
    """Sorted (id, problem) pairs: missing ids, duplicate ids, rings not closed, negative populations."""
    # Replace this line: collect the faults of the four checks into a list, then sort it.
    return []
The sample tests · test_checks.py
from checks import problems

SQUARE = [(0, 0), (2_000, 0), (2_000, 2_000), (0, 2_000), (0, 0)]


def ward(id_, ring=SQUARE, population=1_000):
    return {"id": id_, "ring": ring, "population": population}


def test_clean_layer():
    """three good wards give no problems"""
    assert problems([ward("W01"), ward("W02"), ward("W03")], ["W01", "W02", "W03"]) == []


def test_duplicate_hides_a_missing_ward():
    """a mistyped id makes one duplicate and one missing ward"""
    assert problems([ward("W01"), ward("W01"), ward("W03")], ["W01", "W02", "W03"]) == [
        ("W01", "duplicate id"), ("W02", "missing")]


def test_geometry_and_values():
    """an open ring and a negative population are reported with their ids"""
    layer = [ward("W01", ring=SQUARE[:-1]), ward("W02", population=-120), ward("W03", ring=[(0, 0), (1, 0), (0, 0)])]
    assert problems(layer, ["W01", "W02", "W03"]) == [
        ("W01", "ring not closed"), ("W02", "negative population"), ("W03", "ring not closed")]
A hint

Count the ids first, for example with ids = [w["id"] for w in wards] and ids.count(i). Add a "missing" pair for each expected id not in ids, a "duplicate id" pair for each id in set(ids) that occurs more than once, then loop over the wards for the ring and the population, and return sorted(found).

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then test yourself on the five ideas of this lesson, starting from the one you answered least well:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 Which of these are data quality elements of ISO 19157? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 10

    Answer:

    • Logical consistency
    • Positional accuracy
    • Completeness

    ISO 19157's elements are completeness, logical consistency, positional accuracy, thematic accuracy, temporal quality and usability, each with sub-elements.

  2. Question 2 of 10 Which of these quality statements is the most useful in a dataset's metadata?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: Absolute positional accuracy: horizontal RMSE 2.1 m at 24 check points surveyed by GNSS

    A useful statement says which element it measures, by which measure, with what result and how it was found, as Survey of India's "Horizontal RMSE ±12.5 m" does for its boundaries.

  3. Question 3 of 10 What does ISO 19115-1 define?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: The schema for describing geographic data and services by means of metadata

    ISO 19115-1 defines the schema for metadata about geographic information and services. India's national metadata standard, IS 16439, is its equivalent.

  4. Question 4 of 10 A layer's coordinates have six decimals, and its lineage says "digitised from 1:50,000 sheets". A team wants to use it for a 1:5,000 plan. What do you tell them?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: The lineage and scale say it holds at 1:50,000; the decimals do not make it fit a 1:5,000 plan

    Metadata answers fitness questions before they become mistakes: the lineage and the scale say what the data supports, whatever its coordinates look like.

  5. Question 5 of 10 Which library does the geometry work, such as overlays and buffers, under Shapely and QGIS?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: GEOS

    GEOS for geometry, PROJ with its EPSG copy for coordinates, GDAL for files: three libraries whose versions a log should record.

  6. Question 6 of 10 Which version of the EPSG dataset does PROJ 9.9.0 carry, according to PROJ's release notes?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: v13.102

    PROJ 9.6.0 carries v12.004, 9.8.1 v12.029 and 9.9.0 v13.102: three releases, three EPSG copies.

  7. Question 7 of 10 What does pip's hash-checking mode require?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: Every requirement pinned, and a hash for every package, dependencies included

    With --require-hashes, or any --hash in a requirements file, pip installs only pinned requirements whose files match their hashes.

  8. Question 8 of 10 What should every run of a geospatial pipeline print into its log, so that a result can be explained later?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: The versions of GEOS, PROJ with its EPSG copy, and GDAL

    A result belongs to the libraries that made it, so the log names them, beside the seed of anything random and the lockfile that pinned the environment.

  9. Question 9 of 10 Which of the ward layer's faults may you fix yourself, before asking the partner?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: The mistyped id, because the data proves it

    Fix only what the data itself proves, flag the rest back to the source, and run the checks again after every fix.

  10. Question 10 of 10 What comes right after a fix to a layer that failed its quality checks?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: Running the same checks again

    Check, fix, check again: the release pipeline loops until the report is clean, then the metadata and the version report go out with the data.

Interview questions

Warm-up (fresher): What is lineage in metadata, and why does a user need it?

Lineage is the dataset’s history: the sources it was made from and the steps applied to them, such as Survey of India’s note that its boundaries come from its 1:50,000 topographic database and were converted to WGS 84. A user needs it to judge fitness: a layer digitised from a 1:50,000 map cannot support a 1:5,000 plan, whatever its coordinates’ decimals suggest. It also lets a later analyst repeat or audit the work.

Warm-up (fresher): Why compare results by shape rather than byte for byte?

Because file bytes change when libraries change, even when the data does not: a new GEOS may start a polygon at another vertex or add a vertex on a straight edge, and file writers change their defaults. Compare counts, areas and corners, within a tolerance, so tests fail on real changes only.

Two working situations test these habits, areas that moved after an upgrade and a layer without metadata; the bank holds their first steps:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A district land-use report is rerun a year later on a new laptop, and 7 of 40 class areas differ from last year's published table, by up to 0.4 %. Nobody changed the code. How do you find the cause, and how do you make next year's run give the same numbers?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A partner sends a village boundary layer for a survey, with no metadata, three days before field work starts. What do you check before you use it, and what do you write down?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Which data quality elements does ISO 19157 name?

    Completeness (is anything missing or extra?), logical consistency (does the data obey its own rules?), positional accuracy, thematic accuracy, temporal quality and usability. A good report gives the element, the measure, the result and how it was measured.

  2. How should a dataset's accuracy be reported?

    By element, measure and method, as in "absolute positional accuracy, horizontal RMSE 2.1 m at 24 check points", never as "accurate to 2 m" or "accuracy: good".

  3. What does an ISO 19115-1 metadata record describe?

    A dataset's identification, extent, quality, spatial and temporal aspects, content, spatial reference, portrayal and distribution. India's national metadata standard, IS 16439, is its equivalent, and IS 18565 Part 1 is the counterpart of ISO 19157-1 for quality.

  4. What is lineage in metadata, and why does a user need it?

    The sources a dataset was made from and the steps applied to them. It shows what the data is fit for (a layer digitised from a 1:50,000 map cannot support a 1:5,000 plan, whatever its decimals suggest) and lets a later analyst repeat or audit the work.

  5. Why can the same geospatial script print different numbers on another machine?

    GEOS, PROJ with its own copy of the EPSG dataset, and GDAL do much of the work, and every environment carries its own versions: the track's two environments differed in all four, with EPSG v12.029 against v13.102. Name these versions beside every number you publish.

  6. How do you make next year's run use this year's libraries?

    Lock the Python dependencies (uv sync --locked refuses an out-of-date lockfile; pip's hash-checking mode installs only files that match their hashes), keep QGIS work in one pinned environment or container image run with qgis_process, and log the GEOS, PROJ, EPSG and GDAL versions in every run.

  7. Why test exported geometry by its shape rather than byte for byte?

    A new GEOS may start a polygon at another corner, run it the other way or add a vertex on a straight edge, so the bytes change while the data does not. Compare counts, areas and corners within a tolerance: two different texts of ward W01 both enclosed 4,000,000 m² with the same four corners.

  8. Why run a layer's quality checks as a program on every release?

    Checks a person must remember get skipped. A program reports every problem by element, gives a JSON result that stops the release when ok is false, and runs again after each fix, because a fix can break something else.

Key takeaways

  • State quality element by element, completeness, logical consistency, positional, thematic and temporal accuracy, each with its measure and method.
  • An ISO 19115-1 record says what the data is, where it came from, how accurate it is and what you may do with it; lineage and accuracy are the elements users need most.
  • Results depend on GEOS, PROJ with its EPSG copy, and GDAL, whose versions differ between environments.
  • Pin environments with lock files or images, print versions in every log, and test shapes and counts, not bytes.
  • Run the quality checks as a program on every release, and stop the release when they fail.

Terms in this lesson

completeness (data quality)
The ISO 19157 data quality element that asks whether features, attributes or relationships are missing (omission) or present in excess (commission), against what the data should hold.
lineage
The part of a metadata record that tells a dataset's history: the sources it came from and the steps applied to them, from which a user judges what it is fit for.
logical consistency
The ISO 19157 data quality element that asks whether data obeys the rules of its own structure: its concepts, value domains, format and topology, such as unique ids and closed rings.
metadata (geospatial)
Data that describes a dataset for its users: what it is, its extent, quality, CRS, lineage, constraints and distribution. ISO 19115-1 defines a schema for it.
positional accuracy
The ISO 19157 data quality element that states how close positions are to their true values, absolutely or relative to each other, for example as a horizontal RMSE at check points.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.