Titel: Nomen est Omen? Description: Vornamenssuche leicht gemacht Dataset: https://opendata.muenchen.de/dataset/99ad40ec-9d7b-4a2e-87eb-9bac783fb57a/ Landing: true
# === DO NOT PLOT ===
import matplotlib.pyplot as plt
import seaborn as sns
# === Farbschema ===
primary_color = "#07A299"
background_color = "#f2f2f2"
text_color = "#282a36"
grid_color = "#44475a"
# === Matplotlib Style global anpassen ===
plt.rcParams.update({
# Transparenz für gespeicherte Plots
'savefig.transparent': True,
# Transparenz im Plot selbst
'figure.facecolor': 'none', # GANZES Bild transparent
'axes.facecolor': 'none', # Plot-Hintergrund transparent
# Farben und Achsendesign
'axes.edgecolor': text_color,
'axes.labelcolor': text_color,
'xtick.color': text_color,
'ytick.color': text_color,
'text.color': text_color,
# Schriftgrößen & Typografie
'axes.titleweight': 'bold',
'axes.titlesize': 14,
'axes.labelsize': 12,
'xtick.labelsize': 10,
'ytick.labelsize': 10,
# Gitter
'grid.color': grid_color,
'grid.linestyle': '--',
'grid.alpha': 0.5,
# Legende
'legend.edgecolor': 'none',
'legend.facecolor': 'none' # Auch Legendenhintergrund transparent
})
# === Seaborn-Design setzen (für Konsistenz) ===
sns.set_style("whitegrid", {
'axes.facecolor': 'none',
'grid.color': grid_color,
'grid.linestyle': '--',
'axes.edgecolor': background_color, # Optional: weniger visuell störend
'axes.labelcolor': text_color,
'xtick.color': text_color,
'ytick.color': text_color,
'text.color': text_color
})
# === Farbpalette definieren ===
custom_palette = [primary_color]
secondary_colors = ["#ffa600", "#ff6361", "#bc5090", "#58508d", "#003f5c"]
sns.set_palette([primary_color] + secondary_colors)
import matplotlib_inline
matplotlib_inline.backend_inline.set_matplotlib_formats('svg')
Nomen est Omen? - Ein datenjournalistischer Abenteuerroman in mehreren Codeblöcken¶
Philipp und ich, zwei werdende Väter, sitzen an einem sonnigen Mittwochabend im Biergarten und stellen uns eine der vielleicht schwierigsten Frage unseres bisherigen Lebens: Wie nennen wir unsere Kinder? Nicht zu selten, nicht zu abgefahren, keine falschen Assoziationen, bitte leicht auszusprechen – und natürlich soll das Kind damit später Bundeskanzlerin, Gitarrist, Professorin, Bürgermeister oder wenigstens glücklich werden können. Kein Druck also.
Münchner Kindl - Holen wir uns die Namen!¶
Wir starten unsere Mission dort, wo jede gute Geschichte beginnt: im Datenportal der Stadt München. Hier werden die gemeldeten Vornamen der letzten Jahre aller neuen Münchner Kindl gesammelt. Warum sich also nicht etwas inspierieren lassen? Mit dem Suchparameter "vornamen" bekommen wir eine Liste aller verfügbaren Datensets, welche in ihrem Dateinamen "vornamen" beinhalten.
# Vornamen Dataset: https://opendata.muenchen.de/dataset/99ad40ec-9d7b-4a2e-87eb-9bac783fb57a/
import requests
search_url = "https://opendata.muenchen.de/api/3/action/package_search"
search_params = {"q": "vornamen"}
response = requests.get(search_url, params=search_params)
package = response.json()["result"]["results"][0]
for resource in package['resources']:
print(resource['url'])
https://opendata.muenchen.de/dataset/99ad40ec-9d7b-4a2e-87eb-9bac783fb57a/resource/9f393a00-aa64-4733-8c8c-80eabc18f95a/download/vornamen_muenchen2024.csv https://opendata.muenchen.de/dataset/99ad40ec-9d7b-4a2e-87eb-9bac783fb57a/resource/02ab322e-d33e-4447-a9ab-23df63dfa7e1/download/vornamen-muenchen-2023.csv https://opendata.muenchen.de/dataset/99ad40ec-9d7b-4a2e-87eb-9bac783fb57a/resource/5f11d3a0-4779-4f64-b113-b59326e6d839/download/open_data_portal_2022.csv https://opendata.muenchen.de/dataset/99ad40ec-9d7b-4a2e-87eb-9bac783fb57a/resource/dc6170db-f7f4-4bdc-b790-13df55f0cf64/download/vornamen_2021.csv https://opendata.muenchen.de/dataset/99ad40ec-9d7b-4a2e-87eb-9bac783fb57a/resource/48a4c5fd-f5f2-4c0c-bcb3-222fd9ebda67/download/vornamen_2020.csv https://opendata.muenchen.de/dataset/99ad40ec-9d7b-4a2e-87eb-9bac783fb57a/resource/523d5860-25eb-4bea-96e3-193d1dacfb8f/download/vornamen2019.csv https://opendata.muenchen.de/dataset/99ad40ec-9d7b-4a2e-87eb-9bac783fb57a/resource/23fdd53e-485a-46bd-abb6-d7a51b2ebb18/download/vornamen_2018.csv https://opendata.muenchen.de/dataset/99ad40ec-9d7b-4a2e-87eb-9bac783fb57a/resource/2c70d289-8b1a-4071-9739-866c5e532b77/download/vornamen2017.csv https://opendata.muenchen.de/dataset/99ad40ec-9d7b-4a2e-87eb-9bac783fb57a/resource/6655ec3c-251a-4a5e-9a57-d60c4ad59ca3/download/vornamen2016.csv https://opendata.muenchen.de/dataset/99ad40ec-9d7b-4a2e-87eb-9bac783fb57a/resource/c11e8fef-94c8-4e16-a769-70dc6b98b69b/download/vornamen_2015.csv https://opendata.muenchen.de/dataset/99ad40ec-9d7b-4a2e-87eb-9bac783fb57a/resource/5d581ba3-4c7e-4057-8be1-04d56bb0d8d6/download/vornamen_2014.csv https://opendata.muenchen.de/dataset/99ad40ec-9d7b-4a2e-87eb-9bac783fb57a/resource/17c4915d-6b8b-470f-a6b6-31f61b10a285/download/vornamen_2013.csv
Sieht doch schon mal super aus, jetzt also ab damit in eine einzelne, saubere Tabelle. Datenanalyse kann so schön sein!
import pandas as pd
df = pd.DataFrame()
for resource in package['resources']:
df_jahr = pd.read_csv(resource['url'], delimiter=',')
Jahreszahl = int(resource['url'][-8:-4]) # hier holen wir uns die Jahreszahl aus der URL, das 8.letzte bis 5.letzte Zeichen aus dem String
df_jahr['jahr'] = Jahreszahl
df = pd.concat([df, df_jahr], ignore_index=True)
df
| vorname | anzahl | geschlecht | jahr | |
|---|---|---|---|---|
| 0 | Felix | 100 | m | 2024 |
| 1 | Maximilian | 94 | m | 2024 |
| 2 | Leon | 87 | m | 2024 |
| 3 | Paul | 86 | m | 2024 |
| 4 | Emilia | 85 | w | 2024 |
| ... | ... | ... | ... | ... |
| 50659 | Zora | 4 oder weniger | w | 2013 |
| 50660 | Zoran | 4 oder weniger | m | 2013 |
| 50661 | Zorica | 4 oder weniger | w | 2013 |
| 50662 | Zoubair | 4 oder weniger | m | 2013 |
| 50663 | Züleyha | 4 oder weniger | w | 2013 |
50664 rows × 4 columns
Ergebnis: Wir haben ein paar CSV-Dateien mit Babynamen, die jährlich veröffentlicht werden. Leider nicht immer gleich formatiert. Aber hey – was ist schon ein bisschen Chaos unter Freunden?
Daten putzen – aka das IKEA-Regal der Datenanalyse¶
Also standardisieren wir Spaltennamen, erkennen mehr oder weniger obskure Trennzeichen, verschiedene Schreibweisen von Spalten, und überhaupt: Wer unterscheidet bitte heute noch zwischen Groß- und Kleinschreibung? – kurz; wir kämpfen uns durch den Datendschungel.
df = pd.DataFrame()
# Mehrere Spalten standardisieren
mapping = {
'vorname': 'vorname',
'vornamen': 'vorname',
'anzahl': 'anzahl',
'häufigkeit': 'anzahl',
'geschlecht': 'geschlecht',
'gender': 'geschlecht'
}
for resource in package['resources']:
df_jahr = pd.read_csv(resource['url'], sep=',|;', engine='python') # versuche ; oder (das oder wird durch | ausgedrückt) ,
Jahreszahl = int(resource['url'][-8:-4]) # hier holen wir uns die Jahreszahl aus der URL, das 8.letzte bis 5.letzte Zeichen aus dem String
df_jahr['jahr'] = Jahreszahl
df_jahr = df_jahr.rename(columns=lambda x: mapping.get(x.lower(), x)) # Spaltennamen standardisieren, unabhängig von Groß-/Kleinschreibung
df = pd.concat([df, df_jahr], ignore_index=True) # Zusammenführen der Daten
df
| vorname | anzahl | geschlecht | jahr | |
|---|---|---|---|---|
| 0 | Felix | 100 | m | 2024 |
| 1 | Maximilian | 94 | m | 2024 |
| 2 | Leon | 87 | m | 2024 |
| 3 | Paul | 86 | m | 2024 |
| 4 | Emilia | 85 | w | 2024 |
| ... | ... | ... | ... | ... |
| 50659 | Zora | 4 oder weniger | w | 2013 |
| 50660 | Zoran | 4 oder weniger | m | 2013 |
| 50661 | Zorica | 4 oder weniger | w | 2013 |
| 50662 | Zoubair | 4 oder weniger | m | 2013 |
| 50663 | Züleyha | 4 oder weniger | w | 2013 |
50664 rows × 4 columns
Erster Blick auf die Daten:
- Spaltenchaos beseitigt ✓
- Trennzeichen überlistet ✓
- Jahreszahl extrahiert ✓
Jetzt haben wir noch das Problem dass wir in der spalte anzahl Zahlen erwarten, aber manche Namen wurden "4 oder weniger" mal vergeben und kommen im String Format vor. Schön anonymisiert, aber halt für die Analyse ein bisschen nervig. Für uns heißt das: Wir setzen sie einfach auf 1. Sonst wird das nix mit dem Plotten.
df['anzahl'] = df['anzahl'].replace('4 oder weniger', 1)
df['anzahl'] = df['anzahl'].replace('4 oder wenniger', 1)
df['anzahl'] = pd.to_numeric(df['anzahl'])
df['anzahl'] = df['anzahl'].astype('Int64')
df
| vorname | anzahl | geschlecht | jahr | |
|---|---|---|---|---|
| 0 | Felix | 100 | m | 2024 |
| 1 | Maximilian | 94 | m | 2024 |
| 2 | Leon | 87 | m | 2024 |
| 3 | Paul | 86 | m | 2024 |
| 4 | Emilia | 85 | w | 2024 |
| ... | ... | ... | ... | ... |
| 50659 | Zora | 1 | w | 2013 |
| 50660 | Zoran | 1 | m | 2013 |
| 50661 | Zorica | 1 | w | 2013 |
| 50662 | Zoubair | 1 | m | 2013 |
| 50663 | Züleyha | 1 | w | 2013 |
50664 rows × 4 columns
Es gibt noch Leerzeilen ohne Wert. Interessiert uns nicht, also weg damit.
df[df['anzahl'].isna()]
| vorname | anzahl | geschlecht | jahr | |
|---|---|---|---|---|
| 25952 | <NA> | NaN | 2019 | |
| 30135 | <NA> | NaN | 2018 | |
| 34377 | <NA> | NaN | 2017 | |
| 38686 | <NA> | NaN | 2016 |
Es gibt, duppletten
# Finde doppelte Einträge
duplicates = df[df.duplicated(subset=['jahr', 'vorname', 'geschlecht'], keep=False)]
# Zeige die doppelten Einträge an
print(f"Anzahl der doppelten Einträge: {len(duplicates)}")
print("\nDoppelte Einträge:")
duplicates_sorted = duplicates.sort_values(['jahr', 'vorname', 'geschlecht'])
display(duplicates_sorted)
Anzahl der doppelten Einträge: 26 Doppelte Einträge:
| vorname | anzahl | geschlecht | jahr | |
|---|---|---|---|---|
| 19619 | Ilyas | 15 | m | 2020 |
| 20868 | Ilyas | 1 | m | 2020 |
| 18075 | Stefania | 1 | w | 2020 |
| 19358 | Stefania | 1 | w | 2020 |
| 21617 | Yagiz | 1 | m | 2020 |
| 21618 | Yagiz | 1 | m | 2020 |
| 21639 | Yigit | 1 | m | 2020 |
| 21640 | Yigit | 1 | m | 2020 |
| 20250 | Yilmaz | 1 | m | 2020 |
| 21643 | Yilmaz | 1 | m | 2020 |
| 13924 | Ayse | 1 | w | 2021 |
| 13925 | Ayse | 1 | w | 2021 |
| 16509 | Hizir | 1 | m | 2021 |
| 16510 | Hizir | 1 | m | 2021 |
| 15305 | Ilyas | 11 | m | 2021 |
| 16531 | Ilyas | 1 | m | 2021 |
| 13248 | Isra | 5 | w | 2021 |
| 14338 | Isra | 1 | w | 2021 |
| 15826 | Matej | 1 | m | 2021 |
| 16818 | Matej | 1 | m | 2021 |
| 14708 | Nara | 1 | w | 2021 |
| 14709 | Nara | 1 | w | 2021 |
| 14951 | Seyma | 1 | w | 2021 |
| 14952 | Seyma | 1 | w | 2021 |
| 15651 | Yigit | 1 | m | 2021 |
| 17309 | Yigit | 1 | m | 2021 |
Wir summieren sie
# 2. Summiere die Anzahlen für doppelte Einträge
df = df.groupby(['jahr', 'vorname', 'geschlecht'])['anzahl'].sum().reset_index()
import seaborn as sns
import matplotlib.pyplot as plt
geburten_pro_jahr_geschlecht = df.groupby(['jahr', 'geschlecht'])['anzahl'].sum().reset_index()
plt.figure(figsize=(10, 6))
sns.lineplot(data=geburten_pro_jahr_geschlecht, x='jahr', y='anzahl', hue='geschlecht', marker='o')
plt.title('Anzahl der Geburten pro Jahr nach Geschlecht')
plt.xlabel('Jahr')
plt.ylabel('Anzahl der Geburten')
plt.legend(title='Geschlecht')
plt.grid(True, linestyle='--')
sns.despine()
plt.tight_layout()
plt.show()
Erkenntnis: Es gibt leichte Schwankungen über die Jahre. Fun Fact: die Jungs liegen jedes Jahr vorne
Top 20 Namen – interaktiv und bunt¶
Jetzt wird’s fancy: Wähle ein Jahr, wir zeigen dir die beliebtesten Namen!
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.io as pio
pio.renderers.default='notebook'
# Dataframe für Jungen
df_boys = df[df['geschlecht'] == 'm'].pivot(
index='jahr',
columns='vorname',
values='anzahl'
).fillna(0)
# Dataframe für Mädchen
df_girls = df[df['geschlecht'] == 'w'].pivot(
index='jahr',
columns='vorname',
values='anzahl'
).fillna(0)
# Sortiere die Spalten nach der Gesamtsumme
df_boys = df_boys[df_boys.sum().sort_values(ascending=False).index]
df_girls = df_girls[df_girls.sum().sort_values(ascending=False).index]
# Erstelle die Figuren
fig = make_subplots(rows=1, cols=2,
subplot_titles=('Top 10 Jungennamen', 'Top 10 Mädchennamen'),
horizontal_spacing=0.15)
available_years = sorted(df['jahr'].unique())
# Erstelle die Frames für die Animation
frames = []
for year in available_years:
df_year = df[df['jahr'] == year]
top_m = df_year[df_year['geschlecht'] == 'm'].nlargest(10, 'anzahl').sort_values('anzahl')
top_w = df_year[df_year['geschlecht'] == 'w'].nlargest(10, 'anzahl').sort_values('anzahl')
frame = go.Frame(
data=[
go.Bar(x=top_m['anzahl'], y=top_m['vorname'], orientation='h', marker_color=secondary_colors[4]),
go.Bar(x=top_w['anzahl'], y=top_w['vorname'], orientation='h', marker_color=secondary_colors[2])
],
name=str(year)
)
frames.append(frame)
# Füge die Frames hinzu
fig.frames = frames
# Initiale Daten
initial_year = available_years[0]
df_initial = df[df['jahr'] == initial_year]
top_m_initial = df_initial[df_initial['geschlecht'] == 'm'].nlargest(10, 'anzahl').sort_values('anzahl')
top_w_initial = df_initial[df_initial['geschlecht'] == 'w'].nlargest(10, 'anzahl').sort_values('anzahl')
# Füge die initialen Daten hinzu
fig.add_trace(
go.Bar(x=top_m_initial['anzahl'], y=top_m_initial['vorname'], orientation='h', marker_color=secondary_colors[4]),
row=1, col=1
)
fig.add_trace(
go.Bar(x=top_w_initial['anzahl'], y=top_w_initial['vorname'],orientation='h', marker_color=secondary_colors[2]),
row=1, col=2
)
# Erstelle die Slider-Animation
sliders = [{
'transition': {'duration': 300, 'easing': 'cubic-in-out'},
'pad': {'b': 10, 't': 50},
'len': 0.9,
'x': 0.1,
'y': 0,
'steps': [{
'args': [[f.name], {
'frame': {'duration': 300, 'redraw': True},
'mode': 'immediate',
'transition': {'duration': 300}
}],
'label': str(year),
'method': 'animate'
} for f, year in zip(frames, available_years)]
}]
# Aktualisiere das Layout
fig.update_layout(
sliders=sliders,
height=600,
showlegend=False,
margin=dict(l=100, r=100, t=50, b=50),
updatemenus=[{
'type': 'buttons',
'x': 0.05,
'y': -0.17,
'showactive': False,
'buttons': [{
'label': 'Abspielen',
'method': 'animate',
'args': [None, {
'frame': {'duration': 1000, 'redraw': True},
'fromcurrent': True,
'transition': {'duration': 500}
}]
}]
}]
)
# Zeige die Figur an
fig.show()
Vergleich zweier Namen über die Zeit¶
Wir vergleichen Leon vs. Maximilian – das Duell der Klassiker vs. New Kid on the Block?
name1 = "Leon"
name2 = "Maximilian"
name1_data = df[df['vorname'] == name1]
name2_data = df[df['vorname'] == name2]
name1_by_year = name1_data.groupby('jahr')['anzahl'].sum().reset_index()
name2_by_year = name2_data.groupby('jahr')['anzahl'].sum().reset_index()
plt.plot(name1_by_year['jahr'], name1_by_year['anzahl'], marker='o', label=name1)
plt.plot(name2_by_year['jahr'], name2_by_year['anzahl'], marker='s', label=name2)
plt.title(f'Häufigkeit: {name1} vs. {name2} über die Jahre')
plt.xlabel('Jahr')
plt.ylabel('Anzahl')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
Leon holt auf und siegt! 2023 hat er Maximilian als Spitzenreiter abgelöst. Aber der Max hält sich tapfer – aber will man sein Kind wirklich nach einem 2005er Fußballstar benennen?
Auf- und Absteiger: Welche Namen sind gerade im Trend?¶
Jeder kennt sie: Die Mode-Namen, die plötzlich überall auftauchen (Hi „Levi“) – und solche, die irgendwann einfach verschwinden (rip „Kevin“?).
Wir messen die Trendstärke anhand der Differenz der Nennungen im Vergleich zum Vorjahr.
# Wir gruppieren nach Vorname und Jahr, summieren und sortieren
trend_data = df.groupby(['vorname', 'jahr'])['anzahl'].sum().reset_index()
# Umwandeln zu Zeitreihe für jeden Namen
trend_pivot = trend_data.pivot(index='jahr', columns='vorname', values='anzahl').fillna(0)
# Differenz zum Vorjahr berechnen
trend_diff = trend_pivot.diff().fillna(0)
# Mittelwert der letzten 3 Jahre vergleichen mit den vorherigen 3
trend_score = (trend_pivot.tail(3).mean() - trend_pivot.iloc[-6:-3].mean()).sort_values(ascending=False)
# Erstelle zwei Subplots nebeneinander
plt.figure(figsize=(15, 6))
# Subplot für die häufigsten Jungennamen
plt.subplot(1, 2, 1)
sns.barplot(x=trend_score.head(10).index, y=trend_score.head(10).values)
plt.title('Top 10 Aufsteiger der letzten Jahre')
plt.ylim(-trend_score.abs().max(), trend_score.abs().max()) # Setze y-Achsengrenzen
plt.ylabel('Trend Score')
plt.xlabel('')
# Subplot für die häufigsten Mädchennamen
plt.subplot(1, 2, 2)
sns.barplot(x=trend_score.tail(10).index, y=trend_score.tail(10).values)
plt.ylim(-trend_score.abs().max(), trend_score.abs().max())
plt.title('Top 10 Absteiger der letzten Jahre')
plt.xlabel('')
ax = plt.gca()
ax.set_yticklabels([])
plt.tight_layout()
plt.show()
Unisex oder eindeutig? Analyse nach Geschlechterverteilung¶
Wie viele Namen werden für beide Geschlechter verwendet – und welche?
# Gruppieren nach Vorname und Geschlecht und Summieren
geschlecht_counts = df.groupby(['vorname', 'geschlecht'])['anzahl'].sum().unstack(fill_value=0)
# Umbenennen der Spalten
geschlecht_counts.columns = ['Anzahl (männlich)', 'Anzahl (weiblich)'] if 'm' in geschlecht_counts.columns and 'w' in geschlecht_counts.columns else geschlecht_counts.columns
# Gesamtsumme berechnen
geschlecht_counts['Anzahl (gesamt)'] = geschlecht_counts['Anzahl (männlich)'] + geschlecht_counts['Anzahl (weiblich)']
sns.scatterplot(data=geschlecht_counts, x="Anzahl (männlich)", y="Anzahl (weiblich)")
plt.show()