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

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 in einer bestimmten Stadt

Bahnhöfe mit Gleisen

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 Gleisen

Bahnhöfe mit mehr als 5 Gleisen

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

Bahnhöfe mit mehr als 5 Gleisen

Stationen eines Zuges

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

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

Eine direkte Zugverbindung zwischen zwei Städten

Verbindung mit Umstieg

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

Verbindung mit Umstieg

Bahnsteige höher als 55cm

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

Verbindung mit Umstieg

In einer bestimmten Zeit erreichbar

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

In unter einer Stunde von Hamburg erreichbar

Es sieht ein wenig verwirrend aus, dass der IC2311 und der ICE 209 jeweils in Hamburg HBF und Hamburg-Haburg halten.

Importieren

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)

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)

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)

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)

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)

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)

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)

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

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

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

Team