Skip to content

Case study: publishing a code list as linked data

A statistical agency has a labour force survey documented in DDI. Someone outside the agency wants to know whether the survey's employment-status categories mean the same thing as theirs.

Nothing in the DDI file answers that. This page walks through making it answerable, using only the ddigraph command line and a small DDI file.

Every code block below runs. They are executed as part of the test suite, against a fixture that ships with the repository.

The starting point

The survey is a DDI-L file. Ask what is in it:

ddigraph detect "$FIXTURE"

Step 1: turn it into RDF

ddigraph export "$FIXTURE" --format turtle -o survey.ttl
head -20 survey.ttl

Two things in that output matter.

The subjects are DDI URNs. urn:ddi:test.org:cat1:1.0 is the identifier the agency already assigned. ddigraph does not invent a new one, so the same object keeps the same name wherever it travels.

The categories are skos:Concept, and they point at their code list with skos:inScheme. Any SKOS tool understands that, with no knowledge of DDI.

Step 2: check it is what you think

Shapes come from the same schema that builds the Neo4j constraints:

ddigraph export "$FIXTURE" --format turtle -o survey.ttl
ddigraph shapes -o shapes.ttl --flavor lifecycle
python - <<'PY'
import pyshacl
import rdflib

data = rdflib.Graph().parse("survey.ttl", format="turtle")
shapes = rdflib.Graph().parse("shapes.ttl", format="turtle")
conforms, _graph, report = pyshacl.validate(data, shacl_graph=shapes)
print("conforms:", conforms)
assert conforms, report
PY

Send shapes.ttl along with the data and the recipient can run the same check before trusting it.

Step 3: answer the actual question

The original question was whether two code lists agree. That is a SPARQL query now:

import os

import rdflib

import ddigraph

ddigraph.export(os.environ["FIXTURE"], "survey.ttl", format="turtle")
graph = rdflib.Graph().parse("survey.ttl", format="turtle")

rows = graph.query("""
    PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

    SELECT ?scheme ?label
    WHERE {
        ?concept skos:inScheme ?scheme ;
                 skos:prefLabel ?label .
    }
    ORDER BY ?label
""")

for scheme, label in rows:
    print(f"{label} -> {scheme}")

If the DDI file records external references for its categories, those become skos:exactMatch. That is the link that says "this category is the same as EuroVoc's", and it is what makes the answer machine-checkable rather than a matter of reading two PDFs.

Step 4: keep the graph

The RDF is not a dead end. It reads back:

import os

import ddigraph
from ddigraph.rdf.reader import read_graph

ddigraph.export(os.environ["FIXTURE"], "survey.ttl", format="turtle")

labels = sorted({node.label for chunk in read_graph("survey.ttl") for node in chunk.nodes})
print("recovered types:", labels)

So a colleague can send you Turtle, and you can load it into Neo4j exactly as if it had been the original XML:

ddigraph load survey.ttl

Why the details matter

Two things in the output above are easy to get wrong, and both decide whether the result is worth sending to anyone.

The predicate is a published term, not a relationship name. Emitting ddi:USES_CODELIST would carry a database convention into a format meant for exchange, and no consumer would recognise it.

The subject keeps its URN intact. Flattening urn:ddi:test.org:cat1:1.0 into something like urn_ddi_test.org_cat1_1.0 destroys the one identifier the DDI world already agrees on.

Get either wrong and the file still parses — it simply joins to nothing.