Unsaubere Daten
Wenn man Daten aus unterschiedlichen Quellen konsolidiert (oder sie frei eintippen lässt, anstatt eine kontrollierte Liste vorzugeben), staunt man immer wieder, wie viele Möglichkeiten es gibt, etwas zu schreiben – bei Ländern steht zum Beispiel statt „Deutschland“ gerne mal „DE“, „Germany“ oder „Allemagne“, dazu kommen Kombinationen („Deutschland / Germany“), Abkürzungen („Deutschl.“) und Tippfehler. Weil solcher Wildwuchs in der eigenen Datenbank Recherchen und Reports unnötig verkomplizieren würde, muss man irgendwie dafür sorgen, dass am Ende eine einheitliche Schreibweise vorliegt.
Lösungsansätze
Möchte man einen „Batch“ an Daten interaktiv bereinigen, ist die Open Source-Software OpenRefine ein tolles Werkzeug; ihre „Reconcile“-Funktion schlägt mithilfe von Wikidata passende Ersetzungen vor. Solche „Reconciliation Services“ kann man auch aus der eigenen Anwendung per API aufrufen. Weil in meinem Fall aber laufend neue potentiell „unsaubere“ Daten eintreffen, die vollautomatisch und performant (ohne API-Aufruf) bereinigt werden sollen, wähle ich hier einen anderen Ansatz und setze auf „Suchen und Ersetzen“ auf Basis einer statischen Liste.
„Suchen und Ersetzen“ an sich ist technisch trivial. Spannend ist die Frage, wie ich an die Liste der zu ersetzenden Schreibweisen komme. Was bisher alles so eingegeben wurde, sehe ich zwar in den mir vorliegenden Daten. Aber die Zuordnung – dass „Sverige“ durch „Schweden“ ersetzt werden sollte und „Україна“ durch „Ukraine“ – möchte ich nicht von Hand für Hunderte von Ländern definieren.
Linked Open Data
Als Lösung bietet sich „Linked Open Data“ an: öffentliche maschinenlesbare Datensammlungen wie Wikidata, GeoNames oder die Gemeinsame Normdatei (GND) der Deutschen Nationalbibliothek. Die drei genannten enthalten jeweils Datensätze zu Ländern, siehe z. B. „Sweden“ bei Wikidata, „Schweden“ in der GND und „Kingdom of Sweden“ bei GeoNames. (Eine Auflistung vieler weiterer Ressourcen findet sich in Using Public Taxonomies von Kurt Cagle.)
Ich möchte die „amtliche“ deutsche Schreibweise verwenden und entscheide mich deshalb für die Ländercodes der GND: „Schweden“ hat dort z. B. die ID (URI) https://d-nb.info/gnd/4077258-5. Die ID bleibt stabil, auch falls die Schweden sich mal entscheiden sollten, ihr Land umzubenennen…
Leider enthält die GND außer dem deutschen und englischen Namen keine alternativen Schreibweisen für ein Land. Die sind dafür reichlich in Wikidata vorhanden: der Name in der Landessprache, ISO-Ländercodes, Kfz-Länderkennzeichen usw. Weil Wikidata auch die GND-ID kennt, lassen sich beide Datenquellen kombinieren.
(Feinheiten ignoriere ich hier, sie sind aber auch interessant: Je nach Anwendungsfall meint „Länder“ vielleicht ausschließlich „souveräne Staaten“, was z. B. Puerto Rico ausschließen würde, oder sogar nicht mehr existierende wie die DDR.)
Lokaler RDF Triplestore
Bei „Linked Open Data“ ist RDF das Standard-Datenmodell. Sowohl GND als auch Wikidata veröffentlichen die Datensätze als RDF, u. a. im Turtle-Format. Allerdings nutzen sie nicht dasselbe Vokabular (Klassen- und Feldnamen). Um die von mir gewünschte Liste mit Länderschreibweisen zu erstellen, muss ich die öffentlichen Daten filtern, umwandeln und kombinieren.
Dafür ist keine Programmierung notwendig; am einfachsten geht das in einer RDF-Datenbank („Triplestore“) mit der Abfragesprache SPARQL. Diese Datenbank brauche ich nur vorübergehend: Sobald die statische Liste fertig ist, kann ich sie wieder abschalten. Ich verwende dafür die kostenlose GraphDB von Ontotext.
Und ich muss mich für ein Vokabular entscheiden. Die von Wikidata und GND scheinen mir nicht so passend, besser lesbar finde ich Schema.org, dort gibt es bereits einen Typ Country mit den Properties name und alternateName.
Daten importieren und umwandeln
Bei GND und Wikidata funktioniert der Import unterschiedlich:
Die „GND Geographic Area Codes“ werden als Turtle-Datei bereitgestellt, die ich in GraphDB importiere. Mit einer SPARQL-Abfrage filtere und wandele ich sie ins Schema.org-Vokabular um (und importiere das Ergebnis wieder in GraphDB):
PREFIX areaCode: <https://d-nb.info/standards/vocab/gnd/geographic-area-code#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX schema: <https://schema.org/>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
CONSTRUCT {
?gndIri a schema:Country ;
schema:name ?label ;
owl:sameAs ?id ;
owl:sameAs ?geonamesIri ;
owl:sameAs ?page .
}
WHERE {
?id skos:inScheme <https://d-nb.info/standards/vocab/gnd/geographic-area-code#> .
VALUES ?continents { areaCode:XA areaCode:XB areaCode:XC areaCode:XD areaCode:XE areaCode:XH areaCode:XI areaCode:XK areaCode:XL areaCode:XM }
?id skos:broader ?continents .
?id skos:prefLabel ?label .
OPTIONAL {
?id rdfs:seeAlso ?gndIri .
FILTER (SUBSTR(str(?gndIri), 1, 22) = "https://d-nb.info/gnd/")
}
OPTIONAL {
?id rdfs:seeAlso ?geonamesIri .
FILTER (SUBSTR(str(?geonamesIri), 1, 24) = "http://www.geonames.org/")
}
OPTIONAL { ?id foaf:page ?page }
FILTER regex(str(?id), "#[A-Z]{2}-[A-Z]{2}$")
FILTER (lang(?label) = "de")
}
Wikidata lässt mich dagegen direkt auf der Seite Wikidata Query Service SPARQL-Abfragen ausführen. Diese Abfrage liefert die Zusatzinformationen zu den oben importierten GND-Ländern, das Ergebnis kann ich in GraphDB importieren:
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX schema: <https://schema.org/>
PREFIX exVocab: <https://knowledgegraph.example.com/vocab/>
CONSTRUCT {
?gndIri
schema:alternateName ?label ;
schema:alternateName ?nativeLabel ;
schema:alternateName ?altLabel ;
schema:url ?officialWebsite ;
owl:sameAs ?country ;
owl:sameAs ?wikipediaArticle ;
owl:sameAs ?geoNamesIri ;
exVocab:countryCodeAlpha2 ?iso3166Alpha2Code ;
exVocab:countryCodeAlpha3 ?iso3166Alpha3Code ;
exVocab:countryCodeLicencePlate ?licencePlateCode ;
schema:subjectOf ?spiegelTopicIri .
}
WHERE {
# instance of country
?country wdt:P31 wd:Q6256 .
?country wdt:P227 ?gndId .
OPTIONAL { ?country wdt:P1705 ?nativeLabel } .
OPTIONAL {
?country rdfs:label ?label .
FILTER(LANG(?label) IN ("en", "de", "fr"))
} .
OPTIONAL {
?country skos:altLabel ?altLabel .
FILTER(LANG(?altLabel) IN ("en", "de", "fr"))
} .
OPTIONAL { ?country wdt:P297 ?iso3166Alpha2Code } .
OPTIONAL { ?country wdt:P298 ?iso3166Alpha3Code } .
OPTIONAL { ?country wdt:P395 ?licencePlateCode } .
OPTIONAL { ?country wdt:P856 ?officialWebsite } .
OPTIONAL { ?country wdt:P1566 ?geoNamesId } .
OPTIONAL { ?country wdt:P10234 ?spiegelTopicId } .
OPTIONAL {
?wikipediaArticle <http://schema.org/about> ?country .
FILTER (SUBSTR(str(?wikipediaArticle), 1, 25) IN ("https://en.wikipedia.org/", "https://de.wikipedia.org/"))
} .
BIND (IRI(CONCAT("http://www.geonames.org/", ?geoNamesId)) AS ?geoNamesIri)
BIND (IRI(CONCAT("https://d-nb.info/gnd/", ?gndId)) AS ?gndIri)
BIND (IRI(CONCAT("https://www.spiegel.de/thema/", ?spiegelTopicId, "/")) AS ?spiegelTopicIri)
}
Möchte ich Schreibweisen aus meiner eigenen Datensammlung ergänzen, kann ich sie in einer Turtle-Datei definieren und diese in GraphDB importieren:
@prefix schema: <https://schema.org/> .
# Deutschland
<https://d-nb.info/gnd/4011882-4>
schema:alternateName
"BRD"@de,
"DEU, Deutschland"@de,
"DEU, Deutschland, Germany",
"Deutschland Germany",
"Deutschland, Germany",
"Deutschland / Germany",
"Deutschland (DE)"@de,
"Deutschland (DEU)"@de,
"GER"@en,
"Niemcy"@pl .
Liste für „Suchen und Ersetzen“ erzeugen
Sind die Daten alle in GraphDB angekommen, komme ich mit dieser Abfrage endlich an die gewünschte Liste:
PREFIX exVocab: <https://knowledgegraph.example.com/vocab/>
PREFIX schema: <https://schema.org/>
SELECT DISTINCT ?alternateLower ?name WHERE {
?id a schema:Country .
?id schema:name ?name .
{ ?id schema:alternateName ?alternateName . }
UNION { ?id exVocab:countryCodeAlpha2 ?alternateName . }
UNION { ?id exVocab:countryCodeAlpha3 ?alternateName . }
UNION { ?id exVocab:countryCodeLicencePlate ?alternateName . }
BIND (LCASE(STR(?alternateName)) AS ?alternateLower)
} ORDER BY ?alternateLower
Das Ergebnis kann ich aus GraphDB im CSV-Format herunterladen und dann in meiner Anwendung fürs „Suchen und Ersetzen“ nutzen.
Hier ein kleiner Auszug des Ergebnisses:
…
deu,Deutschland
"deu, deutschland",Deutschland
"deu, deutschland, germany",Deutschland
deutschland,Deutschland
deutschland (de),Deutschland
deutschland (deu),Deutschland
deutschland / germany,Deutschland
deutschland germany,Deutschland
"deutschland, germany",Deutschland
die niederlande,Niederlande
die philippinen,Philippinen
die staaten,USA
dj,Dschibuti
djazaïr,Algerien
dji,Dschibuti
djibouti,Dschibuti
dk,Dänemark
dm,Dominica
dma,Dominica
dnk,Dänemark
…