Titel: Ozapft is! Description: 30 Jahre Wiesn in Zahlen Dataset: https://opendata.muenchen.de/dataset/8d6c8251-7956-4f92-8c96-f79106aab828/ Dataset: https://opendata.muenchen.de/dataset/022a11ff-4dcb-4f03-b7dd-a6c94a094587/ Dataset: https://opendata.muenchen.de/dataset/3621ad08-aa97-4c2b-b0b0-82780375743c/ Dataset: https://opendata.muenchen.de/dataset/5e73a82b-7cfb-40cc-9b30-45fe5a3fa24e/ Landing: true
# Import Required Libraries
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import requests
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import seaborn as sns
from scipy.stats import zscore
from scipy import stats
from datetime import datetime, date
from datetime import timedelta
import numpy as np
# Plotly-Konfiguration für bessere HTML-Konvertierung
import plotly.io as pio
pio.renderers.default = 'notebook'
import requests
import statsmodels.formula.api as smf # <-- ADDED for DiD regression
search_url = "https://opendata.muenchen.de/api/3/action/package_search"
search_params = {"q": "Oktoberfest"}
response = requests.get(search_url, params=search_params)
package = response.json()["result"]["results"][0]
# Helper für einheitliches Layout ohne Ränder
# Nutzung: apply_standard_layout(fig)
def apply_standard_layout(fig, legend_y=-0.25, top_margin=60):
fig.update_layout(
legend=dict(
orientation='h',
yanchor='bottom',
y=legend_y,
xanchor='center',
x=0.5,
title_text='' # kein Legendentitel
),
margin=dict(l=0, r=0, t=top_margin, b=0),
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
hovermode='x unified'
)
return fig
O'zapft is!¶
30 Jahre Wies'n in Zahlen¶
Die Wies'n. Gehasst, geliebt aber auf jeden Fall weit mehr als nur ein Volksfest – sie ist ein wirtschaftliches Phänomen, ein kulturelles Ereignis und für Datenanalyst:innen fast so lecker wie eine frische, heiße Packung gebrannte Mandeln nach dem Sierra Madre. Seit über drei Jahrzehnten pilgern Millionen von Menschen auf die Theresienwiese wenns wieder heißt O'Zapft is, um a Maß'erl zu trinken, Hendl zu essen und das Leben zu feiern. Doch was passiert eigentlich hinter den Kulissen dieses gigantischen Spektakels?
In dieser Analyse tauchen wir tief in die Daten des Münchner Open Data Portals ein und beleuchten das Oktoberfest mit einem Blick hinaus über den Rand des Maßkrugs. Wir untersuchen nicht nur, wie sich Besucherzahlen und Bierkonsum über die Jahre entwickelt haben, sondern beschäftigen uns auch mit den wirklich wichtigen Fragen des Lebens: Wird bei weiß-blauem Himmel mehr getrunken? Wo bekomme ich mein Bier am günstigsten? Und kann man nach 2 Maß tatsächlich noch mim Auto nach Hause fahren? Und wie kann man auf der Wiesn umsonst Bier trinken?
Also dann, pack mas!
df_years = pd.read_csv(package['resources'][0]['url'], delimiter=',')
def get_zusatz(jahr):
zusatz = []
# Bayerisches Zentral-Landwirtschaftsfest
landwirtschaftsfest_jahre = [1987, 1990, 1993, 1996, 2000, 2004, 2008, 2012, 2016]
if jahr in landwirtschaftsfest_jahre:
zusatz.append('Bayerisches Zentral-Landwirtschaftsfest')
# Oide Wiesn (seit 2010, außer 2012 und 2016)
if jahr >= 2010 and jahr not in landwirtschaftsfest_jahre:
zusatz.append('OideWiesn')
return ', '.join(zusatz)
# Rescale
df_years['besucher_gesamt'] = df_years['besucher_gesamt']*1e6 # Millionen -> Einzelpersonen
df_years['bier_konsum'] = df_years['bier_konsum']*100 # Hektoliter -> Liter
df_years['besucher_tag'] = df_years['besucher_tag']*1e3 # Tausend -> Einzelpersonen
# Spalten hinzufügen
df_years['bier_konsum_tag'] = df_years['bier_konsum'] / df_years['dauer']
df_years['bier_konsum_besucher'] = (df_years['bier_konsum'] ) / ( df_years['besucher_gesamt'])
df_years['zusatz'] = df_years['jahr'].apply(get_zusatz)
## Inflationsbereinigung
# VPI-Daten von Genesis (Basis 2020=100, https://www-genesis.destatis.de/datenbank/online/statistic/61111/table/61111-0001/search/s/dmVyYnJhdWNoZXJwcmVpc2luZGV4JTIwZiVDMyVCQ3IlMjBkZXV0c2NobGFuZA==)
vpi_data = {
1991: 61.9, 1992: 65.0, 1993: 67.9, 1994: 69.7, 1995: 71.0, 1996: 72.0,
1997: 73.4, 1998: 74.0, 1999: 74.5, 2000: 75.5, 2001: 77.0, 2002: 78.1,
2003: 78.9, 2004: 80.2, 2005: 81.5, 2006: 82.8, 2007: 84.7, 2008: 86.9,
2009: 87.2, 2010: 88.1, 2011: 90.0, 2012: 91.7, 2013: 93.1, 2014: 94.0,
2015: 94.5, 2016: 95.0, 2017: 96.4, 2018: 98.1, 2019: 99.5, 2020: 100.0,
2021: 103.1, 2022: 110.2, 2023: 116.7, 2024: 119.3
}
df_years['bier_preis_real'] = df_years['bier_preis'] * vpi_data[2024] / df_years['jahr'].map(vpi_data)
df_years['hendl_preis_real'] = df_years['hendl_preis'] * vpi_data[2024] / df_years['jahr'].map(vpi_data)
#df_years.tail()
numeric_cols = df_years.select_dtypes(include=['float64', 'int64']).columns
numeric_cols = [col for col in numeric_cols if col != 'jahr'] # Jahr ausschließen
fig = px.line(df_years, x='jahr', y=numeric_cols,
title='Oktoberfest Daten - Interaktiv',
markers=True)
# Define which traces to show by default (by name)
default_visible_traces = ['besucher_gesamt', 'bier_konsum']
# Hide all traces except the specified default ones
for trace in fig.data:
if trace.name not in default_visible_traces:
trace.update(visible='legendonly')
apply_standard_layout(fig, legend_y=-0.25, top_margin=60)
fig.update_layout(yaxis=dict(autorange=True), yaxis_title='')
fig.show()
Mit einem Klick in der Legende können wir einzelne Variablen an und Abwählen und damit die Daten einfach interpretieren und vergleichen. Die Besucherzahlen schwanken um einen Mittelwert von 6,3 Millionen beziehungsweise 388k Besucher pro Tag. Dabei haben in der Vergangenheit immer wieder Weltweite Ereignisse wie Terroranschläge (USA: 2001, Europa: 2015/2016), Finanzkrisen (1987, 2009) oder Nachwirkungen der Corona Pandemie. Wir können uns auch die Inflationsbereinigten Preise anschauen. Dazu haben wir unsere Daten erweitert um den Verbraucherpreisindex (VPI), von Destatis, der ab 1991 zur Verfügung steht.
How much is the Fish?¶
Wenig überraschend und weithin bekannt sind die Bierpreise über die Jahre kontinuierlich gestiegen. Interessanter wird es beim Hendl Preis: hier können wir von 1999 als das halbe Hendl noch günstiger war als die Maß Bier auf 2000 einen Preisansprung von fast 46% beobachten. Hier ist die Frage worin die Ursuchen dafür liegen könnten. Eine Vermutung legt nahe, dass das Aufkommen der ersten BSE Fälle in Deutschland zu einer erhöhten Nachfrage von Geflügel geführt hat. Oder die Vogelgrippe in Italien für einen Preisanstieg verantwortlich zu machen ist. Die würde aber nicht erklären, warum der Hendl Preis anschließend kontinuierlich weiter angestiegen ist. Wirft man einen Blick auf den Hendl-Konsum, stellen wir fest, dass dieser 2000 normal hoch ist. Damit können wir nicht mal den Wiesn-Wirten unterstellen, dass ihre Preisgier belohnt wurde und deshalb die hohen Preise beibehalten wurden. Interessanterweise ist ein Jahr verspätet ein Einbruch beim Hendlkonsum zu verzeichnen, der sich zwar in den Folgejahren etwas, jedoch nie wieder vollständig erholt hat. Insgesamt ist als ein rücklaufender Trend über die Jahre zu erkennen. Was ist also blos los mit dem Hendlpreis? Hat der Süßmeier wieder seine Finger im Spiel gehabt und aus einem Hendl drei halbe gemacht?
Vielleicht liegt es ja an der Einführung der Bio-Hendl und damit einem sprunghaften Einstieg der Durchschnittspreise? Auch eine Möglichkeit. Der wahrscheinlichste Fall ist jedoch, dass die Methode der Datenerhebung einfach geändert wurde. Werfen wir einen Blick auf die Abschlussbilanz der Wiesn 2000 der Stadt München, so sind dort für die Hendl-Preise zwei Kennzahlen aufgeführt, darunter einmal die Preise ausschließlich in den Festzelten.
1999 | 2000 | |
---|---|---|
Hendlpreis nur in Festzelten | 15,78 DM (8,07 €) | 16,14 DM (8,25 €) |
Hendlpreis | 10,53 DM (5,38 €) | 11,57 DM (5,92 €) |
Hier lässt sich dieser Preisanstieg nicht beobachten. Komischerweise ist aber auch nicht immer die Preise mit einem Umrechnungsfaktor von 1,95583 in € in unseren Daten zu finden. 1999 ist in unseren Daten ein Hendlpreis von 5,83 € gelistet, das scheint der zweiten Zeile zu entsprechen. Im Jahr 2000 sind unseren Daten 7,85 € zu entnehmen. Hier scheint ein Sprung auf die Hendlpreise in den Festzelten gemacht zu sein, oder - da diese Daten sich nicht zu 100% entsprechen - eine andere Mischung aus Preisen. Wie so oft im Data Scientismus lernen wir also auch hier wieder: you need to know your data!
Teuerer ist immer besser¶
Kehren wir zurück zu unseren Daten. Um diese besser in einem Plot vergleichbar zu machen, führen wir eine Z-Score Transformation durch. Das Ergebnis ist der untere Plot. Zwar lassen sich hieraus nun keine absoluten Werte mehr ablesen. Wir können jedoch den oben beschriebenen Zusammenhang zwischen Hendlpreis und Hendlkonsum viel besser visualisieren, ohne dass dazu für jede Variable eine eigene Y-Achse notwendig wäre.
df_z = df_years.copy()
for col in numeric_cols:
df_z[col] = zscore(df_years[col], nan_policy='omit')
numeric_cols = df_z.select_dtypes(include=['float64', 'int64']).columns
numeric_cols = [col for col in numeric_cols if col != 'jahr'] # Jahr ausschließen
fig = px.line(df_z, x='jahr', y=numeric_cols,
title='Oktoberfest Daten - Normalisiert (Z-Score)',
markers=True)
# Define which traces to show by default (by name)
default_visible_traces = ['bier_preis', 'bier_konsum']
# Hide all traces except the specified default ones
for trace in fig.data:
if trace.name not in default_visible_traces:
trace.update(visible='legendonly')
apply_standard_layout(fig, legend_y=-0.25, top_margin=60)
fig.show()
Hier können wir uns auch noch weitere Zusammenhänge visualisieren. Wählen wir beispielsweise Bierpreis und Bierkonsum aus können wir direkt feststellen: Je höher der Preis, desto höher ist auch der Konsum. Den Wiesnwirten können also nur wärmstens empfehlen, die Preise einfach weiter bis ins unermessliche zu erhöhen. Oder aber wir beherzigen den Ratschlag "correlation does not imply causation".
Gratisbier im Augustiner¶
Werfen wir einen genaueren Blick in die Daten stellen wir eher fest, dass in den Jahren nach der Pandemie ein rückläufiger Trend beim Bierkonsum zu beobachten ist. Noch deutlicher wird das wenn wir den Bierkonsum pro Besucher plotten. Hierbei wäre noch interessant, ob der Datensatz auch alkoholfreies Bier beinhaltet und ob auch auf der Wiesn ein Trend zum Alkoholfreien erkennbar ist. Diese Daten sind leider aktuell nicht separat ausgewiesen. Dafür aber die Bierpreise der einzelnen Zelte. Und da sehen wir, Augustiner ist einfach ein Schnapper! Mit 6% unter dem Durschnitt war 2023 quasie jede 17. Maß umsonst. Und das sogar im Holzfass! Na Prostmahlzeit.
numeric_cols = df_years.select_dtypes(include=['float64', 'int64']).columns
numeric_cols = [col for col in numeric_cols if col != 'jahr'] # Jahr ausschließen
# Define which traces to show by default (by name)
default_visible_traces = ['besucher_gesamt', 'bier_konsum']
fig = go.Figure()
for col in numeric_cols:
visible = True if col in default_visible_traces else 'legendonly'
trace = go.Scatter(x=df_years['jahr'], y=df_years[col], mode='lines+markers', name=col, visible=visible)
fig.add_trace(trace)
apply_standard_layout(fig, legend_y=-0.25, top_margin=60)
fig.update_layout(yaxis=dict(autorange=True), yaxis_title='')
fig.show()
Der Blick über den Maßkrug-Rand¶
Bisher haben wir unsere Analysen größtenteils auf die Daten des Oktobefest Datasets beschränkt. Nun möchten wir allerdings die Vorteile des Munich Open Data Portals nutzen auf viele weitere kommunale Datensätze zugreifen zu können und unsere Wiesn Daten mit weiteren Datasets anzureichern. Nach einem Blick auf alle Datensätze haben wir drei weitere relevante Datasets gefunden, die uns helfen können die Oktoberfest Daten noch besser zu verstehen oder die Auswirkungen der Wiesn auf die Stadt München zu verstehen:
- Daten der Raddauerzählstellen (insbesondere die tagesaktuellen Wetterdaten)
- Monatszahlen Tourismus
- Monatszahlen Verkehrsunfälle
Aus den Daten der Raddauerzählstellen können wir uns ab 2008 die Wetterdaten für jeden Tag an den Standorten der einzelnen Raddauerzählstellen ausgeben lassen. Dies beinhaltet die maximal und minimal Temperature, Niederschlag, Bewölkgung und Sonnenstunden. Auf Grunde der Nähe zur Wiesn wählene wir die Daten der Zählstelle an der Arnulfstraße aus. Die eigentlichen Fahrraddaten brauchen wir nicht. Aus den Oktoberfest Daten wissen wir wie lange die Wiesn jedes Jahr gedauert hat. Die Eröffnung der Wiesn findet immer am ersten Samstag nach dem 15. September statt. Daraus können wir ableiten zu welchem Datum jedes Jahr die Wiesn stattgefunden hat. Diese Daten können wir dann für jedes Jahr aus den Wetterdaten extrahieren und aufsummieren (Niederschlag und Sonnenstunden) oder den Mittelwert (Bewölkung und Temperatur) bilden.
Was kann man sich herrlicheres vorstelllen als einen warmen, spätsommerlichen Wiesntag und eine frische Maß dazu? Dazu brauchen wir kein Dataset. Aber wir können der Frage nachgehen, ob der Bierkonsum oder die Besucherzahlen vom Wetter abhängig sind.
search_url = "https://opendata.muenchen.de/api/3/action/package_search"
search_params = {"q": "Tageswerte"}
response = requests.get(search_url, params=search_params)
package = response.json()["result"]["results"][0]
dfs = []
for r in package['resources']:
if 'sonnenstunden' in pd.read_csv(r['url'], nrows=1).columns.tolist(): # wir bekommen für jedes Jahr zwei Ressourcen, wir wollen nur die mit Wetterdaten
df = pd.read_csv(r['url'], low_memory=False)
df['datum'] = pd.to_datetime(df['datum'], errors='coerce') # das Datumsformat ändert sich 2023
dfs.append(df)
df_weather = pd.concat(dfs, ignore_index=True).sort_values('datum')
df_weather = df_weather[df_weather['zaehlstelle'] == 'Arnulf'] # wir nutzen nur die Zählstelle in der Arnulfstraße nahe der Wiesn
# Oktoberfest-Tage sammeln (Eröffnung ist immer der erste Samstag nach dem 15. September)
oktoberfest_dates = []
for _, row in df_years.iterrows():
start = datetime(row['jahr'], 9, 15) + timedelta(days=(5-datetime(row['jahr'], 9, 15).weekday())%7 or 7)
oktoberfest_dates.extend([start + timedelta(days=i) for i in range(row['dauer'])])
# Nur Oktoberfest-Tage filtern
df_weather = df_weather[df_weather['datum'].dt.date.isin([d.date() for d in oktoberfest_dates])]
# Jahr aus Datum extrahieren und aggregieren
df_weather['jahr'] = df_weather['datum'].dt.year
weather_agg = df_weather.groupby('jahr').agg({
'min.temp': 'mean', 'max.temp': 'mean', 'bewoelkung': 'mean',
'niederschlag': 'sum', 'sonnenstunden': 'sum'
}).round(2)
# Mit df_years zusammenführen
df_years = df_years.merge(weather_agg, left_on='jahr', right_index=True, how='left')
fig, axes = plt.subplots(2, 2, figsize=(12, 8), facecolor='none')
weather_vars = ['max.temp', 'bewoelkung', 'niederschlag', 'sonnenstunden']
weather_vars = [
('max.temp', 'Mittelwert der Tageshöchsttemperaturen'),
('bewoelkung', 'Durschnittliche Bewölkung'),
('niederschlag', 'Gesamtniederschlag'),
('sonnenstunden', 'Summe der Sonnenstunden')
]
for i, (var, label) in enumerate(weather_vars):
ax = axes[i//2, i%2]
ax.set_facecolor('none')
sns.regplot(data=df_years, x=var, y='besucher_tag', ax=ax)
corr = df_years[var].corr(df_years['besucher_tag'])
ax.set_title(f'{label} (r={corr:.3f})')
ax.set_xlabel('')
ax.set_ylabel('Besucher pro Tag')
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f'{int(x/1000)}k')) # Y-Achse in Tausend
sns.despine(ax=ax)
plt.tight_layout()
plt.show()
plt.savefig("temp.png", transparent=True)
<Figure size 640x480 with 0 Axes>
Wie sieht es mit Wetter und Bierkonsum aus?¶
Gar nix! Dem gemeinem Wiesnbesucher ist es ziemlich wurscht obs regnt oder schneibt und trinkt gmiatlich sei Bier weida.
Werfen wir einen Blick auf die Korrelationskoeffizienten in den einzelnen Subplots. Temperatur und Niederschlag scheinen in keinerlei Zusammenhang mit dem Bierkonsum zu stehen. Auch bei Bewölkung und Sonnenstunden kann wenn überhaupt nur ein schwacher Zusammenhang beobachtet werden, welcher sogar noch kontraintuitiv ist: bei zunehmender Bewölkung beziehungsweise weniger Sonnenstunden kann ein sehr leicht erhöhter Bierkonsum beobachtet werden. Ein ähnliches Bild zeichnet sich bei den Besucherzahlen ab. Die Temperatur (Tagesmaximalwert) scheint noch den höchsten Zusammenhang mit den Besucherzahlen zu haben und selbst dieser ist noch als schwach zu bezeichnen. Allgemeingültige Aussagen zu möglichen Zusammenhängen zwischen Bierkonsum und Besucherzahlen mit dem Wetter lassen sich also nicht ableiten. Zumindest nicht wenn man die Wetterdaten über die Dauer der Wiesn aggregiert. Vielleicht lassen sich Zusammenhänge finden wenn man die Daten weiter aufbereitet, beispielsweise Regentage und nicht Regentage unterscheidet oder die Wochenenden seperat behandelt. Dies ist aber eine Aufgabe für ein weiteres Notebook. Vielleicht sitzt du ja gerade auch auf der Wiesn und hast nichts besseres zu tun als diese Datengeschichte zu lesen. Was auch immer da schief gelaufen ist, schnapp dir ein Notebook und schick uns deine Analyse.
fig, axes = plt.subplots(2, 2, figsize=(12, 8), facecolor='none')
for i, (var, label) in enumerate(weather_vars):
ax = axes[i//2, i%2]
ax.set_facecolor('none')
sns.regplot(data=df_years, x=var, y='bier_konsum_tag', ax=ax)
corr = df_years[var].corr(df_years['bier_konsum_tag'])
ax.set_title(f'{label} (r={corr:.3f})')
ax.set_xlabel('')
ax.set_ylabel('Bierkonsum pro Tag')
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f'{int(x/1000)}k')) # Y-Achse in Tausend
sns.despine(ax=ax)
plt.tight_layout()
plt.show()
plt.savefig("temp.png", transparent=True)
<Figure size 640x480 with 0 Axes>
Kann ich mit zwei Maß noch fahren?¶
Verlassen wir nun jedoch die Wetterdaten, setzen uns ins Riesenrad und lassen unseren Blick abseits der Wiesn auf die Stadt schweifen. Welche Auswirkungen hat die Wiesn auf Verkehr und Hotelübernachtungen in München?
# 1. München Verkehrsunfälle laden
search_params_unfaelle = {"q": "id:5e73a82b-7cfb-40cc-9b30-45fe5a3fa24e"}
response_unfaelle = requests.get(search_url, params=search_params_unfaelle)
df_unfaelle = pd.read_csv(response_unfaelle.json()["result"]["results"][0]['resources'][0]['url'])
# München Daten bereinigen
df_unfaelle = df_unfaelle[(df_unfaelle['MONAT'] != 'Summe') & (df_unfaelle['AUSPRAEGUNG'] == 'insgesamt')]
df_unfaelle['jahr'] = pd.to_datetime(df_unfaelle['MONAT'], format='%Y%m').dt.year
df_unfaelle['monat_num'] = pd.to_datetime(df_unfaelle['MONAT'], format='%Y%m').dt.month
df_unfaelle = df_unfaelle[df_unfaelle['jahr'].isin(df_years['jahr'].tolist())] # Nur Oktoberfest-Jahre
# 2. Deutschland Verkehrsunfälle laden
df_deutschland = pd.read_csv('Wiesn_Verkehrsunfaelle_Deutschland.csv', delimiter=';')
df_deutschland['value'] = pd.to_numeric(df_deutschland['value'], errors='coerce')
df_deutschland = df_deutschland.dropna(subset=['value'])
df_deutschland['jahr'] = df_deutschland['time'].astype(int)
# COVID-Jahre 2020 und 2021 entfernen
df_deutschland = df_deutschland[~df_deutschland['jahr'].isin([2020, 2021])]
# Monatsnamen zu Zahlen
monat_mapping = {'Januar': 1, 'Februar': 2, 'März': 3, 'April': 4, 'Mai': 5, 'Juni': 6,
'Juli': 7, 'August': 8, 'September': 9, 'Oktober': 10, 'November': 11, 'Dezember': 12}
df_deutschland['monat_num'] = df_deutschland['1_variable_attribute_label'].map(monat_mapping)
# Deutschland Alkoholunfälle filtern
df_deutschland_alkohol = df_deutschland[df_deutschland['4_variable_attribute_label'].str.contains('Alkoholeinfluss', na=False)]
# Deutschland Gesamt-Verkehrsunfälle (ohne Alkohol-Filter)
df_deutschland_gesamt = df_deutschland[~df_deutschland['4_variable_attribute_label'].str.contains('Alkoholeinfluss', na=False)]
# Einwohnerzahlen für Skalierung
einwohner_muenchen, einwohner_deutschland = 1.5e6, 83e6
skalierung = einwohner_muenchen / einwohner_deutschland
# 3. Interaktiver Plot erstellen
fig = go.Figure()
confidence_level = 0.95
# Define which traces to show by default (by name)
default_visible_traces = ['München Alkoholunfälle', 'Deutschland Alkoholunfälle (skaliert)']
# München Verkehrsunfälle (3 Kategorien)
categories = df_unfaelle['MONATSZAHL'].unique()
colors = ['#1f77b4', '#ff7f0e', '#2ca02c']
for i, category in enumerate(categories):
data_cat = df_unfaelle[df_unfaelle['MONATSZAHL'] == category]
monthly_stats = data_cat.groupby('monat_num')['WERT'].agg(['mean', 'std', 'count']).reset_index()
# Konfidenzintervall
monthly_stats['ci'] = monthly_stats.apply(
lambda row: stats.t.ppf((1 + confidence_level) / 2, row['count'] - 1) * row['std'] / np.sqrt(row['count'])
if row['count'] > 1 else 0, axis=1)
monthly_stats['upper'] = monthly_stats['mean'] + monthly_stats['ci']
monthly_stats['lower'] = monthly_stats['mean'] - monthly_stats['ci']
trace_name = f'München {category}'
visible = True if trace_name in default_visible_traces else 'legendonly'
# Konfidenzintervall als Fläche
ci_trace = go.Scatter(
x=list(monthly_stats['monat_num']) + list(monthly_stats['monat_num'][::-1]),
y=list(monthly_stats['upper']) + list(monthly_stats['lower'][::-1]),
fill='toself', fillcolor=f'rgba({int(colors[i][1:3], 16)}, {int(colors[i][3:5], 16)}, {int(colors[i][5:7], 16)}, 0.2)',
line=dict(color='rgba(255,255,255,0)'), showlegend=False, hoverinfo='skip',
legendgroup=f'muenchen_{category}', name=f'München {category} CI', visible=visible)
fig.add_trace(ci_trace)
# Mittellinie
line_trace = go.Scatter(
x=monthly_stats['monat_num'], y=monthly_stats['mean'], mode='lines+markers',
name=trace_name, line=dict(color=colors[i], width=2),
marker=dict(size=6), legendgroup=f'muenchen_{category}', visible=visible)
fig.add_trace(line_trace)
# Deutschland Alkoholunfälle (skaliert)
deutschland_alkohol_stats = df_deutschland_alkohol.groupby(['jahr', 'monat_num'])['value'].sum().reset_index()
deutschland_alkohol_monthly = deutschland_alkohol_stats.groupby('monat_num')['value'].agg(['mean', 'std', 'count']).reset_index()
deutschland_alkohol_monthly['ci'] = deutschland_alkohol_monthly.apply(
lambda row: stats.t.ppf((1 + confidence_level) / 2, row['count'] - 1) * row['std'] / np.sqrt(row['count'])
if row['count'] > 1 else 0, axis=1)
deutschland_alkohol_monthly[['mean', 'upper', 'lower']] = deutschland_alkohol_monthly[['mean', 'std', 'ci']].apply(
lambda x: [x['mean'] * skalierung, (x['mean'] + x['ci']) * skalierung, (x['mean'] - x['ci']) * skalierung], axis=1, result_type='expand')
# Deutschland Alkohol - Konfidenzintervall
fig.add_trace(go.Scatter(
x=list(deutschland_alkohol_monthly['monat_num']) + list(deutschland_alkohol_monthly['monat_num'][::-1]),
y=list(deutschland_alkohol_monthly['upper']) + list(deutschland_alkohol_monthly['lower'][::-1]),
fill='toself', fillcolor='rgba(139, 0, 139, 0.2)', line=dict(color='rgba(255,255,255,0)'),
showlegend=False, hoverinfo='skip', legendgroup='deutschland_alkohol', name='Deutschland Alkohol CI'))
# Deutschland Alkohol - Mittellinie
fig.add_trace(go.Scatter(
x=deutschland_alkohol_monthly['monat_num'], y=deutschland_alkohol_monthly['mean'],
mode='lines+markers', name='Deutschland Alkoholunfälle (skaliert)',
line=dict(color='darkviolet', width=3, dash='dash'), marker=dict(size=8, symbol='diamond'),
legendgroup='deutschland_alkohol'))
# Deutschland Gesamt-Verkehrsunfälle (skaliert)
deutschland_gesamt_stats = df_deutschland_gesamt.groupby(['jahr', 'monat_num'])['value'].sum().reset_index()
deutschland_gesamt_monthly = deutschland_gesamt_stats.groupby('monat_num')['value'].agg(['mean', 'std', 'count']).reset_index()
deutschland_gesamt_monthly['ci'] = deutschland_gesamt_monthly.apply(
lambda row: stats.t.ppf((1 + confidence_level) / 2, row['count'] - 1) * row['std'] / np.sqrt(row['count'])
if row['count'] > 1 else 0, axis=1)
deutschland_gesamt_monthly[['mean', 'upper', 'lower']] = deutschland_gesamt_monthly[['mean', 'std', 'ci']].apply(
lambda x: [x['mean'] * skalierung, (x['mean'] + x['ci']) * skalierung, (x['mean'] - x['ci']) * skalierung], axis=1, result_type='expand')
# Deutschland Gesamt - Konfidenzintervall
fig.add_trace(go.Scatter(
x=list(deutschland_gesamt_monthly['monat_num']) + list(deutschland_gesamt_monthly['monat_num'][::-1]),
y=list(deutschland_gesamt_monthly['upper']) + list(deutschland_gesamt_monthly['lower'][::-1]),
fill='toself', fillcolor='rgba(205, 92, 92, 0.2)', line=dict(color='rgba(255,255,255,0)'),
showlegend=False, hoverinfo='skip', legendgroup='deutschland_gesamt', name='Deutschland Gesamt CI',
visible='legendonly'))
# Deutschland Gesamt - Mittellinie
fig.add_trace(go.Scatter(
x=deutschland_gesamt_monthly['monat_num'], y=deutschland_gesamt_monthly['mean'],
mode='lines+markers', name='Deutschland Verkehrsunfälle gesamt (skaliert)',
line=dict(color='indianred', width=3, dash='dot'), marker=dict(size=8, symbol='square'),
legendgroup='deutschland_gesamt', visible='legendonly'))
# Layout (Minimal, Rest im Helper)
fig.update_layout(
title='Verkehrsunfälle München vs Deutschland nach Monaten<br><sub>Deutschland skaliert auf München-Einwohnerzahl (ohne COVID-Jahre 2020/2021)</sub>',
xaxis_title='Monat', yaxis_title='Anzahl Unfälle',
xaxis=dict(tickmode='array', tickvals=list(range(1, 13)),
ticktext=['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']),
height=700
)
apply_standard_layout(fig, legend_y=-0.25, top_margin=60)
# Oktoberfest-Bereich hervorheben
fig.add_vrect(x0=9.5, x1=10.1, fillcolor="orange", opacity=0.1, layer="below", line_width=0,
annotation_text="Oktoberfest", annotation_position="top left")
fig.show()
Die Verkehrsunfalldaten in München liegen Monatsweise seit 2000 vor und werden unterteilt in Gesamtanzahl, Fluchtunfälle und Alkoholunfälle. Wir wollen der These nachgehen, ob sich während der Wiesn Alkoholunfälle häufen. Zum Vergleich werden dazu auch die Deutschlandweiten Verkehrsunfälle geplottet. Diese Daten werden von Genisis (Destatis) bezogen und sind seit 2011 gelistet. Die Daten wurden skaliert auf die Einwohnerzahl Münchens.
Betrachten wir die Daten stellen wir zunächst einen sehr saisonalen Verlauf der Unfalldaten fest, mit einer Zunahme in den Sommermonaten. In München sind diese mit einem sehr starken Rückgang im August verbunden, welcher sich vermutlich auf die Bayrischen Sommerschulferien zurückführen lässt. Deutschland weit ist dieses lokale Minimum im August ebenfalls zu beobachten, wenn auch bei weitem nicht so startk ausgeprägt. Deutschlandweit zeichnet sich für die Unfälle verursacht durch Alkohol ein ähnliches Bild ab. Ein anderes Bild ergibt sich jedoch für München. Hier ist das Maximum im September, der Monat in dem der Großteil der Wiesn stattfindet.
Die Wiesn als Tourismusmagnet¶
Auch die Münchner Tourismuszahlen in Form von Übernachtungen folgen im Vergleich zu den bundesweiten Zahlen einem ähnlichen Trend: auch hier scheint das Oktoberfest den saisonalen Verlauf der Daten in den September hinein zu verlängern.
# Tourismus-Vergleich München vs Deutschland nach dem Vorbild der Verkehrsunfälle
# 1. München Tourismus-Daten laden (bereits vorhanden, aber nochmal für Klarheit)
search_params_tourismus = {"q": "id:3621ad08-aa97-4c2b-b0b0-82780375743c"}
response_tourismus = requests.get(search_url, params=search_params_tourismus)
df_tourismus_muenchen = pd.read_csv(response_tourismus.json()["result"]["results"][0]['resources'][0]['url'])
# München Daten bereinigen
df_tourismus_muenchen = df_tourismus_muenchen[df_tourismus_muenchen['MONAT'] != 'Summe']
df_tourismus_muenchen['jahr'] = pd.to_datetime(df_tourismus_muenchen['MONAT'], format='%Y%m').dt.year
df_tourismus_muenchen['monat_num'] = pd.to_datetime(df_tourismus_muenchen['MONAT'], format='%Y%m').dt.month
df_tourismus_muenchen = df_tourismus_muenchen[df_tourismus_muenchen['jahr'].isin(df_years['jahr'].tolist())]
# 2. Deutschland Tourismus-Daten laden
df_tourismus_deutschland = pd.read_csv('Wiesn_Uebernachtungen_Deutschland.csv', delimiter=';')
df_tourismus_deutschland['value'] = pd.to_numeric(df_tourismus_deutschland['value'], errors='coerce')
df_tourismus_deutschland = df_tourismus_deutschland.dropna(subset=['value'])
df_tourismus_deutschland['jahr'] = df_tourismus_deutschland['time'].astype(int)
# COVID-Jahre 2020 und 2021 entfernen
df_tourismus_deutschland = df_tourismus_deutschland[~df_tourismus_deutschland['jahr'].isin([2020, 2021])]
# Monatsnamen zu Zahlen
monat_mapping = {'Januar': 1, 'Februar': 2, 'März': 3, 'April': 4, 'Mai': 5, 'Juni': 6,
'Juli': 7, 'August': 8, 'September': 9, 'Oktober': 10, 'November': 11, 'Dezember': 12}
df_tourismus_deutschland['monat_num'] = df_tourismus_deutschland['1_variable_attribute_label'].map(monat_mapping)
# Deutschland Tourismus-Kategorien filtern
df_deutschland_ankuenfte = df_tourismus_deutschland[df_tourismus_deutschland['value_variable_label'] == 'Ankünfte']
df_deutschland_uebernachtungen = df_tourismus_deutschland[df_tourismus_deutschland['value_variable_label'] == 'Übernachtungen']
# 3. Interaktiver Plot erstellen
fig_tourismus_vergleich = go.Figure()
confidence_level = 0.95
# Define which traces to show by default (by name)
default_visible_traces = ['München Ankünfte', 'München Übernachtungen']
# München Tourismus-Kategorien (2 Kategorien)
tourismus_categories = df_tourismus_muenchen['MONATSZAHL'].unique()
colors = ['#1f77b4', '#ff7f0e']
for i, category in enumerate(tourismus_categories):
data_cat = df_tourismus_muenchen[df_tourismus_muenchen['MONATSZAHL'] == category]
monthly_stats = data_cat.groupby('monat_num')['WERT'].agg(['mean', 'std', 'count']).reset_index()
# Konfidenzintervall
monthly_stats['ci'] = monthly_stats.apply(
lambda row: stats.t.ppf((1 + confidence_level) / 2, row['count'] - 1) * row['std'] / np.sqrt(row['count'])
if row['count'] > 1 else 0, axis=1)
monthly_stats['upper'] = monthly_stats['mean'] + monthly_stats['ci']
monthly_stats['lower'] = monthly_stats['mean'] - monthly_stats['ci']
trace_name = f'München {category}'
visible = True if trace_name in default_visible_traces else 'legendonly'
# Konfidenzintervall als Fläche
ci_trace = go.Scatter(
x=list(monthly_stats['monat_num']) + list(monthly_stats['monat_num'][::-1]),
y=list(monthly_stats['upper']) + list(monthly_stats['lower'][::-1]),
fill='toself', fillcolor=f'rgba({int(colors[i][1:3], 16)}, {int(colors[i][3:5], 16)}, {int(colors[i][5:7], 16)}, 0.2)',
line=dict(color='rgba(255,255,255,0)'), showlegend=False, hoverinfo='skip',
legendgroup=f'muenchen_{category}', name=f'München {category} CI', visible=visible)
fig_tourismus_vergleich.add_trace(ci_trace)
# Mittellinie
line_trace = go.Scatter(
x=monthly_stats['monat_num'], y=monthly_stats['mean'], mode='lines+markers',
name=trace_name, line=dict(color=colors[i], width=2),
marker=dict(size=6), legendgroup=f'muenchen_{category}', visible=visible)
fig_tourismus_vergleich.add_trace(line_trace)
# Deutschland Ankünfte (skaliert)
deutschland_ankuenfte_stats = df_deutschland_ankuenfte.groupby(['jahr', 'monat_num'])['value'].sum().reset_index()
deutschland_ankuenfte_monthly = deutschland_ankuenfte_stats.groupby('monat_num')['value'].agg(['mean', 'std', 'count']).reset_index()
deutschland_ankuenfte_monthly['ci'] = deutschland_ankuenfte_monthly.apply(
lambda row: stats.t.ppf((1 + confidence_level) / 2, row['count'] - 1) * row['std'] / np.sqrt(row['count'])
if row['count'] > 1 else 0, axis=1)
deutschland_ankuenfte_monthly[['mean', 'upper', 'lower']] = deutschland_ankuenfte_monthly[['mean', 'std', 'ci']].apply(
lambda x: [x['mean'] * skalierung, (x['mean'] + x['ci']) * skalierung, (x['mean'] - x['ci']) * skalierung], axis=1, result_type='expand')
# Deutschland Ankünfte - Konfidenzintervall
fig_tourismus_vergleich.add_trace(go.Scatter(
x=list(deutschland_ankuenfte_monthly['monat_num']) + list(deutschland_ankuenfte_monthly['monat_num'][::-1]),
y=list(deutschland_ankuenfte_monthly['upper']) + list(deutschland_ankuenfte_monthly['lower'][::-1]),
fill='toself', fillcolor='rgba(139, 0, 139, 0.2)', line=dict(color='rgba(255,255,255,0)'),
showlegend=False, hoverinfo='skip', legendgroup='deutschland_ankuenfte', name='Deutschland Ankünfte CI',
visible='legendonly'))
# Deutschland Ankünfte - Mittellinie
fig_tourismus_vergleich.add_trace(go.Scatter(
x=deutschland_ankuenfte_monthly['monat_num'], y=deutschland_ankuenfte_monthly['mean'],
mode='lines+markers', name='Deutschland Ankünfte (skaliert)',
line=dict(color='darkviolet', width=3, dash='dash'), marker=dict(size=8, symbol='diamond'),
legendgroup='deutschland_ankuenfte', visible='legendonly'))
# Deutschland Übernachtungen (skaliert)
deutschland_uebernachtungen_stats = df_deutschland_uebernachtungen.groupby(['jahr', 'monat_num'])['value'].sum().reset_index()
deutschland_uebernachtungen_monthly = deutschland_uebernachtungen_stats.groupby('monat_num')['value'].agg(['mean', 'std', 'count']).reset_index()
deutschland_uebernachtungen_monthly['ci'] = deutschland_uebernachtungen_monthly.apply(
lambda row: stats.t.ppf((1 + confidence_level) / 2, row['count'] - 1) * row['std'] / np.sqrt(row['count'])
if row['count'] > 1 else 0, axis=1)
deutschland_uebernachtungen_monthly[['mean', 'upper', 'lower']] = deutschland_uebernachtungen_monthly[['mean', 'std', 'ci']].apply(
lambda x: [x['mean'] * skalierung, (x['mean'] + x['ci']) * skalierung, (x['mean'] - x['ci']) * skalierung], axis=1, result_type='expand')
# Deutschland Übernachtungen - Konfidenzintervall
fig_tourismus_vergleich.add_trace(go.Scatter(
x=list(deutschland_uebernachtungen_monthly['monat_num']) + list(deutschland_uebernachtungen_monthly['monat_num'][::-1]),
y=list(deutschland_uebernachtungen_monthly['upper']) + list(deutschland_uebernachtungen_monthly['lower'][::-1]),
fill='toself', fillcolor='rgba(205, 92, 92, 0.2)', line=dict(color='rgba(255,255,255,0)'),
showlegend=False, hoverinfo='skip', legendgroup='deutschland_uebernachtungen', name='Deutschland Übernachtungen CI'))
# Deutschland Übernachtungen - Mittellinie
fig_tourismus_vergleich.add_trace(go.Scatter(
x=deutschland_uebernachtungen_monthly['monat_num'], y=deutschland_uebernachtungen_monthly['mean'],
mode='lines+markers', name='Deutschland Übernachtungen (skaliert)',
line=dict(color='indianred', width=3, dash='dot'), marker=dict(size=8, symbol='square'),
legendgroup='deutschland_uebernachtungen'))
# Layout
fig_tourismus_vergleich.update_layout(
title='Tourismus München vs Deutschland nach Monaten<br><sub>Deutschland skaliert auf München-Einwohnerzahl (ohne COVID-Jahre 2020/2021)</sub>',
xaxis_title='Monat', yaxis_title='Anzahl (Gäste/Übernachtungen)',
xaxis=dict(tickmode='array', tickvals=list(range(1, 13)),
ticktext=['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']),
hovermode='x unified', height=700,
legend=dict(
orientation="h",
yanchor="top",
y=-0.07,
xanchor="center",
x=0.5,
title_text='',
traceorder='normal'
)
)
apply_standard_layout(fig_tourismus_vergleich, legend_y=-0.25, top_margin=60)
fig_tourismus_vergleich.show()
Nun stellt sich die Frage, wie wir die Daten über reine Beobachtungen hinaus miteinander vergleichen können. Zunächst stellen wir die Hypothese auf, dass währende der Wiesn eine erhöhte Anzahl von Alkoholunfällen und Übernachtungen auftritt. Als Vergleich nutzen wir die bundesweiten Daten. Zum Test dieser Hypothese eignet sich am besten die differences in differences (DID) Methode.
Die Wiesn vs. Deutschland¶
Dabei vergleichen wir die Entwicklung der Zielgrößen im Oktober (Wiesn-Monat) in München (Treatment-Gruppe) mit der Entwicklung im selben Zeitraum in Deutschland (Kontrollgruppe) über mehrere Jahre. Die DiD-Methode kontrolliert für allgemeine Trends und Unterschiede zwischen den Gruppen, indem sie die Differenz der Differenzen betrachtet. Als Schätzmodell verwenden wir eine Poisson-Regression mit Fixed Effects für das Jahr. Die Interaktion von Treatment (München) und Zeit (Oktober) liefert den geschätzten Wiesn-Effekt.
# DataFrame für DiD-Analyse erstellen (nur ab 2011, Deutschland monatsweise aufsummiert)
# 1. München: Alkoholunfälle extrahieren
muc = df_unfaelle[df_unfaelle['MONATSZAHL'].str.contains('Alkohol')].copy()
muc_df = muc[['jahr', 'monat_num', 'WERT']].rename(columns={'jahr': 'Jahr', 'monat_num': 'Monat', 'WERT': 'Alkoholunfälle'})
muc_df['Ort'] = 'München'
muc_df['treatment'] = 1
# 2. Deutschland: Alkoholunfälle extrahieren und monatsweise aufsummieren
deu = df_deutschland[df_deutschland['4_variable_attribute_label'].str.contains('Alkoholeinfluss', na=False)].copy()
deu_grouped = deu.groupby(['jahr', 'monat_num'], as_index=False)['value'].sum()
deu_df = deu_grouped.rename(columns={'jahr': 'Jahr', 'monat_num': 'Monat', 'value': 'Alkoholunfälle'})
deu_df['Ort'] = 'Deutschland'
deu_df['treatment'] = 0
# 3. Kombinieren und sortieren
did_df = pd.concat([muc_df, deu_df], ignore_index=True)
did_df = did_df[(did_df['Jahr'] != 2020) & (did_df['Jahr'] != 2021) & (did_df['Jahr'] >= 2011)]
# 4. time-Variable: 1 für Oktober, sonst 0 (außer 2020/2021, die sind schon entfernt)
did_df['time'] = (did_df['Monat'] == 10).astype(int)
# 5. DiD Variable: Interaktion treatment (München) und time (Oktober)
did_df['did'] = did_df['treatment'] * did_df['time']
# 6. Spaltenreihenfolge
cols = ['Jahr', 'Monat', 'Ort', 'Alkoholunfälle', 'treatment', 'time', 'did']
did_df = did_df[cols].sort_values(['Jahr', 'Monat', 'Ort']).reset_index(drop=True)
# 7. DiD-Modell: Poisson-Regression (geeignet für Zähldaten)
formula = 'Alkoholunfälle ~ treatment + time + did + C(Jahr) + C(Monat)'
model = smf.poisson(formula=formula, data=did_df).fit()
print(model.summary())
# Effektgröße und Interpretation
did_coef = model.params['did']
did_se = model.bse['did']
did_p = model.pvalues['did']
did_ci = model.conf_int().loc['did']
print(f"\nDiD-Koeffizient: {did_coef:.3f} (SE={did_se:.3f}, p={did_p:.3g})")
print(f"95%-Konfidenzintervall: [{did_ci[0]:.3f}, {did_ci[1]:.3f}]")
print(f"Exponentiierter Effekt (IRR): {np.exp(did_coef):.3f}")
if did_p < 0.05:
print('Der Effekt ist statistisch signifikant: Während der Wiesn treten in München signifikant mehr Alkoholunfälle auf als im Rest Deutschlands (kontrolliert für Monat und Jahr).')
else:
print('Der Effekt ist nicht signifikant: Es gibt keinen statistisch nachweisbaren Anstieg der Alkoholunfälle in München während der Wiesn im Vergleich zu Deutschland.')
Optimization terminated successfully. Current function value: 5.301273 Iterations 8 Poisson Regression Results ============================================================================== Dep. Variable: Alkoholunfälle No. Observations: 288 Model: Poisson Df Residuals: 263 Method: MLE Df Model: 24 Date: Fri, 19 Sep 2025 Pseudo R-squ.: 0.9859 Time: 09:27:42 Log-Likelihood: -1526.8 converged: True LL-Null: -1.0801e+05 Covariance Type: nonrobust LLR p-value: 0.000 =================================================================================== coef std err z P>|z| [0.025 0.975] ----------------------------------------------------------------------------------- Intercept 6.8353 0.007 950.576 0.000 6.821 6.849 C(Jahr)[T.2012] -0.0463 0.011 -4.150 0.000 -0.068 -0.024 C(Jahr)[T.2013] -0.1263 0.011 -11.085 0.000 -0.149 -0.104 C(Jahr)[T.2014] -0.1520 0.011 -13.251 0.000 -0.175 -0.130 C(Jahr)[T.2015] -0.1773 0.012 -15.346 0.000 -0.200 -0.155 C(Jahr)[T.2016] -0.1728 0.012 -14.981 0.000 -0.195 -0.150 C(Jahr)[T.2017] -0.1700 0.012 -14.749 0.000 -0.193 -0.147 C(Jahr)[T.2018] -0.1289 0.011 -11.308 0.000 -0.151 -0.107 C(Jahr)[T.2019] -0.1255 0.011 -11.015 0.000 -0.148 -0.103 C(Jahr)[T.2022] 0.0589 0.011 5.416 0.000 0.038 0.080 C(Jahr)[T.2023] -0.0195 0.011 -1.762 0.078 -0.041 0.002 C(Jahr)[T.2024] -0.0755 0.011 -6.717 0.000 -0.098 -0.053 C(Monat)[T.2] -0.0086 0.010 -0.875 0.382 -0.028 0.011 C(Monat)[T.3] 0.1296 0.008 15.368 0.000 0.113 0.146 C(Monat)[T.4] 0.2741 0.009 31.820 0.000 0.257 0.291 C(Monat)[T.5] 0.5614 0.006 87.132 0.000 0.549 0.574 C(Monat)[T.6] 0.5717 0.008 75.734 0.000 0.557 0.587 C(Monat)[T.7] 0.5999 0.007 81.443 0.000 0.585 0.614 C(Monat)[T.8] 0.5650 0.005 109.954 0.000 0.555 0.575 C(Monat)[T.9] 0.4781 0.007 71.110 0.000 0.465 0.491 C(Monat)[T.10] 0.1932 nan nan nan nan nan C(Monat)[T.11] 0.2461 nan nan nan nan nan C(Monat)[T.12] 0.2448 0.009 27.442 0.000 0.227 0.262 treatment -3.5662 0.015 -236.535 0.000 -3.596 -3.537 time 0.1932 nan nan nan nan nan did 0.0058 0.051 0.112 0.911 -0.095 0.107 =================================================================================== DiD-Koeffizient: 0.006 (SE=0.051, p=0.911) 95%-Konfidenzintervall: [-0.095, 0.107] Exponentiierter Effekt (IRR): 1.006 Der Effekt ist nicht signifikant: Es gibt keinen statistisch nachweisbaren Anstieg der Alkoholunfälle in München während der Wiesn im Vergleich zu Deutschland.
# DataFrame für DiD-Analyse: Übernachtungszahlen München vs. Deutschland (nur ab 2011, Deutschland monatsweise aufsummiert)
# 1. München: Übernachtungen extrahieren und aufsummieren (nur eine Zeile pro Jahr/Monat)
muc_ue = df_tourismus_muenchen[df_tourismus_muenchen['MONATSZAHL'].str.contains('Übernachtungen')].copy()
muc_ue_grouped = muc_ue.groupby(['jahr', 'monat_num'], as_index=False)['WERT'].sum()
muc_ue_df = muc_ue_grouped.rename(columns={'jahr': 'Jahr', 'monat_num': 'Monat', 'WERT': 'Übernachtungen'})
muc_ue_df['Ort'] = 'München'
muc_ue_df['treatment'] = 1
# 2. Deutschland: Übernachtungen extrahieren und monatsweise aufsummieren
deu_ue = df_deutschland_uebernachtungen.copy()
deu_ue_grouped = deu_ue.groupby(['jahr', 'monat_num'], as_index=False)['value'].sum()
deu_ue_df = deu_ue_grouped.rename(columns={'jahr': 'Jahr', 'monat_num': 'Monat', 'value': 'Übernachtungen'})
deu_ue_df['Ort'] = 'Deutschland'
deu_ue_df['treatment'] = 0
# 3. Kombinieren und sortieren
did_ue_df = pd.concat([muc_ue_df, deu_ue_df], ignore_index=True)
did_ue_df = did_ue_df[(did_ue_df['Jahr'] != 2020) & (did_ue_df['Jahr'] != 2021) & (did_ue_df['Jahr'] >= 2011)]
# 4. time-Variable: 1 für Oktober, sonst 0 (außer 2020/2021, die sind schon entfernt)
did_ue_df['time'] = (did_ue_df['Monat'] == 10).astype(int)
# 5. DiD Variable: Interaktion treatment (München) und time (Oktober)
did_ue_df['did'] = did_ue_df['treatment'] * did_ue_df['time']
# 6. Spaltenreihenfolge
cols_ue = ['Jahr', 'Monat', 'Ort', 'Übernachtungen', 'treatment', 'time', 'did']
did_ue_df = did_ue_df[cols_ue].sort_values(['Jahr', 'Monat', 'Ort']).reset_index(drop=True)
# Ausgabe
print(did_ue_df.head(10))
# Difference-in-Differences (DiD) Analyse: Steigen die Übernachtungen in München während der Wiesn im Vergleich zu Deutschland?
# ACHTUNG: Weniger Dummy-Variablen, um Multikollinearität zu vermeiden
formula_ue = 'Übernachtungen ~ treatment + time + did + C(Jahr)'
model_ue = smf.poisson(formula=formula_ue, data=did_ue_df).fit()
print(model_ue.summary())
# Effektgröße und Interpretation
did_coef_ue = model_ue.params['did']
did_se_ue = model_ue.bse['did']
did_p_ue = model_ue.pvalues['did']
did_ci_ue = model_ue.conf_int().loc['did']
print(f"\nDiD-Koeffizient: {did_coef_ue:.3f} (SE={did_se_ue:.3f}, p={did_p_ue:.3g})")
print(f"95%-Konfidenzintervall: [{did_ci_ue[0]:.3f}, {did_ci_ue[1]:.3f}]")
print(f"Exponentiierter Effekt (IRR): {np.exp(did_coef_ue):.3f}")
if did_p_ue < 0.05:
print('Der Effekt ist statistisch signifikant: Während der Wiesn gibt es in München signifikant mehr Übernachtungen als im Rest Deutschlands (kontrolliert für Jahr).')
else:
print('Der Effekt ist nicht signifikant: Es gibt keinen statistisch nachweisbaren Anstieg der Übernachtungen in München während der Wiesn im Vergleich zu Deutschland.')
Jahr Monat Ort Übernachtungen treatment time did 0 2011 1 Deutschland 20061022.0 0 0 0 1 2011 1 München 1475830.0 1 0 0 2 2011 2 Deutschland 21058968.0 0 0 0 3 2011 2 München 1488976.0 1 0 0 4 2011 3 Deutschland 25068914.0 0 0 0 5 2011 3 München 1626770.0 1 0 0 6 2011 4 Deutschland 31257046.0 0 0 0 7 2011 4 München 1807960.0 1 0 0 8 2011 5 Deutschland 35317359.0 0 0 0 9 2011 5 München 1990686.0 1 0 0 Optimization terminated successfully. Current function value: 762825.937726 Iterations 7 Poisson Regression Results ============================================================================== Dep. Variable: Übernachtungen No. Observations: 294 Model: Poisson Df Residuals: 278 Method: MLE Df Model: 15 Date: Fri, 19 Sep 2025 Pseudo R-squ.: 0.9228 Time: 09:27:42 Log-Likelihood: -2.2427e+08 converged: True LL-Null: -2.9040e+09 Covariance Type: nonrobust LLR p-value: 0.000 =================================================================================== coef std err z P>|z| [0.025 0.975] ----------------------------------------------------------------------------------- Intercept 17.2868 4.93e-05 3.51e+05 0.000 17.287 17.287 C(Jahr)[T.2012] 0.0360 6.87e-05 524.586 0.000 0.036 0.036 C(Jahr)[T.2013] 0.0492 6.84e-05 718.754 0.000 0.049 0.049 C(Jahr)[T.2014] 0.0790 6.8e-05 1162.548 0.000 0.079 0.079 C(Jahr)[T.2015] 0.1083 6.75e-05 1604.993 0.000 0.108 0.108 C(Jahr)[T.2016] 0.1317 6.71e-05 1962.692 0.000 0.132 0.132 C(Jahr)[T.2017] 0.1621 6.66e-05 2432.536 0.000 0.162 0.162 C(Jahr)[T.2018] 0.2065 6.6e-05 3129.697 0.000 0.206 0.207 C(Jahr)[T.2019] 0.2446 6.54e-05 3739.556 0.000 0.244 0.245 C(Jahr)[T.2022] 0.1471 6.69e-05 2200.374 0.000 0.147 0.147 C(Jahr)[T.2023] 0.2301 6.56e-05 3505.748 0.000 0.230 0.230 C(Jahr)[T.2024] 0.2508 6.53e-05 3838.332 0.000 0.251 0.251 C(Jahr)[T.2025] 0.1453 8.31e-05 1747.349 0.000 0.145 0.145 treatment -2.6894 5.67e-05 -4.74e+04 0.000 -2.690 -2.689 time 0.1160 4.7e-05 2467.512 0.000 0.116 0.116 did 0.0342 0.000 186.136 0.000 0.034 0.035 =================================================================================== DiD-Koeffizient: 0.034 (SE=0.000, p=0) 95%-Konfidenzintervall: [0.034, 0.035] Exponentiierter Effekt (IRR): 1.035 Der Effekt ist statistisch signifikant: Während der Wiesn gibt es in München signifikant mehr Übernachtungen als im Rest Deutschlands (kontrolliert für Jahr).
Langsam läuten wir das Ende unseres Wiesn-Datenbesuchs ein. Doch ehe sich die Band zum letzen Lied bereit macht werfen wir noch einen kurzen Blick auf die DID-Ergebnisse. Das Modell zu Alkoholeinflüssen konnte keinen signifikanten Effekt der Wiesn feststellen. Laut dem zweiten Modell ist die Wiesn jedoch für mehr Übernachtungen im September als im bundesweiten Vergleich verantwortlich. Das hört sich doch nach erfreulichen Ergebnissen an. Wie eine fremde Maß Bier sind diese jedoch mit Vorsicht zu genießen. Die Daten beruhen lediglich auf Monatszahlen, die Wiesn findet jedoch nur in den letzten 1,5-2 Wochen im September und Anfang Oktober statt. Zudem sind die Münchner Zahlen - wenn auch nur zu einem kleinen Anteil - auch in den Bundesweiten Zahlen enthalten. Deutlich besser geeignet wären Unfallzahlen für beide Orte getrennt auf Basis von Tageswerten. Diese könnten zudem mit Wetterdaten und weiteren Informationen zum Tag (Wochenende oder Feiertag) angereichert werden, um belastbare Aussagen zu treffen. Diese Analysen sind somit eher als Inspiration für folgende Notebooks hier im Datengarten zu sehen.
Damit sind wir nun wirklich am Ende. Wir haben jede Menge über die Wiesn gelernt und jetzt bleibt uns nur noch eins zu sagen: I'm loving angels instead!