First Look: Die Daten der Radzählstellen in Bremen¶
Vorbemerkungen¶
In Bremen gibt es eine Reihe von Dauerzählstellen für den Radverkehr. Die Bekannteste ist wohl die auf der Wilhelm-Kaisen-Brücke, auch weil hier eine blaue Säule die Zahl der vorbeigefahrenen Fahrradfahrer*innen anzeigt. Doch es gibt weitere, eher unscheinbare Zählstellen. Auf der Webseite der Verkehrsmanagementzentrale (https://vmz.bremen.de/radzaehlstationen) kann man sich die insgesamt 12 Zählstellen ansehen.
Ein Blick auf die Webseite verrät, dass bereits seit 2012 Dauerzählstellen Daten über den Radverkehr erheben. Die Daten werden auf der Webseite nur in einfacher Form dargestellt: Es werden die Werte der letzten 7 und 30 Tage sowie der letzten 25 Monate und die Jahreswerte seit 2012 anzeigt. Eine detaillierte Auswertung und Analyse der Daten liegen bisher nicht vor oder falls doch, sind sie der Öffentlichkeit nicht zugänglich (wenn Google es nicht findet, existiert es nicht). Andere Städte sind hier weiter, zum Beispiel Berlin (https://www.berlin.de/sen/uvk/_assets/verkehr/verkehrsplanung/radverkehr/weitere-radinfrastruktur/zaehlstellen-und-fahrradbarometer/bericht_radverkehr_2018.pdf).
In diesem Artikel werde ich einen ersten Blick auf die Daten der Dauerzählstellen werfen. Dabei werde ich die Daten nur soweit aufbereiten, wie es für diesen ersten Einblick nötig ist. Daher gilt: Alle Angaben hier wurden von mir mit bestem Wissen und Gewissen erstellt, aber ohne Gewähr auf Richtigkeit oder statistische Signifikanz.
Auch werde ich den Weg, wie ich die Daten von der VMZ Webseite heruntergeladen habe, nicht beschreiben. Nur so viel: Ein Browser reicht aus. Ich habe zunächst nur eine kleine „Probe“ der Daten heruntergeladen und die erhaltene Datenmenge hochgerechnet. So konnte ich grob den Umfang des gesamten Datensatzes schätzen, schließlich hatte ich nicht vor, den Server der VMZ mit meiner Anfrage zu crashen oder den Betrieb negativ zu beeinflussen.
Der Datensatz, den ich hier verwende, enthält die stündlichen Messwerte aller 12 Dauerzählstationen vom 1. Januar 2012 bis einschließlich 31. August 2020. Die Daten sind im JSON Format pro Zählstelle circa 2.1 Megabyte groß, für alle Zählstellen umfassen sie circa 25 Megabyte. Damit sind sie überschaubar, zumindest was den verursachten Traffic und den Festplattenplatz betrifft.
Und los geht’s¶
Zu Beginn werden die nötigen Programmbibliotheken importiert. Während pandas, matplotlib, numpy und seaborn eine Reihe von Bibliotheken für die Datenanalyse darstellen, enthält moinstefko von mir entworfene Funktionen.
from moinstefko import load_files
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
Für jede Dauerzählstelle liegt eine JSON Datei vor. load_files liest diese ein und legt die Daten als pandas dataframe in einer Liste (data[]) ab.
data = load_files()
Mit data[x] (x = Position in der Liste) lassen sich nun bereits die einzelnen dataframes aufrufen. Von den insgesamt fünf Spalten im dataframe ist jedoch nur eine (values) wirklich interessant. Sie enhält Datum, Uhrzeit und den gemessenen Wert.
data[0].head(3)
An dieser Stelle kommt prepare ins Spiel. Die Funktion dient dazu:
- Datum/Uhrzeit und den Wert aus der Spalte
valuesin separate Spalten zu schreiben - die Zeitangaben als Index des dataframes zu setzen
- unnötige Spalten zu löschen
- die Spalte
valuesmit einem anprepareübergebenen Namen umzubenennen
def prepare(data, name):
df = data.join(pd.DataFrame(data['values'].tolist(),index=data.index).add_prefix('B_'))
df = df.rename(columns={'B_0':'DateTime', 'B_1':'Value'})
df['Value'] = df['Value'].astype('float64', copy=True)
pd.to_datetime(df.iloc[:, 5], format='%Y-%m-%d %H:%M:%S').copy()
df.set_index(pd.DatetimeIndex(df.iloc[:, 5]), inplace=True)
df = df.drop(columns={'fromDateTime', 'toDateTime', 'resolution', 'stationId', 'values', 'DateTime'})
df = df.rename(columns={'Value': name})
return df
Damit einzelne dataframes bei Bedarf gezielt aufgerufen werden können, ohne sich die Position in der Liste merken zu müssen, habe ich zusätzlich neue Namen in Form von Abkürzungen für die dataframes vergeben. Diese werden in einer Liste df_list zusammengefasst, um spätere Berechnungen und Operationen zu vereinfachen.
wkb_o = prepare(data[9],'Wilhelm-Kaisen-Brücke (Ost)')
wkb_w = prepare(data[0], 'Wilhelm-Kaisen-Brücke (West)')
lng_o = prepare(data[1], 'Langemarckstraße (Ostseite)')
lng_w = prepare(data[2], 'Langemarckstraße (Westseite)')
rkw = prepare(data[3], 'Radweg Kleine Weser')
gms_o = prepare(data[4], 'Graf-Moltke-Straße (Ostseite)')
gms_w = prepare(data[5], 'Graf-Moltke-Straße (Westseite)')
shr = prepare(data[6], 'Schwachhauser Ring')
wms_s = prepare(data[7], 'Wachmannstraße auswärts (Süd)')
wms_n = prepare(data[8], 'Wachmannstraße einwärts (Nord)')
ode = prepare(data[10], 'Osterdeich')
hbs = prepare(data[11], 'Hastedter Brückenstraße')
df_list = [wkb_w, wkb_o, lng_o, lng_w, rkw, gms_o, gms_w, hbs, shr, wms_s, wms_n, ode]
wkb_w.head()zeigt als Ergebnis im Vergleich zu data[0].head() (siehe oben) einen vereinfachten und auf das Nötigste reduzierten dataframe für die Wilhelm-Kaisen-Brücke (West).
wkb_w.head(3)
Summe aller erfassten Werte der Radzählstellen¶
Mit der Frage, wie viele Radfahrer*innen insgesamt seit 2012 von den Dauerzählstellen gezählt wurden, möchte ich in die Datenauswertung einsteigen. Die Summe aller erfassten Werte berechne ich, indem die Spaltensummen miteinander addiert werden.
Zwischen dem 1.1.2012 und dem 31.8.2020 wurden demnach 97.496.172 Radfahrer*innen gezählt.
sum = 0
for df in df_list:
dfsum = df.iloc[:, 0].sum()
sum = sum + dfsum
sum
Allgemeine deskriptive Werte¶
Pandas bietet mit pandas.DataFrame.describe eine komfortable Möglichkeit statistische Grundwerte anzuzeigen. Für eine kompakte Darstellung lege ich die Ergebnisse von .describe() in einer Liste (describe_list) ab. Anschließend fasse ich sie in einem dataframe (df_describe) zusammen.
describe_list = []
for df in df_list:
describe_list.append(df.dropna().describe().T)
df_describe = pd.concat(describe_list)
df_describe
Spitzenwerte der einzelnen Zählstellen mit Datum/Uhrzeit¶
Die Spalte max zeigt den höchsten Wert an, der zwischen dem 1.1.2012 und 31.8.2020 von dieser Dauerzählstelle zu einer bestimmten Stunde gemessen wurde und es interessiert mich, zu welchem Zeitpunkt dieser Wert genau auftrat.
Um die zu den jeweiligen Spitzenwerten zugehörigen Zeitangaben zu ermitteln, nutze ich die Funktionen .max() und .idxmax().
max_values = []
max_date = []
for df in df_list:
max_values.append(df.max())
max_date.append(df.idxmax())
df_max_values = pd.concat(max_values)
df_max_date = pd.concat(max_date)
df_max = pd.concat([df_max_values, df_max_date],axis=1)
df_max.rename(columns={0 : 'max', 1 : 'datetime'})
An dieser Stelle gucke ich etwas ungläubig auf die Werte. Auf der Langemarckstraße wurden morgens um 7:00 Uhr 4.090 Radfahrer*innen gezählt? Im Januar? Ernsthaft?
Exkurs: Ausreißer¶
Da mir der Wert von 4.090 gezählten Radfahrer*innen in der Langemarckstraße am 9. Januar 2018 unplausibel erscheint, gehe ich der Sache einmal nach. Die Darstellung der stündlichen Werte in einem Box-Plot vermittelt mir einen schnellen Überblick über die Verteilung der Daten.
lng_w.boxplot()
Wie das Box-Plot vermuten lässt, liegen im Datensatz der Langemarckstraße (West) eine Reihe von Ausreißern vor. Um die zeitliche Position der potentiellen Ausreißer zu bestimmen, stelle ich die stündlichen Werte in einem zeitlichen Verlauf dar.
fig, ax = plt.subplots()
fig.set_size_inches(18.5, 8.5, forward=True)
ax.plot(lng_w, linewidth=0.5, label='stündliche Werte')
ax.set_xlabel('Jahr')
ax.set_ylabel('Anzahl')
ax.set_title('Langemarckstraße (Westseite)')
ax.legend()
Die Darstellung des Box-Plots und des zeitlichen Verlaufs zeigt: Die stündlichen Werte bewegen sich größtenteils in einem Bereich unter der 500er Marke und der „Spitzenwert“ im Januar 2018 war nicht der einzige dieser Art. Auch in 2016 und 2017 traten einige sehr große Werte auf.
Da es sich hierbei vermutlich um unplausible Werte handelt, stelle ich eine einfache Berechnung auf. Dazu errechne ich die Summe der stündlichen Werte des 8.1., 9.1 und 10.1.2018.
freaky = lng_w.loc['2018-01-08':'2018-01-10'].sum()
freaky
Zum Vergleich ziehe ich den Mittelwert (.mean()) der wöchentlichen Summen (.resample('W').sum()) der gesamten Zeitreihe heran.
lng_w.resample('W').sum().mean()
Es zeigt sich ein deutlicher Unterschied: In den drei Tagen (8.1-11.1.2018) wurden zweieinhalb mal so viele Radfahrer*innen in der Langemarckstraße (West) gezählt wie im wöchentlichen Durchschnitt über alle Jahre hinweg.
Es kann natürlich vorkommen, dass zu bestimmten Zeiten ein hohes, überdurchschnittliches Maß an Radverkehr auftritt, doch meine Vermutung ist, dass es sich hierbei um unplausible und fehlerhafte Werte handelt.
Zum Abschluss dieses Exkurses bilde ich die Jahressummen für die Langemarckstraße und vergleiche diese mit den Angaben auf der VMZ Webseite.
lng_w.resample('Y').sum()
(Screenshot vmz.bremen.de (abgerufen 26.8.2020))
Die täglichen Summen und der gleitende Mittelwert über 365 Tage¶
Die Daten liegen mit stündlichen Werten sehr hochaufgelöst vor und sind damit für eine Darstellung der gesamten Zeitreihe eher ungeeignet. Für die folgenden Diagramme nutze ich daher .resample('D').sum(), um die täglichen Summen zu bilden. Sie werden als blaue Linie im Graph dargestellt.
Um die Zeitreihe zu glätten und so möglicherweise einen Trend erkennen zu können, habe ich mit .rolling().mean() den gleitenden Mittelwert über ein Fenster von 365 Tagen gebildet window=365. Er wird als organge Linie dargestellt.
for df in df_list:
fig, ax = plt.subplots()
fig.set_size_inches(18.5, 8.5, forward=True)
ax.plot(df.iloc[:, 0].resample('D').sum(), linewidth=0.5, label='Täglich')
ax.plot(df.iloc[:, 0].resample('D').sum().rolling(window=365, center=True, min_periods=360).mean(), linewidth=2, label='365 Tage (gleitender Mittelwert)')
ax.set_xlabel('Jahr')
ax.set_ylabel('Anzahl')
ax.set_title(df.columns.values[0])
ax.legend()
Gut zu erkennen sind die jahreszeitlichen Unterschiede. In den wärmeren Jahreszeiten wird mehr Fahrrad gefahren als in den kälteren. Überall,aber nicht überall gleich. So sind die Unterschiede zwischen Sommer und Winter an der Hastedter Brückenstraße und dem Radweg Kleine Weser größer als beispielsweise an der Graf-Moltke-Straße.
Interessanterweise zeigen alle Diagramme, mit Ausnahme der Wilhelm-Kaisen-Brücke, zu Beginn des Jahres 2012 eine Nulllinie. Ich vermute, dass nicht alle Dauerzählstellen gleichzeitig mit den Messungen begonnen haben, sondern erst nach und nach in Betrieb genommen wurden. Das erste Auftreten eines Wertes über Null sollte demnach den Beginn der Messungen dokumentieren. Gut zu erkennen sind auch die „Ausfälle“ der Zählstellen (z.B. Wachmannstraße Nord) und deren Einfluss auf die Darstellung des gleitenden Mittelwertes.
Gehen wir noch einen Schritt weiter und schauen uns für die einzelnen Dauerzählstellen an, wie sich das Radverkehrsaufkommen über den Tag verändert.
Durchschnittlicher täglicher Verlauf¶
Für die folgenden Diagramme habe ich anhand der Tageszeit (df.index.time) die dataframes gruppiert (df.groupby()) und den Mittelwert der Gruppierungen (.mean()) berechnet. Die Diagramme geben wieder, wie viele Radfahrer*innen zu den jeweiligen Tageszeiten im Durchschnitt gezählt wurden.
for df in df_list:
by_time= df.groupby(df.index.time).mean()
hourly_ticks = 4 * 60 * 60 * np.arange(6)
by_time.plot(xticks=hourly_ticks, style='-')
Hohe Werte in den Morgen- und/oder Abendstunden sind ein Indiz dafür, dass zu diesen Zeiten viele Berufstätige oder Schüler mit ihren Fahrrädern unterwegs sind. Besonders gut zu erkennen ist dies im Vergleich der Nord- und Südseite der Wachmannstraße. Morgens geht’s in die Stadt und abends wieder heraus.
Durchschnittlicher Verlauf Werktage (links) und Wochenende (rechts)¶
Ich möchte noch einen Schritt weiter gehen und den Radverkehr der Werktage dem am Wochenende gegenüberstellen. Dazu werden die Daten anhand des Index‘ in Werktage (df.index < 5) und Wochenende aufgeteilt. Anschließend erfolgt wieder eine zeitliche Gruppierung (.groupby()) und Bestimmung des Mittelwerts (.mean()). Der Unterschied zwischen Wochentagen und Wochenendtagen wird in zwei Graphen nebeneinander dargestellt.
for df in df_list:
weekend = np.where(df.index.weekday < 5, 'Weekday', 'Weekend')
by_time = df.groupby([weekend, df.index.time]).mean()
fig, ax = plt.subplots(1,2)
fig.set_size_inches(18.5, 6, forward=True)
by_time.xs('Weekday').plot(ax=ax[0], title='Weekday', xticks=hourly_ticks)
by_time.xs('Weekend').plot(ax=ax[1], title='Weekend', xticks=hourly_ticks)
Wie sich zeigt, ändert sich das Radverkehrsaufkommen an den Wochenenden im Vergleich zu den Wochentagen. Es gibt keinen Berufs-/ und Schulvekehr, also entallen die morgendlichen Spitzen. An den Wochenenden entwickelt sich das Radverkehrsaufkommen allmählich im Tagesverlauf. Dafür gibt es in den Nachtstunden am Wochenende an einigen Zählstationen mehr Radverkehr als an den Werktagen, z.B. am Osterdeich.
Der Einbruch des Graphen im Diagramm Wilhelm-Kaisen-Bücke (Ost) an den Werktagen wirkt irritierend. Daher nutze ich .between_time(), um mir die Werte, die möglicherweise zwischen 14:01 Uhr und 14:59 Uhr liegen anzugucken. Das Ergebnis zeigt am 27.3.2018 einen Indexeintrag um 14:30 Uhr an.
df_test = wkb_o.between_time('14:01:00', '14:59:00')
df_test
Anschließend bereinige ich den dataframe und entferne den Wert mittels .drop().
df_test = wkb_o.drop(pd.Timestamp('2018-03-27 14:30:00'))
df_test = df_test.groupby(df_test.index.time).mean()
hourly_ticks = 4 * 60 * 60 * np.arange(6)
df_test.plot(xticks=hourly_ticks, style='-')
Fazit und Ausblick¶
Ich habe versucht zu zeigen, dass in diesen Daten mehr steckt als ein paar absolute Werte. Die Daten der Bremer Radverkehr-Dauerzählstellen sind sehr umfangreich und in vielerlei Hinsicht interessant und spannend. Wie sich zeigt, sind die Daten jedoch nicht frei von Fehlern. Mir stellt sich dabei vorrangig die Frage, wie man mit unplausiblen Ausreißern und den Nullwerten umgeht. Besonders die Trennung der Nullwerte in plausible Nullwerte und Nullwerte, die möglicherweise einen Ausfall der Zählstelle darstellen, kann sich als schwierig erweisen.
In diesem Sinne muss ich sagen: further research is needed. Ich denke, in naher Zukunft werde ich hierzu ein Update veröffentlichen. Ich hoffe, dann auch etwas belastbarere Zahlen liefern zu können.
Danke¶
Ich beschäftige mich erst seit ein paar Monaten mit dem Thema Data Science und der Auswertung von Daten mittels Python. Und auch wenn ich denke, dass ich in den letzten Wochen vieles gelernt habe und sich meine Kenntnisse auf diesem Gebiet vervielfältigt haben, gebe ich gerne zu: Nicht alle in diesem Artikel verwendeten Methoden und Workarounds sind von allein meinem Hirn entsprungen.
Ich danke allen Menschen, die sich die Zeit nehmen und die Mühe machen, Tutorials und Codebeispiele ins Netz zu stellen und anderen auf Plattformen wie stackoverflow oder github ihre Unterstützung zukommen lassen.