Files

3.5 KiB

GTFS-Build-Notizen (aus echtem Lauf 2026-08, Urlaub-Webseite)

Konkrete SQL/Schema/Logik, die in dieser Session verifiziert funktioniert haben.

SQLite-Schema (eine DB, 3 Tabellen + Indizes)

CREATE TABLE stops(stop_id TEXT PRIMARY KEY, stop_name TEXT, stop_lat REAL, stop_lon REAL);
CREATE TABLE trips(trip_id TEXT PRIMARY KEY, route_id TEXT, service_id TEXT);
CREATE TABLE st(stop_id TEXT, trip_id TEXT, seq INT, arr TEXT, dep TEXT);
CREATE INDEX idx_st_stop ON st(stop_id);
CREATE INDEX idx_st_trip ON st(trip_id);
CREATE INDEX idx_st_seq ON st(trip_id, seq);

Echte Mengen aus r.und.s.bahnen.zip (Regional+S-Bahn, kein ICE):

  • stops.txt: 16.310 Haltestellen → DB 15.083 (dedupliziert)
  • trips.txt: 110.087 Rohzeilen → DB 964 (trips.txt hat nur selten-service; st ist die Wahrheit)
  • stop_times.txt: 82 MB → st 1.591.002 Zeilen, 110.087 eindeutige trip_id, 8.558 eindeutige stop_id

Spaltenreihenfolgen (GTFS ist NICHT standardisiert was Namen angeht — Header lesen!)

  • stops.txt: stop_name, parent_station, stop_id, stop_lat, stop_lon, location_type, platform_code → stop_id = Spalte 2; parent_station = Spalte 1 (Gruppen-ID). Viele Stops haben leeres parent_station. Import-Fallback: sid = p[2] if p[2] else p[1] (bevorzugt stop_id, sonst parent_station).
  • trips.txt: route_id, service_id, trip_id → trip_id = Spalte 2.
  • stop_times.txt: trip_id, arrival_time, departure_time, stop_id, stop_sequence, ... → stop_id = Spalte 3, arr = 1, dep = 2, seq = 4.

Bahnhofs-Alias (kritisch)

Ein Bahnhof kann mehrere stop_id-Zeilen haben (z.B. Köln Hbf = 222681 UND 452576). Nur EINE ist in stop_times aktiv. Suche-Methode:

-- aktive ID ermitteln
SELECT stop_id, stop_name FROM stops WHERE stop_name = '<Name>';
-- dann je Kandidat:
SELECT COUNT(*) FROM st WHERE stop_id = '<id>';   -- >0 => aktiv

Beispiel: Aachen Hbf → 548582 (aktiv), Köln Hbf → 452576 (aktiv; 222681 tot). Saarbrücken Hbf hat 10 IDs, davon 9 aktiv.

Zeit-basierter Dijkstra (Kern)

Vorladen (einmal):

from collections import defaultdict
deps_by_stop = defaultdict(list)      # stop_id -> [(dep_sec, trip_id, seq)]  sortiert
stops_by_trip = defaultdict(list)     # trip_id -> [(seq, stop_id, arr_sec, dep_sec)]  sortiert
# Befülle beide in einem Durchlauf über st; sortiere jede Liste.

Suche:

INF = float('inf')
dist = defaultdict(lambda: INF); dist[von_id] = dep0
heap = [(dep0, von_id)]
while heap:
    t, stop = heapq.heappop(heap)
    if t != dist[stop]: continue
    if stop == nach_id: break
    for dep_s, tid, seq in deps_by_stop[stop]:
        if dep_s < t: continue
        for (sseq, sid, arr_s, dep2) in stops_by_trip[tid]:
            if sseq <= seq: continue
            if arr_s < dist[sid]:
                dist[sid] = arr_s; heapq.heappush(heap, (arr_s, sid))
# Ziel-Abbruch + dist[stop]-Guard sind ESSENZIELL für 110k Trips.

Verifikations-Referenz (echte GTFS-Ergebnisse 2026-08)

  • Aachen Hbf → Köln Hbf: 1h 12min, 1 Umstieg, 09:00 → 10:12 ✅ (real plausibel)
  • Trier Hbf → Saarbrücken Hbf: nur direkt via trips, keine aktiven stops-Zuordnung nötig

Wichtige Erkenntnis zur Datenlage

Diese GTFS („R- und S-Bahnen") deckt Bahn-Knoten ab, aber nicht alle: Remscheid-Lennep Bf und Frankfurt (Main) Hauptbahnhof hatten in der st-Tabelle 0 aktive Trips → müssen per Alias oder Alternativ-Bahnhof (nächster aktiver) aufgelöst werden, oder liegen außerhalb des Feeds. Vor dem Neuberechnen ALLER Ziele: prüfen welche Kern-Bahnhöfe aktiv sind, fehlende via Nachbar-Bahnhof mappen.