DB Stations und Zugdaten als Graph

Auf dem DeutscheBahn OpenData Hackathon 2019 haben wir Graphdatenbanken ergründet und geschaut, wie man darin mit OpenData arbeiten kann.
Verwendet haben wir dafür Neo4j. Weiter unten beschreibe ich die Installation und das Importieren von Daten.
Bahnhöfe einer bestimmten Stadt link
Für den Einstieg eine kleine Abfrage. Dabei suchen wir im Graphen nach einer Stadt Wetzlar
die im (LOCATED
) Bundesland Hessen
liegt.
MATCH
(s {name: 'Hessen'})
-[loc:LOCATED]->(l {name: 'Wetzlar'})
-[bas:BASED]->(st)
RETURN s, loc, l, bas, st
Bahnhöfe mit Gleisen link
Um noch zusätzlich alle Gleise zu sehen muss der Pfad einfach nur erweitert werden.
MATCH
(s {name: 'Hessen'})
-[loc:LOCATED]->(l {name: 'Wetzlar'})
-[bas:BASED]->(st)
-[pla:PLACED]->(tr:Track)
RETURN s, loc, l, bas, st, pla, tr
Bahnhöfe mit mehr als 5 Gleisen link
Diese Abfrage kann nun auch durch Wenn (WHERE
) Bedingungen erweitert werden. Im folgenden Beispiel zählen wir z.B. alle Verbindungen zwischen einer Station und ihren Gleisen. Wenn diese dann höher als 5 ist, wird der ganze Pfad angezeigt. Hat das Bundesland oder der Ort keine entsprechenden Bahnhöfe wird der Pfad nicht mit übergeben.
MATCH
(s:State)
-[loc:LOCATED]->(l:Location)
-[bas:BASED]->(st:Station)
-[pla:PLACED]->(t:Track)
WITH s, loc, l, bas, st, count(pla) as tCount
WHERE tCount > 5
RETURN s, loc, l, bas, st, tCount
Stationen eines Zuges link
Mit importierten Zugdaten können wir uns nun vom Zug aus über die Verbindungen Ankunft ARRIVAL
und Abfahrt DEPARTURE
zu allen angefahrenen Gleisen vorarbeiten. Von hier an können, mit dem von vorherigen Beispielen bekannten Graphen, die Station, Stadt und Bundeland geholt werden.
MATCH
(t:Train {name: "ICE 709"})
-[sto:ARRIVAL|:DEPARTURE]->(tr:Track)
<-[pla:PLACED]-(s:Station)
<-[bas:BASED]-(l:Location)
<-[loc:LOCATED]-(st:State)
RETURN t, sto, tr, pla, s, bas, l, loc, st
Direkte Zugverbindung finden link
So langsam werden die Abfragen komplexer. Es kann z.B. abgefragt werden wie ich direkt von Hamburg
nach Augsburg
komme. Dafür werden wieder die Verbindungen im Graphen abgefragt.
Interessant ist hier, dass es vom Ort 2 Möglichkeiten für einen Start gibt und diese auch gefunden werden.
MATCH
(st:Location {name: 'Hamburg'})
-[stBas:BASED]->(stStation:Station)
-[stPla:PLACED]->(stTrack:Track)
-[stDeparture:DEPARTURE]->(t:Train)
-[enArrival:ARRIVAL]->(enTrack:Track)
<-[enPla:PLACED]-(enStation:Station)
<-[enBas:BASED]-(en:Location {name: 'Augsburg'})
RETURN st, stBas, stStation, stPla, stTrack, stDeparture,
t, enArrival, enTrack, enPla, enStation, enBas, en
Verbindung mit Umstieg link
Neo4j als Graphdatenbank hat hier helfende Funktionen die eine Abfrage deutlich leichter machen. Mit der Funktion shortestPath
kann der schnellste Weg von einem definierten Start- und Endpunkt gefunden werden. Dabei kann eine Art Whitelist definiert werden, welche Relationen dafür infrage kommen und mit maximal wie vielen Schritten zum Ziel gekommen werden muss.
Hier erlauben wir nur Verbindungen zwischen Stadt - Bahnhof (:BASED
), Bahnhof - Gleis (:PLACED
), Gleis - Zug (:ARRIVAL
), Zug - Gleis (:DEPARTURE
).
MATCH
(st:Location {name: 'Hamburg'}),
(en:Location {name: 'Aachen'}),
p = shortestPath((st)-[:BASED|:PLACED|:ARRIVAL|:DEPARTURE*..30]-(en))
RETURN p
Bahnsteige höher als 55cm link
Bei Gesprächen über die Daten kam herraus, dass es ungefähr zwei Bahnsteighöhen gibt und hier oft die Frage im Raum steht, ob das Gleis von diesem Zug angefahren werden kann. Dies können wir auch mit beliebigen Zusatzinfos visualisieren.
Schön zu sehen ist, dass bei großen Bahnhöfen wie in der Mitte Frankfurt am Main viele Gleise eine geeignete Höhe haben.
MATCH (st:Station)-[pl:PLACED]-(tr:Track)
WITH st, pl, tr
WHERE tr.height > 55
RETURN st, pl, tr
In einer bestimmten Zeit erreichbar link
Zum Abschluss eine eher lustige Abfrage. Welche Bahnhöfe kann ich von Hamburg in unter 1 Stunde erreichen? Die Regionalbahn kommt hier natürlich nicht so weit wie der ICE oder IC.
MATCH
(stLoc:Location {name: 'Hamburg'})
-[stBas:BASED]->(stSta:Station)
-[stPla:PLACED]->(stTra:Track)
-[de:DEPARTURE]->(t:Train)
-[ar:ARRIVAL]->(enTra:Track)
<-[enPla:PLACED]-(enSta:Station)
WITH stLoc, stBas, stSta, stPla, stTra, de, t, ar, enTra, enPla, enSta
WHERE ar.arrival > de.departure AND ar.arrival-de.departure < 3600000
RETURN stLoc, stBas, stSta, stPla, stTra, de, t, ar, enTra, enPla, enSta, de.departure-ar.arrival
Es sieht ein wenig verwirrend aus, dass der IC2311 und der ICE 209 jeweils in Hamburg HBF und Hamburg-Haburg halten.
Importieren link
Die folgenden Befehle importieren Daten von der Deutschen Bahn und stellen dies als Graph da. Im Neo4j Dashboard können die Befehle nacheinander in der Konsolenleiste ausgeführt werden.
Orte (l:Locations) link
Der folgende Befehl lädt die Orte mit einem Bahnhof vom Portal der DeutschenBahn und importiert diese. Wenn dieser fertig ausgeführt ist, sollten alle Orte als Punkte in der Ausgabe zusehen sein.
LOAD CSV WITH HEADERS FROM '{URL}' AS row
FIELDTERMINATOR ';'
WITH row WHERE NOT row.Ort IS null
MERGE (l:Location {name: row.Ort})
SET l.name = row.Ort
RETURN l
URL: http://download-data.deutschebahn.com/static/datasets/bahnsteig/DBSuS-Bahnsteigdaten-Stand2019-03.csv
Bundesländer (s:State) link
Der folgende Befehl lädt die Bundesländer mit einem Bahnhof vom Portal der DeutschenBahn und importiert diese. Wenn dieser fertig ausgeführt ist, sollten alle Bahnhöfe als Punkte in der Ausgabe zusehen sein.
LOAD CSV WITH HEADERS FROM '{URL}' AS row
FIELDTERMINATOR ';'
WITH row WHERE NOT row.Bundesland IS null
MERGE (s:State {name: row.Bundesland})
SET s.name = row.Bundesland
RETURN s
URL: http://download-data.deutschebahn.com/static/datasets/bahnsteig/DBSuS-Bahnsteigdaten-Stand2019-03.csv
Relationen zwischen Orten und Bundesländern (rel:LOCATED) link
Der folgende Befehl verbindet die Orte mit den Bundesländern. Dadurch entsteht der erste Graph.
LOAD CSV WITH HEADERS FROM '{URL}' AS row
FIELDTERMINATOR ';'
WITH row WHERE NOT row.Bundesland IS null AND NOT row.Ort IS null
MATCH (l:Location {name: row.Ort})
MATCH (s:State {name: row.Bundesland})
MERGE (s)-[rel:LOCATED]->(l)
RETURN count(rel)
URL: http://download-data.deutschebahn.com/static/datasets/bahnsteig/DBSuS-Bahnsteigdaten-Stand2019-03.csv
Importieren der Bahnhöfe (st:Station) link
Nachdem die Metadaten existieren, haben wir die eigentlichen Bahnhöfe st:Station
angelegt.
LOAD CSV WITH HEADERS FROM '{URL}' AS row
FIELDTERMINATOR ';'
WITH row WHERE NOT row.Bundesland IS null AND NOT row.Ort IS null
MERGE (st:Station {name: row.Station})
SET st.name = row.Station, st.ds100 = row.`Bf DS 100 Abk.`, st.id = row.`Bf. Nr.`
RETURN st
URL: http://download-data.deutschebahn.com/static/datasets/bahnsteig/DBSuS-Bahnsteigdaten-Stand2019-03.csv
Relationen der Bahnhöfe (rel:BASED) link
Als nächstes haben wir die Bahnhöfe st:Station
einem Ort zugewiesen l:Location
.
LOAD CSV WITH HEADERS FROM '{URL}' AS row
FIELDTERMINATOR ';'
WITH row WHERE NOT row.Bundesland IS null AND NOT row.Ort IS null
MATCH (l:Location {name: row.Ort})
MATCH (st:Station {name: row.Station})
MERGE (l)-[rel:BASED]->(st)
RETURN count(rel)
URL: http://download-data.deutschebahn.com/static/datasets/bahnsteig/DBSuS-Bahnsteigdaten-Stand2019-03.csv
Importieren der Gleise (t:Track) link
Jeder Bahnhof hat in unserem Graph auch seine Gleise als eigene Entität.
LOAD CSV WITH HEADERS FROM '{URL}' AS row
FIELDTERMINATOR ';'
WITH row
MERGE (t:Track {name: row.`örtliche Bezeichnung`, station_id: row.Bahnhofsnummer})
SET t.name = row.`örtliche Bezeichnung`,
t.height = toInteger(row.`Höhe Bahnsteigkante (cm)`),
t.length = toInteger(row.`Nettobaulänge(m)`),
t.number = row.Gleisnummer,
t.track = row.Bahnsteig,
t.station_id = row.Bahnhofsnummer
RETURN count(t)
URL: http://download-data.deutschebahn.com/static/datasets/bahnsteig/DBSuS-Bahnsteigdaten-Stand2019-03.csv
Relation zwischen Bahnhof und Gleis (rel:PLACED) link
Dazu muss auch wieder die Verbindung zum Bahnhof importiert werden.
MATCH (t:Track),(s:Station)
WHERE t.station_id = s.id
CREATE (s)-[rel:PLACED]->(t)
RETURN count(t)
EVA Nummern zu Bahnhöfen link
Damit die Bahnhöfe auch mit den Zügen gemappt werden können, muss aus einer anderen Datenquelle jeder Bahnhof anhand seiner DS100 Nummer der EVA Nummer zugeordnet werden.
LOAD CSV WITH HEADERS FROM '{URL}' AS row
FIELDTERMINATOR ';'
MATCH (st:Station {ds100: row.DS100})
SET st.eva_nr = row.EVA_NR
RETURN st
URL: http://download-data.deutschebahn.com/static/datasets/haltestellen/D_Bahnhof_2017_09.csv
Installation mit Docker link
Neo4j kann natürlich auch normal auf dem Computer installiert werden. Für den Hackathon haben wir einen Docker Container mit Neo4J aufgesetzt. Die Logindaten im Beispiel sind der Username neo4j
und das Passwort test
. Dies darf gerne geändert werden.
docker run \
--name testneo4j \
--rm \
-p7474:7474 -p7687:7687 \
-d \
-v $HOME/neo4j/data:/data \
-v $HOME/neo4j/logs:/logs \
-v $HOME/neo4j/import:/var/lib/neo4j/import \
-v $HOME/neo4j/plugins:/plugins \
--env NEO4J_AUTH=neo4j/test \
--env NEO4J_dbms_memory_pagecache_size=2G \
neo4j:latest