Um Vorhersagen über Zugverspätungen machen zu können, werden zunächst Daten gesammelt. Der Rhein-Main-Verkehrsverbund bietet eine offene API an. Dabei wird eine Access ID benötigt. Dafür ist eine Anmeldung erforderlich, über die Nutzung muss Auskunft erteilt werden usw.

Laut der Nutzungsbedingungen dürfen dabei maximal 600 Einzelabfragen pro Stunde und bis zu 5000 Einzelabfragen pro Tag vorgenommen werden.

RMV-Logo

RMV Open Data

https://opendata.rmv.de/site/start.html

Beispielsweise werden die Abfahrtszeiten von Groß-Gerau Dornberg nach Frankfurt Main Hauptbahnhof wie folgt abgerufen:

https://www.rmv.de/hapi/departureBoard?id=3004801&direction=3000010&accessId=Your_API_Key&format=json

Man erhält z.B. folgende JSON-Daten (gekürzt):

  1  
  2
  3
...
231
232
...
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
...
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
...
775
...
788
{
    "Departure": [
        {
... , "Notes": { "Note": [ ..., { "key": "text.occup.jny.2nd.11", "txtN": "2. Klasse: Geringe Belegung", "type": "A", "value": "2. Klasse: Geringe Belegung" }, { "key": "text.occup.jny.max.11", "txtN": "Geringe Belegung", "type": "A", "value": "Geringe Belegung" }, { "key": "text.occup.loc.2nd.11", "txtN": "2. Klasse: Geringe Belegung", "type": "A", "value": "2. Klasse: Geringe Belegung" }, { "key": "text.occup.loc.max.11", "txtN": "Geringe Belegung", "type": "A", "value": "Geringe Belegung" } ] }, "Occupancy": [ { "name": "SECOND", "raw": 11 } ], ..., "ProductAtStop": { "admin": "800528", "catCode": "3", "catIn": "S25", "catOut": "S", "catOutL": "S-Bahn", "catOutS": "S25", "cls": "8", "displayNumber": "S7", "icon": { "backgroundColor": { "b": 87, "g": 151, "hex": "#009757", "r": 0 }, "foregroundColor": { "b": 255, "g": 255, "hex": "#FFFFFF", "r": 255 }, "res": "prod_comm_t" }, "internalName": " S7", "line": "S7", "lineId": "de:rmv:00001322:", "matchId": "35736", "name": "S7", "num": "3131", "operator": "DB Regio AG S-Bahn Rhein-Main", "operatorCode": "DBR" }, "altId": [ "A\u00d7de:06433:4801" ], "date": "2022-11-19", "direction": "Frankfurt (Main) Stadion", "directionFlag": "2", "name": "S7", "prognosisType": "PROGNOSED", "reachable": true, "rtDate": "2022-11-19", "rtTime": "12:10:00", "rtTrack": "4", "stop": "Gro\u00df-Gerau Dornberg Bahnhof", "stopExtId": "3004801", "stopid": "A=1@O=Gro\u00df-Gerau Dornberg Bahnhof@X=8494582@Y=49912101@U=80@L=3004801@", "time": "12:09:00", "track": "4", "type": "ST" },
... 2-3 weitere Abfahrten ], ... }

Listing 1: Beispiel einer JSON-Antwort der RMV Open Api

CollectDelays.py

Mit dem folgenden Script CollectDelays.py werden die Daten zunächst abgerufen und in einer MongoDB-Datenbank gespeichert:

  1  
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
# RMV Open Data API
# https://opendata.rmv.de/site/start.html
# Get Client ID and API key from the Developer Console

import settings
import urllib.request
import json
import os
from pymongo import MongoClient
from datetime import datetime
import csv

client = MongoClient(settings.CONNECTION_STRING) # get MongoDB Client

db = client['RMV-Pull-Stop']                     # create mono db if not exists

collection = db["GG-FFM"]                        # create collection if not exists
#collection.drop()                               

# get pull-stop data from RMV API
def pull_stop_data(stopID: str, direction: str): 
    source_url = f"https://www.rmv.de/hapi/departureBoard?id={stopID}&direction={direction}&accessId={settings.API_TOKEN}&format=json"
    with urllib.request.urlopen(source_url) as url:
        data: str = json.loads(url.read().decode())

    # write to file
    dir = os.path.dirname(__file__)
    filename = os.path.join(dir, "data/departureBoard.json")
    f = open(filename, "w")
    f.write(json.dumps(data, indent = 4, sort_keys=True))
    f.close()
    #print(json.dumps(data, indent = 4, sort_keys=True))
    
    stopData = []

    for departure in data["Departure"]:         # collect some data
        dictionary = {}
        dictionary['stop'] = departure['stop']
        dictionary['direction'] = departure['direction']
        dictionary['date'] = departure['date']
        dictionary['time'] = departure['time']
        dictionary['name'] = departure['name']
        if 'rtDate' in departure:               # date of delayed arrival
            dictionary['rtDate'] = departure['rtDate']
        if 'rtTime' in departure:               # time of delayed arrival
            dictionary['rtTime'] = departure['rtTime']
        
        if 'Occupancy' in departure:  # if info 'Belegung' available 
            raw = departure['Occupancy'][0]['raw']
            key = "text.occup.jny.max." + str(raw)
            for n in departure['Notes']['Note']:
                if n['key'] == key:
                    #print(n['value'])
                    dictionary['occupancy'] = n['value']
        stopData.append(dictionary)
    return stopData


# Save data in mongo db
for direction in settings.directions: # get some departure data per direction
    pullStopDir = pull_stop_data(settings.stopID, direction)
    for psd in pullStopDir:
        # insert departure data in document if departure times not exist
        collection.update_one(  
            {
                'date' : psd.get("date"), 
                'time' : psd.get("time")
            },
            {
                '$setOnInsert': psd
            },
                upsert = True
        )
        # update delay data in existing document if changed
        collection.update_one(  
            {
                'date' : psd.get("date"), 
                'time' : psd.get("time")
            }, 
            {
                '$set': { 
                    'rtDate': psd.get("rtDate"), 
                    'rtTime': psd.get("rtTime")
                }
            },
            upsert = True
        )

Listing 2: Aufruf der RMV API und speichern und aktualisieren der JSON-Daten in MongoDB

Die aktuelle Software findet sich auf GitHub:

https://github.com/ateachment/RMV

Eine aktuelle JSON-Antwort für den Bahnhof Groß-Gerau Dornberg in die Richtungen Frankfurt (Main) Hauptbahnhof) wird hier bereitgestellt:

https://h.eick-at.de/downloads/departureBoard.json
(Aktualisierung alle 10 Minuten)

MongoDB

MongoDB ist ein sogenanntes NoSQL-Datenbankmanagmentsystem (Not only SQL), das es erlaubt, die JSON-Daten der API (ohne Strukturzwang) zu speichern bzw. diese sich ändernden JSON-Daten einfach zu aktualisieren, 

Z.B. werden so folgende Dokumente in der MongoDB gespeichert und gegebenenfalls aktualisiert:

{
    '_id': ObjectId('6378d945a703cf4edb35cad8'),
    'date': '2022-11-19',
'time': '12:09:00',
    'direction': 'Frankfurt (Main) Hauptbahnhof',
    'name': 'S7',
    'occupancy': 'Geringe Belegung',
    'rtDate': '2022-11-19',
    'rtTime': '12:10:00',
    'stop': 'Groß-Gerau Dornberg Bahnhof'
}

Dabei wird _id von MongoDB hinzugefügt.

CSV-Format

Im CSV-Format (comma-separated values) können die Daten dann durch Datenanalyse oder KI-Tools weiterverarbeitet werden. Daher werden im weiteren Verlauf von CollectDelays.py die Daten dann aus der MongoDB gelesen, verarbeitet und entsprechend gespeichert:

 94  
95
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
# write to csv file
dir = os.path.dirname(__file__)
filename = os.path.join(dir, "data/delays.csv")
f = open(filename, 'w', newline='')
writer = csv.writer(f)
fields = ['datetime', 'delay', 'name', 'occupancy']
writer.writerow(fields)

query = { }  
doc = collection.find(query,{'_id':False}) # exlude _id from result dict (save memory)
for d in doc:
    datetimePlanned = datetime.strptime(d['date']+d['time'],"%Y-%m-%d%H:%M:%S")
    if 'rtDate' not in d:
        d['rtDate'] = d['date']
    if d['rtDate'] == None:
        d['rtDate'] = d['date']
    if d['rtTime'] == None:
        d['rtTime'] = d['time']
    if 'name' not in d:
        d['name'] = d['product']
    if 'occupancy' not in d:
        d['occupancy'] = "-"
    datetimeDelayed = datetime.strptime(d['rtDate']+d['rtTime'],"%Y-%m-%d%H:%M:%S")
    delay = datetimeDelayed - datetimePlanned
    print(datetimeDelayed.isoformat() + " - " + datetimePlanned.isoformat() + " = " + str(delay))
    data = [datetimePlanned.isoformat(),str(delay), d['name'], d['occupancy']]
    writer.writerow(data)

f.close()

Listing 3: JSON-Daten aus MongoDB in das CSV-Format überführen

Eine Zeile sieht dann z.B. so aus:

2022-11-19T12:09:00,0:01:00,S7,Geringe Belegung

Die aktuelle CSV-Datei mit Verspätungsdaten wird hier bereitgestellt:

https://h.eick-at.de/downloads/delays.csv
(Aktualisierung alle 10 Minuten)

Weitere Quellen:

https://www.mongodbtutorial.org/mongodb-crud/mongodb-updateone/      (19.11.2022)

Last modified: Thursday, 12 February 2026, 9:14 AM