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


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 Gleisen


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

Bahnhöfe mit mehr als 5 Gleisen


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

Eine direkte Zugverbindung zwischen zwei Städten


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

Verbindung mit Umstieg


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

Verbindung mit Umstieg


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

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 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

Team link