Wiener Straßenquartett¶
Zählstellen¶
Das sind die Stellen in der Stadt an denen Verkehrsaufkommen gemessen wird.
Quelle: https://www.data.gv.at/katalog/dataset/4707e82a-154f-48b2-864c-89fffc6334e1
Abgerufen: 18.05.2025
import pandas as pd
zaehlstellen = pd.read_csv("datasets/zaehlstellen.csv")
zaehlstellen.head()
Das Datenset enthält auch Zählstellen für Radverkehr.
Hier nicht relevant, deshalb entfernen.
zaehlstellen = zaehlstellen[zaehlstellen['GERAETEART'] != 'R_SG']
Eine der vier Metriken der Quartettkarten ist die Spurenanzahl.
Zählstellenposition auf Google Street View ansteuern und zählen.
Abgerufen 18.05.2025
lanes_mapping = {
1075: 6,
1078: 7,
1089: 4,
1096: 4,
1131: 6,
1170: 8,
1171: 2,
1172: 4,
1177: 4,
1179: 6,
1180: 3,
1181: 2,
1182: 6,
1184: 2,
1187: 4,
1188: 5,
1189: 5,
1190: 3,
1191: 6,
1192: 3,
1193: 4,
1194: 3,
1195: 2,
1196: 5,
1197: 3,
1198: 3,
1199: 2,
1200: 2,
1201: 2,
1202: 2,
1203: 3,
1204: 3,
1205: 2,
1206: 2,
1207: 4,
1208: 5,
1209: 5,
1210: 2,
1211: 6,
1212: 5,
1213: 3,
1214: 2,
1215: 5,
1216: 3,
1217: 6,
1218: 2,
1219: 2,
1220: 4,
1221: 4,
1222: 3,
1223: 4,
1224: 6,
1600: 2,
1603: 3,
1607: 3,
1608: 8,
1609: 3,
1610: 4,
1611: 2,
1612: 2,
1613: 2,
1614: 2,
1615: 3,
1616: 2,
1617: 2,
1618: 2,
1619: 2,
1620: 2,
1621: 7,
1622: 2,
1623: 2,
1624: 2,
1625: 4,
1626: 5,
1627: 2
}
zaehlstellen['LANES'] = zaehlstellen['ZST_ID'].map(lanes_mapping)
zaehlstellen[['ZST_NAME', 'ZST_ID', 'LANES']].head()
Jede Quartettkarte braucht eine Kurzbeschreibung unter dem Titel.
Dafür Wien-Geschichte-Wiki-Artikel (https://www.geschichtewiki.wien.gv.at) zu Straße auf der die jeweilige Zählstelle liegt zusammenfassen mit ChatGPT.
description_mapping = {
1623: "Benannt nach der Gemeinde Breitenlee, früher Pressburger Straße.",
1612: "Benannt nach Josephine Haas von Längenfeld-Pfalzheim, Stifterin.",
1620: "Benannt nach Robert Endres, früher Teil der Hauptstraße.",
1187: "Historische Handelsstraße nach Triest, ab 1934 verbreitert.",
1191: "Teil der Westeinfahrt Wiens, gebaut in den 1930er Jahren.",
1197: "Benannt nach Breitenlee, um 1830 als Pressburger Straße bekannt.",
1205: "Benannt zur Wahrung des Namens Hernals, seit 1905 verändert.",
1206: "Benannt nach Hütteldorf, früher Breitenseer-Hütteldorfer Straße.",
1211: "Benannt nach Laxenburg, 1971 zur modernen Durchzugsstraße ausgebaut.",
1212: "Benannt nach dem 22. Bezirk Donaustadt, seit 1971.",
1600: "Verbindet die Stadt mit Schönbrunn, früher Hundsturmerstraße.",
1614: "Benannt nach der Ansiedlung Maxing, führt nach Küniglberg.",
1616: "Benannt nach dem Vorort Gersthof, früher Gersthofer Hauptstraße.",
1184: "Benannt nach Himberg, früher Teil der Favoritenstraße.",
1192: "Folgt einer mittelalterlichen Ausfallstraße, 1938-45 umbenannt.",
1210: "Benannt nach Dr. Josef Kopp, früher Teil der Neustiftgasse.",
1216: "Benannt nach Perchtoldsdorf, einer Nachbargemeinde Wiens.",
1609: "Teil des Währinger Gürtels, früher Linienwall.",
1181: "Früher Liesinger Weg, später Breitenfurter Waldämtliche Straße.",
1195: "Benannt nach Deutsch-Wagram, früher Süßenbrunner Straße.",
1198: "Verbindet Wien mit Brünn, 1736 als Reichsstraße angelegt.",
1208: "Benannt nach dem Wienerberg, früher Meidlinger Straße.",
1209: "Benannt nach Atzgersdorf, mehrfach verlängert und verändert.",
1214: "Einer der ältesten Verkehrswege Wiens, seit 1862 benannt.",
1619: "Früher Liesinger Weg, ab 1888 Breitenfurter Straße.",
1170: "Franz-Josefs-Kai, ehemals Teil der Stadtmauern, stark befahren.",
1618: "Benannt nach Werner von Siemens, früher Leopoldauerstraße.",
1622: "Benannt nach Himberg, ehemals Teil der Favoritenstraße.",
1078: "Wichtiger Verkehrsknotenpunkt, zweitgrößtes Verkehrsbauwerk Wiens.",
1177: "Benannt nach dem Donauhandel, früher Handelsquai.",
1180: "Führt über eine Anhöhe zwischen Rodaun und Perchtoldsdorf.",
1199: "Benannt nach dem Ortsnamen Essling.",
1215: "Benannt nach Erzherzog Carl, früher Asperner Straße.",
1194: "Historische Reichsstraße nach Prag, seit 1728.",
1201: "Benannt nach Bäumen aus Vorarlberg, seit 1990 offiziell.",
1202: "Verbindungsstraße bei Vösendorf.",
1207: "Benannt nach Altmannsdorf, früher Laxenburger Straße.",
1200: "Benannt nach Bäumen aus Vorarlberg, seit 1990 offiziell.",
1624: "Benannt nach der Ortschaft Seyring, früher Leopoldauer Platz.",
1608: "Benannt nach der Vorstadt Margareten, Teil des Gürtelsystems.",
1610: "Teil des Währinger Gürtels, früher Linienwall.",
1131: "Benannt nach Karl VI., früher eine Aulandschaft.",
1172: "Begleitstraße des Donaukanals, stark befahren, seit 1857.",
1182: "Römische Militärstraße, Teil der historischen Limesstraße.",
1188: "Benannt nach Altmannsdorf, früher Laxenburger Allee.",
1189: "Benannt nach der Fabrikantenfamilie Shuttleworth.",
1193: "Benannt nach Heiligenstadt, früher Nußdorfer Straße.",
1196: "Benannt nach Erzherzog Carl, früher Asperner Straße.",
1204: "Erinnert an die Türkenbelagerungen, früher Am Glacis.",
1607: "Begleitet den Donaukanal, früher Teil der Pressburger Bahn.",
1615: "Benannt nach Ludwig Freiherr von Gablenz, früher Burggasse.",
1611: "Römische Militärstraße, Teil der historischen Limesstraße.",
1613: "Benannt nach Lainz, urkundlich seit 1313 belegt.",
1213: "Benannt nach der Hofburg, eine der ältesten Straßen Wiens.",
1603: "Benannt nach Hirschstetten, früher Hirschstettner Hauptstraße.",
1617: "Benannt nach Angern an der March, früher Bahngasse.",
1075: "Doppelstockbrücke für Auto-, U-Bahn- und Radverkehr.",
1089: "Ersetzt die Kaiser-Franz-Josephs-Brücke, 1978 neu eröffnet.",
1096: "Straßenbrücke über die Donau, seit 1982 in Betrieb.",
1171: "Benannt nach dem Verlauf entlang des Donaukanals.",
1179: "Benannt nach Brunn am Gebirge, südlich von Wien.",
1190: "Straßenname unklar, keine weiteren Informationen verfügbar.",
1203: "Benannt nach Maria Theresia, früher Teil einer Allee.",
1218: "Verbindungsstraße bei Leopoldsdorf.",
1217: "Benannt nach den dortigen Museen, früher Hofstallstraße.",
1219: "Benannt nach dem Fabrikanten Stephan Barawitzka.",
1220: "Historisch gewachsene Vorstadtstraße mit repräsentativen Gebäuden.",
1625: "Benannt nach dem Taubstummeninstitut, seit 1816 erwähnt.",
1626: "Benannt nach Ludwig von Höhnel, historisch umstritten.",
1627: "Benannt nach der Ried Felbern, bedeutet Weidenbaum.",
1221: "Benannt nach einem historischen Flurnamen, früher Grenzweg.",
1222: "Benannt nach der Universität Wien, früher Franzensring.",
1223: "Benannt nach dem Dichter Adalbert Stifter.",
1224: "Benannt nach Deutsch-Wagram, früher Süßenbrunner Straße.",
1621: "Teil des Währinger Gürtels, früher Linienwall."
}
zaehlstellen['DESCRIPTION'] = zaehlstellen['ZST_ID'].map(description_mapping)
zaehlstellen[['ZST_NAME', 'ZST_ID', 'DESCRIPTION']].head()
Die Zählstellenkoordinaten liegen als WKT-Spalte vor.
Auf separate Long- und Lat-Spalten verteilen.
zaehlstellen['LATITUDE'] = zaehlstellen['SHAPE'].apply(lambda x: float(x[7:].split()[1][:-1]))
zaehlstellen['LONGITUDE'] = zaehlstellen['SHAPE'].apply(lambda x: float(x[7:].split()[0]))
zaehlstellen[['ZST_NAME', 'LATITUDE', 'LONGITUDE']].head()
Jede Quartettkarte inkludiert das Wappen des Bezirks in dem sich die jeweilige Zählstelle befindet.
Dafür PLZ der Zählstellenkoordinate ermitteln.
from joblib import Memory
import time
from geopy.geocoders import Nominatim
from geopy.exc import GeocoderTimedOut
memory = Memory("./cache", verbose=0)
geolocator = Nominatim(user_agent="postcode_retriever")
# https://stackoverflow.com/questions/75202475/joblib-persistence-across-sessions-machines/
def cache(mem, module, **mem_kwargs):
def cache_(f):
f.__module__ = module
f.__qualname__ = f.__name__
return mem.cache(f, **mem_kwargs)
return cache_
@cache(memory, "verkehrszaehlungen")
def get_postcode(lat, lon):
time.sleep(1) # Nominatim rate limit respektieren
try:
location = geolocator.reverse((lat, lon), exactly_one=True)
if location and 'address' in location.raw:
return location.raw['address'].get('postcode', None)
except GeocoderTimedOut:
return None
zaehlstellen['PLZ'] = zaehlstellen.apply(lambda row: get_postcode(row['LATITUDE'], row['LONGITUDE']), axis=1)
zaehlstellen[['ZST_NAME', 'PLZ']].head()
Sind eh alle Zählstellen in Wien bzw. haben eine Wiener PLZ?
zaehlstellen[~zaehlstellen['PLZ'].astype(str).str.startswith('1')]
Ja!
Zählwerte¶
Das sind die eigentlichen Messwerte des Verkehrsaufkommens.
Quelle: https://www.data.gv.at/katalog/dataset/4707e82a-154f-48b2-864c-89fffc6334e1
Abgerufen: 18.05.2025
zaehlungen = pd.read_csv("datasets/zaehlungen.csv", encoding="latin", sep=";")
zaehlungen.head()
Getrennte JAHR- und MONAT-Spalte zu einer Datumsspalte zusammenführen.
month_mapping = {
'JAN.': 1, 'FEB.': 2, 'MÄRZ': 3, 'APRIL': 4, 'MAI': 5, 'JUNI': 6,
'JULI': 7, 'AUG.': 8, 'SEP.': 9, 'OKT.': 10, 'NOV.': 11, 'DEZ.': 12
}
zaehlungen['MONAT'] = zaehlungen['MONAT'].map(month_mapping)
zaehlungen = zaehlungen.rename(columns={'MONAT': 'MONTH', 'JAHR': 'YEAR'})
zaehlungen['DATE'] = pd.to_datetime(zaehlungen[['YEAR', 'MONTH']].assign(DAY=1))
zaehlungen[['DATE', 'ZNAME']].head()
Zählstellen mit Zählwerten verknüpfen¶
Kann jeder Zählwert einer Zählstelle zugeordnet werden?
set(zaehlungen['ZNR']) - set(zaehlstellen['ZST_ID'])
Nope! Welche Zählstellen fehlen?
set(zaehlungen[(zaehlungen['ZNR'] == 1185) | (zaehlungen['ZNR'] == 1605) | (zaehlungen['ZNR'] == 1606)]['ZNAME'])
Von den fehlenden Zählstellen sind nur die Straßennamen bekannt.
Die Koordinate in etwa auf den Mittelpunkt der Straße legen.
Fehlende Zählstellen konstruieren, einfügen und die beiden Datensätze verknüpfen.
zaehlstellen = pd.concat([zaehlstellen, pd.DataFrame.from_records([{ 'ZST_ID': 1185, 'ZST_NAME': 'Neustiftgasse' ,'LONGITUDE': 16.34754073472826, 'LATITUDE': 48.205706700846775, 'PLZ': 1070, 'LANES': 2 , 'DESCRIPTION': 'Einer der ältesten Verkehrswege Wiens, seit 1862 benannt.' }])])
zaehlstellen = pd.concat([zaehlstellen, pd.DataFrame.from_records([{ 'ZST_ID': 1605, 'ZST_NAME': 'Burggasse' , 'LONGITUDE': 16.347385492037365, 'LATITUDE': 48.20443811428511, 'PLZ': 1070,'LANES': 2, 'DESCRIPTION': 'Benannt nach der Hofburg, eine der ältesten Straßen Wiens.' }])])
zaehlstellen = pd.concat([zaehlstellen, pd.DataFrame.from_records([{ 'ZST_ID': 1606, 'ZST_NAME': 'Leopoldsdorfer Straße' , 'LONGITUDE': 16.39689203897414, 'LATITUDE': 48.13088363172005, 'PLZ': 1100,'LANES': 2, 'DESCRIPTION': 'Führt nach Leopoldsdorf, 1956 durch Blumengasse verlängert.' }])])
zaehlungen = pd.merge(zaehlstellen, zaehlungen, left_on="ZST_ID", right_on="ZNR", how="inner")
Die verknüpften Zählwerte auf relevante Spalten reduzieren.
Wichtige Abkürzungen:
- DTVMS = Durchschnittlicher täglicher Verkehr Montag bis Sonntag
- RINAME = Richtungsname
- FZTYP = Fahrzeugtyp (Kfz/LkwÄ)
zaehlungen = zaehlungen[['DATE', 'ZST_ID', 'ZST_NAME', 'DESCRIPTION', 'LATITUDE', 'LONGITUDE', 'PLZ', 'RINAME', 'FZTYP', 'DTVMS', 'LANES']]
zaehlungen.head()
Jeder Zählwert (Zeile im Datensatz) enthält die Messwerte an einer Zählstelle für:
- einen Monat
- eine Fahrrichtung
- einen Fahrzeugtypen
Als erstes die Zählwerte auf jeweils jene Fahrtrichtung mit den höchsten Messwerten reduzieren.
Pro Zählstelle, Monat und Fahrzeugtyp gibt es je drei Zählwerte:
- Richtung A
- Richtung B
- Gesamt.
Für Einbahnen sind die Messwerte der Gegenrichtung sowie für Gesamt gleich -1.
Zählwerte reduzieren auf jene mit dem höchsten Messwert.
zaehlungen = zaehlungen.groupby(list(zaehlungen.columns.drop(['DTVMS', 'RINAME'])), as_index=False).agg({ 'DTVMS': 'max' })
zaehlungen.head()
Als nächstes Messwerte für verschiedene Fahrzeugtypen summieren und LKW-Anteil berechnen.
dtvms_sum = zaehlungen.groupby(list(zaehlungen.columns.drop(['DTVMS', 'FZTYP'])))['DTVMS'].transform('sum')
zaehlungen['LKWRATIO'] = zaehlungen['DTVMS'] / dtvms_sum
zaehlungen['DTVMS'] = dtvms_sum
zaehlungen = zaehlungen[zaehlungen['FZTYP'] == 'LkwÄ']
zaehlungen = zaehlungen.drop('FZTYP', axis=1)
zaehlungen.head()
Die prozentuelle Veränderung der Messwerte einer Zählstelle über die letzten fünf Jahre berechnen.
Dazu zunächst die durchschnittlichen Zählwerte für das Jahr 2019 berechnen und in einer eigenen Spalte speichern.
dtvms2019 = zaehlungen[zaehlungen['DATE'].dt.year == 2019].groupby('ZST_NAME')['DTVMS'].mean().reset_index()
dtvms2019 = dtvms2019.rename(columns={ 'DTVMS': 'DTVMS2019' })
zaehlungen = zaehlungen.merge(dtvms2019, on='ZST_NAME', how='left')
zaehlungen.head()
Dann die durchschnittlichen Zählwerte für 2024 berechnen.
zaehlungen = zaehlungen[zaehlungen['DATE'].dt.year == 2024]
zaehlungen = zaehlungen.groupby(list(zaehlungen.columns.drop(['DATE', 'DTVMS', 'LKWRATIO', 'DTVMS2019'])), as_index=False).agg({ 'DTVMS': 'mean', 'LKWRATIO': 'mean', 'DTVMS2019': 'max' })
zaehlungen.head()
Dann die prozentuelle Veränderung der 2019er-Werte zum 2024er-Wert hin berechnen.
zaehlungen['TREND'] = (zaehlungen['DTVMS'] - zaehlungen['DTVMS2019']) / zaehlungen['DTVMS2019']
zaehlungen = zaehlungen.drop('DTVMS2019', axis=1)
zaehlungen.head()
Jede Quartettkarte gehört einer Kategorie an.
Jede Kategorie umfasst genau vier Karten.
Bei 71 Zählstellen müssen drei Zählstellen entfernt werden ... 71 % 4 = 3
Zu entfernende Zählstellen erhalten im Kategorie-Mapping den Wert -1
Die restlichen möglichst sinnvollen Kategorien zuordnen.
Jede Karte enthält auch drei Verweise auf die anderen Karten in der jeweiligen Kategorie (siehe create_linked_strings).
categories = {
1: 'Am Gürtel',
2: 'Uferstraßen',
3: 'Brücken',
4: 'Zweierlinie',
5: 'Stadteinfahrten Nord',
6: 'Stadteinfahrten Süd',
7: 'Autobahnzufahrten',
8: 'Gewerbegebiete Süd',
9: 'Hauptverkehrsstraßen Süd',
10: 'Sehenswürdigkeiten',
11: 'Im Wienerwald',
12: 'Hauptverkehrsstraßen Nord',
13: 'Gewerbegebiete Nord',
14: 'Wohnstraßen Zentrum',
15: 'Wohnstraßen West',
16: 'Wohnstraßen Süd',
17: 'Wohnstraßen Nord'
}
categories_mapping = {
1078: 1,
1621: 1,
1170: 2,
1608: 1,
1207: 9,
1191: 11,
1075: 3,
1193: 2,
1131: 10,
1179: 8,
1221: 9,
1215: 12,
1089: 3,
1096: 3,
1220: 4,
1222: 10,
1607: 2,
1177: 2,
1219: 17,
1196: 12,
1626: 7,
1208: 9,
1212: 12,
1224: 12,
1189: -1, # Shuttleworthstraße entfernen
1223: 3,
1188: 9,
1182: 6,
1187: 6,
1211: 8,
1217: 4,
1612: -1, # Längenfeldgasse entfernen
1199: 5,
1209: 16,
1198: 5,
1600: 1,
1197: 5,
1619: 6,
1205: 15,
1618: 13,
1616: 17,
1627: 14,
1210: 15,
1603: 13,
1204: 4,
1611: 10,
1617: 17,
1615: 15,
1200: 8,
1623: 13,
1181: 11,
1202: 7,
1192: 11,
1624: 17,
1190: -1, # Angyalföldstraße entfernen
1614: 10,
1613: 16,
1625: 14,
1203: 4,
1622: 6,
1206: 15,
1180: 11,
1184: 7,
1194: 13,
1214: 14,
1213: 14,
1218: 7,
1201: 8,
1216: 16,
1195: 5,
1620: 16,
}
zaehlungen['CATEGORY_ID'] = zaehlungen['ZST_ID'].map(categories_mapping)
zaehlungen = zaehlungen[zaehlungen['CATEGORY_ID'] != -1]
zaehlungen['CATEGORY'] = zaehlungen['CATEGORY_ID'].map(categories)
zaehlungen['CATEGORY_ID_UNIQUE'] = zaehlungen['CATEGORY_ID'].astype(str) + zaehlungen.groupby('CATEGORY_ID').cumcount().map(lambda x: ['A', 'B', 'C', 'D'][x])
def create_linked_strings(group):
linked_entries = []
for _, row in group.iterrows():
others = group[group['CATEGORY_ID_UNIQUE'] != row['CATEGORY_ID_UNIQUE']]
linked_strings = others.apply(
lambda other: f"{other['CATEGORY_ID_UNIQUE'][-1].lower()}) {other['ZST_NAME']}", axis=1
).tolist()
if len(linked_strings) != 3:
raise ValueError("Zu jeder Quartett-Kategorie muss es genau vier Karten geben")
linked_strings = linked_strings[:3]
linked_entries.append(linked_strings)
group = group.copy()
group[['LINK1', 'LINK2', 'LINK3']] = linked_entries
return group
zaehlungen = zaehlungen.groupby('CATEGORY_ID', group_keys=False, as_index=False).apply(lambda group: create_linked_strings(group.reset_index(drop=True)), include_groups=True)
zaehlungen = zaehlungen.drop(columns=['CATEGORY_ID'])
zaehlungen = zaehlungen.rename(columns={'CATEGORY_ID_UNIQUE': 'CATEGORY_ID'})
zaehlungen = zaehlungen.reset_index()
zaehlungen[['ZST_NAME', 'ZST_ID', 'CATEGORY', 'CATEGORY_ID', 'LINK1', 'LINK2', 'LINK3']].head()
Die aufbereiteten Daten sichten...
zaehlungen = zaehlungen.sort_values('DTVMS', ascending=False)
with pd.option_context('display.max_rows', None, 'display.max_columns', None):
display(zaehlungen)
... und als JSON abspeichern.
zaehlungen.to_json('web/quartett.json', orient="records", index=False)