Ein frei kopier- und anpassbares Lehrmittel von eduskript.org

Zahlen zur Zuwanderung visualisiert

Für die Initiative "Gegen eine 10 Millionen Schweiz" wollte ich eine Übersicht der Zuwanderungssituation in der Schweiz. Ihr Computer wird hier die Zahlen direkt aus der Datenbank des Bundesamtes für Statistik (BFS) abrufen und plotten.1 Sie können die Plots zoomen und herumschieben. Links können Sie den Python-Code aufklappen, falls Sie das interessiert.

PythonLoading editor…
import json
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from pyodide.http import pyfetch

# ============================================================
# BFS PxWeb API — Daten direkt abfragen (kein Asset-Upload nötig)
# ============================================================
API = "https://www.pxweb.bfs.admin.ch/api/v1/de/px-x-0103010000_151/px-x-0103010000_151.px"

async def pxweb_query(components, nationalities):
    query = {
        "query": [
            {"code": "Jahr", "selection": {"filter": "all", "values": ["*"]}},
            {"code": "Staatsangehörigkeit", "selection": {"filter": "item", "values": nationalities}},
            {"code": "Geschlecht", "selection": {"filter": "item", "values": ["0"]}},
            {"code": "Demografische Komponente", "selection": {"filter": "item", "values": components}},
        ],
        "response": {"format": "json-stat2"},
    }
    resp = await pyfetch(API, method="POST", headers={"Content-Type": "application/json"}, body=json.dumps(query))
    return await resp.json()

def jsonstat_to_long(d):
    """JSON-stat2-Antwort in ein Long-Format-DataFrame umwandeln (wie es pyaxis.parse liefern würde)."""
    dims = d["id"]
    sizes = d["size"]
    labels = [d["dimension"][dim]["category"]["label"] for dim in dims]
    codes = [list(d["dimension"][dim]["category"]["index"].keys()) for dim in dims]
    vals = [np.nan if v is None else v for v in d["value"]]
    arr = np.array(vals, dtype=float).reshape(sizes)
    rows = []
    for idx in np.ndindex(*sizes):
        val = arr[idx]
        if np.isnan(val):
            continue
        row = {dims[k]: labels[k][codes[k][idx[k]]] for k in range(len(dims))}
        row["DATA"] = val
        rows.append(row)
    return pd.DataFrame(rows)

# Verfügbare Staatsangehörigkeits-Codes aus den Metadaten holen (statt fest zu verdrahten)
meta_resp = await pyfetch(API)
meta = await meta_resp.json()
nat_var = next(v for v in meta["variables"] if v["code"] == "Staatsangehörigkeit")
other_nat_codes = [c for c in nat_var["values"] if c not in ("0", "8100")]

# Query 1: Total + Schweiz, die 6 Komponenten, die wir für die Übersichts-Plots brauchen
# 14=Bestand 31.12, 3=Geburtenüberschuss, 6=Wanderungssaldo, 10=Erwerb Bürgerrecht,
# 13=Bestandesbereinigung, 15=Veränderung
main_json = await pxweb_query(["14", "3", "6", "10", "13", "15"], ["0", "8100"])

# Query 2: alle übrigen Staatsangehörigkeiten, nur Bestand 31.12 (für die Herkunfts-Aufschlüsselung)
nat_json = await pxweb_query(["14"], other_nat_codes)

df = pd.concat([jsonstat_to_long(main_json), jsonstat_to_long(nat_json)], ignore_index=True)
df["DATA"] = pd.to_numeric(df["DATA"], errors="coerce")

# ============================================================
# Filter: Total gender, key nationalities (Total, Schweiz)
# ============================================================
mask_gender = df["Geschlecht"] == "Geschlecht - Total"

components = [
    "Bestand am 31. Dezember",
    "Geburtenüberschuss",
    "Wanderungssaldo inkl. Änderung des Bevölkerungstyps",
    "Erwerb des Schweizer Bürgerrechts",
    "Veränderung",
]

nationalities = ["Staatsangehörigkeit - Total", "Schweiz"]

filtered = df[mask_gender &
              df["Staatsangehörigkeit"].isin(nationalities) &
              df["Demografische Komponente"].isin(components)].copy()

# Pivot to wide format
pivot = filtered.pivot_table(
    index="Jahr",
    columns=["Staatsangehörigkeit", "Demografische Komponente"],
    values="DATA"
).reset_index()

# Flatten column names
pivot.columns = ["_".join(col).strip("_") if col[1] else col[0] for col in pivot.columns]
pivot["Jahr"] = pivot["Jahr"].astype(int)
pivot = pivot.sort_values("Jahr")

# Extract time series
years = pivot["Jahr"].values

# Total population (end of year)
total_pop = pivot["Staatsangehörigkeit - Total_Bestand am 31. Dezember"].values
swiss_pop = pivot["Schweiz_Bestand am 31. Dezember"].values
foreign_pop = total_pop - swiss_pop

# Einbürgerungen: auf Gesamtebene = 0 (kein Effekt auf Total),
# daher nehmen wir "Schweiz" -> "Erwerb des Schweizer Bürgerrechts" = Zugang zu Schweizer Bev.
einbuergerungen = pivot["Schweiz_Erwerb des Schweizer Bürgerrechts"].values

# Geburtenüberschuss Total
geburtenueberschuss_total = pivot["Staatsangehörigkeit - Total_Geburtenüberschuss"].values

# Veränderung foreign pop
veraenderung_foreign = np.diff(foreign_pop, prepend=np.nan)
veraenderung_foreign[0] = np.nan  # kein Vorjahr

# Ausländeranteil
anteil = foreign_pop / total_pop * 100

# ============================================================
# NATIONALITY BREAKDOWN — grouped: Top 3 + Rest EU/EFTA + Rest Europa + Rest Welt
# ============================================================
mask_nat = (df["Geschlecht"] == "Geschlecht - Total") & \
           (df["Demografische Komponente"] == "Bestand am 31. Dezember") & \
           (~df["Staatsangehörigkeit"].isin(["Staatsangehörigkeit - Total", "Schweiz"]))
nat_data = df[mask_nat][["Jahr", "Staatsangehörigkeit", "DATA"]].copy()
nat_data["Jahr"] = nat_data["Jahr"].astype(int)

eu_efta = {
    "Belgien", "Bulgarien", "Dänemark", "Deutschland", "Estland", "Finnland",
    "Frankreich", "Griechenland", "Irland", "Italien", "Kroatien", "Lettland",
    "Litauen", "Luxemburg", "Malta", "Niederlande", "Österreich", "Polen",
    "Portugal", "Rumänien", "Schweden", "Slowakei", "Slowenien", "Spanien",
    "Tschechien", "Ungarn", "Zypern",
    "Island", "Liechtenstein", "Norwegen",
}
europa_rest = {
    "Albanien", "Andorra", "Bosnien und Herzegowina", "Vereinigtes Königreich",
    "Kosovo", "Moldau", "Monaco", "Montenegro", "Nordmazedonien", "Russland",
    "San Marino", "Serbien", "Serbien und Montenegro", "Türkiye", "Ukraine",
    "Vatikanstadt", "Belarus", "Georgien", "Armenien", "Aserbaidschan",
    "Jugoslawien", "Sowjetunion", "Tschechoslowakei",
}
top3 = {"Deutschland", "Kosovo", "Portugal"}

def classify(nat):
    if nat in top3:
        return nat
    elif nat in eu_efta:
        return "Übrige EU/EFTA"
    elif nat in europa_rest:
        return "Übriges Europa"
    else:
        return "Rest der Welt"

nat_data["Gruppe"] = nat_data["Staatsangehörigkeit"].apply(classify)
grouped = nat_data.groupby(["Jahr", "Gruppe"])["DATA"].sum().unstack(fill_value=0).sort_index()
nat_changes_grouped = grouped.diff()
nat_stacked = nat_changes_grouped.dropna(how="all")  # erstes Jahr hat keinen Vorjahreswert

stack_order = ["Deutschland", "Portugal", "Kosovo", "Übrige EU/EFTA", "Übriges Europa", "Rest der Welt"]

# ============================================================
# SWISS POPULATION COMPONENTS (für Plot 3)
# ============================================================
mask_ch = (df["Geschlecht"] == "Geschlecht - Total") & \
          (df["Staatsangehörigkeit"] == "Schweiz") & \
          (df["Demografische Komponente"].isin([
              "Geburtenüberschuss",
              "Wanderungssaldo inkl. Änderung des Bevölkerungstyps",
              "Erwerb des Schweizer Bürgerrechts",
              "Bestandesbereinigung",
              "Veränderung",
          ]))
ch_data = df[mask_ch].pivot_table(index="Jahr", columns="Demografische Komponente", values="DATA")
ch_data.index = ch_data.index.astype(int)
ch_geburt = ch_data["Geburtenüberschuss"].values
ch_wanderung = ch_data["Wanderungssaldo inkl. Änderung des Bevölkerungstyps"].values
ch_einbuergerung = ch_data["Erwerb des Schweizer Bürgerrechts"].values
ch_bereinigung = ch_data["Bestandesbereinigung"].values
ch_veraenderung = ch_data["Veränderung"].values

# ============================================================
# HISTORISCHE ZEITREIHE — Zusammenfassung
# ============================================================
print("=" * 70)
print("BFS DEMOGRAFISCHE BILANZ — ZEITREIHE 1981-2024")
print("=" * 70)
print(f"{'Jahr':>6} {'Total':>12} {'Schweizer':>12} {'Ausländer':>12} {'Anteil%':>8} {'Δ Ausl.':>10} {'Einbürg.':>10}")
print("-" * 70)
for i, y in enumerate(years):
    delta = f"{veraenderung_foreign[i]:>10,.0f}" if not np.isnan(veraenderung_foreign[i]) else "        --"
    print(f"{y:>6} {total_pop[i]:>12,.0f} {swiss_pop[i]:>12,.0f} {foreign_pop[i]:>12,.0f} {anteil[i]:>7.1f}% {delta} {einbuergerungen[i]:>10,.0f}")

# ============================================================
# DURCHSCHNITTSWERTE (informativ, nicht mehr für die Projektion genutzt —
# siehe unten die offiziellen BFS-Szenarien)
# ============================================================
mask_10y = (years >= 2015) & (years <= 2024)
idx_10y = np.where(mask_10y)[0]

total_changes_10y = [total_pop[i] - total_pop[i - 1] for i in idx_10y if i > 0]
avg_total_change_10y = np.mean(total_changes_10y)
foreign_changes_10y = [foreign_pop[i] - foreign_pop[i - 1] for i in idx_10y if i > 0]
avg_foreign_change_10y = np.mean(foreign_changes_10y)
avg_einbuergerungen_10y = np.mean(einbuergerungen[idx_10y])

# FZA-era averages (2002-2024)
# Korrektur: 2023 enthält ~50'600 Statuswechsel von S-Status (Ukraine) — kein
# echter Zuzug in 2023, sondern eine statistische Umbuchung. Wird aus dem
# Durchschnitt herausgerechnet.
ukraine_s_effect_2023 = 50600

mask_fza = (years >= 2002) & (years <= 2024)
idx_fza = np.where(mask_fza)[0]

total_changes_fza = []
foreign_changes_fza = []
for i in idx_fza:
    if i == 0:
        continue
    fc = foreign_pop[i] - foreign_pop[i - 1]
    tc = total_pop[i] - total_pop[i - 1]
    if years[i] == 2023:
        fc -= ukraine_s_effect_2023
        tc -= ukraine_s_effect_2023
    total_changes_fza.append(tc)
    foreign_changes_fza.append(fc)

avg_total_change_fza = np.mean(total_changes_fza)
avg_foreign_change_fza = np.mean(foreign_changes_fza)
avg_einbuergerungen_fza = np.mean(einbuergerungen[idx_fza])

print(f"\n{'=' * 70}")
print("DURCHSCHNITTSWERTE (informativ)")
print(f"{'=' * 70}")
print("10-Jahres-Durchschnitt (2015-2024):")
print(f"  Veränderung Gesamtbev.:      {avg_total_change_10y:>10,.0f} / Jahr")
print(f"  Veränderung Ausländer:       {avg_foreign_change_10y:>10,.0f} / Jahr")
print(f"  Einbürgerungen:              {avg_einbuergerungen_10y:>10,.0f} / Jahr")
print("\nFZA-Ära Durchschnitt (2002-2024), bereinigt um Ukraine-S-Effekt 2023:")
print(f"  Veränderung Gesamtbev.:      {avg_total_change_fza:>10,.0f} / Jahr")
print(f"  Veränderung Ausländer:       {avg_foreign_change_fza:>10,.0f} / Jahr")
print(f"  Einbürgerungen:              {avg_einbuergerungen_fza:>10,.0f} / Jahr")

# ============================================================
# BFS-BEVÖLKERUNGSSZENARIEN 2024–2075 (px-x-0104000000_102)
# Referenzszenario A-00-2025, "hohes" Szenario B-00-2025, "tiefes" Szenario C-00-2025
# ============================================================
API_SCEN = "https://www.pxweb.bfs.admin.ch/api/v1/de/px-x-0104000000_102/px-x-0104000000_102.px"

async def pxweb_query_scenarios(szenarien, nationalities, komponenten):
    query = {
        "query": [
            {"code": "Szenario-Variante", "selection": {"filter": "item", "values": szenarien}},
            {"code": "Staatsangehörigkeit (Kategorie)", "selection": {"filter": "item", "values": nationalities}},
            {"code": "Geschlecht", "selection": {"filter": "item", "values": ["0"]}},
            {"code": "Alter", "selection": {"filter": "item", "values": ["0"]}},
            {"code": "Jahr", "selection": {"filter": "all", "values": ["*"]}},
            {"code": "Beobachtungseinheit", "selection": {"filter": "item", "values": komponenten}},
        ],
        "response": {"format": "json-stat2"},
    }
    resp = await pyfetch(API_SCEN, method="POST", headers={"Content-Type": "application/json"}, body=json.dumps(query))
    return await resp.json()

# 0=Referenz A-00-2025, 1=hoch B-00-2025, 2=tief C-00-2025; 0=Total, 1=Schweiz; 6=Bestand 31.12
scen_json = await pxweb_query_scenarios(["0", "1", "2"], ["0", "1"], ["6"])
df_scen = jsonstat_to_long(scen_json)
df_scen["Jahr"] = df_scen["Jahr"].astype(int)

scen_pivot = df_scen.pivot_table(
    index="Jahr", columns=["Szenario-Variante", "Staatsangehörigkeit (Kategorie)"], values="DATA"
).sort_index()
scen_years = scen_pivot.index.values

scenarios = {
    "Referenz (A-00-2025)": ("Referenzszenario A-00-2025", "#2c3e50", "-"),
    "hoch (B-00-2025)": ("'hohes' Szenario B-00-2025", "#27ae60", "--"),
    "tief (C-00-2025)": ("'tiefes' Szenario C-00-2025", "#e74c3c", "--"),
}

scen_total = {name: scen_pivot[(col, "Staatsangehörigkeit - Total")].values for name, (col, _, _) in scenarios.items()}
scen_swiss = {name: scen_pivot[(col, "Schweiz")].values for name, (col, _, _) in scenarios.items()}
scen_foreign = {name: scen_total[name] - scen_swiss[name] for name in scenarios}
scen_anteil = {name: scen_foreign[name] / scen_total[name] * 100 for name in scenarios}

# ============================================================
# PLOTTING
# ============================================================
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
fig.suptitle("Schweiz: Bevölkerungsentwicklung nach den drei BFS-Hauptszenarien (2024–2075)\n"
             "Quelle: BFS – Demografische Bilanz nach Staatsangehörigkeit, 1981–2024, plus Bevölkerungsszenarien",
             fontsize=13, fontweight="bold", y=0.98)

c_total = "#2c3e50"
c_foreign = "#e74c3c"
c_swiss = "#3498db"
c_anteil = "#e67e22"
c_einb = "#27ae60"
c_bereinigung = "#95a5a6"

# --- Plot 1: Bevölkerung ---
ax1 = axes[0, 0]
ax1.plot(years, total_pop / 1e6, "o-", color=c_total, linewidth=2, markersize=2.5, label="Gesamtbevölkerung")
ax1.plot(years, foreign_pop / 1e6, "s-", color=c_foreign, linewidth=2, markersize=2.5, label="Ausländische Bev.")
ax1.plot(years, swiss_pop / 1e6, "^-", color=c_swiss, linewidth=2, markersize=2.5, label="Schweizer Bev.")

for name, (col, color, ls) in scenarios.items():
    ax1.plot(scen_years, scen_total[name] / 1e6, ls, color=color, linewidth=1.6, alpha=0.85, label=f"Total, {name}")

ax1.axvline(x=2024.5, color="gray", linestyle=":", alpha=0.4)
ax1.axvline(x=2002, color="black", linestyle=":", alpha=0.3)
ax1.text(2002.5, 5.2, "FZA", fontsize=7, color="black", alpha=0.6)

ax1.axhline(y=10, color="gray", linestyle=":", alpha=0.2)
ref_total = scen_total["Referenz (A-00-2025)"]
for i, v in enumerate(ref_total):
    if v / 1e6 >= 10:
        ax1.annotate(f"10 Mio. ~{scen_years[i]} (Referenz)", xy=(scen_years[i], 10), fontsize=8, color=c_total, ha="center", va="bottom")
        break

ax1.set_title("Bevölkerungsentwicklung — BFS-Szenarien (Mio.)", fontweight="bold")
ax1.set_ylabel("Millionen")
ax1.legend(fontsize=6.5, loc="upper left")
ax1.grid(True, alpha=0.3)

# --- Plot 2: Ausländeranteil ---
ax2 = axes[0, 1]
ax2.plot(years, anteil, "o-", color=c_anteil, linewidth=2, markersize=2.5, label="Historisch")

for name, (col, color, ls) in scenarios.items():
    ax2.plot(scen_years, scen_anteil[name], ls, color=color, linewidth=1.6, alpha=0.85, label=name)

ax2.axvline(x=2024.5, color="gray", linestyle=":", alpha=0.4)
ax2.axvline(x=2002, color="black", linestyle=":", alpha=0.3)
ax2.text(2002.5, 14.8, "FZA", fontsize=7, color="black", alpha=0.6)

ax2.annotate(f"{anteil[-1]:.1f}% (2024)", xy=(2024, anteil[-1]), fontsize=9, color=c_anteil, fontweight="bold", ha="right")

ax2.set_title("Ausländeranteil — BFS-Szenarien (%)", fontweight="bold")
ax2.set_ylabel("Prozent")
ax2.legend(fontsize=7, loc="upper left")
ax2.grid(True, alpha=0.3)

# --- Plot 3: Schweizer Bevölkerung — Komponenten der Veränderung ---
# Echtes kumulatives Stacking (positive Werte aufeinander, negative aufeinander) —
# ein reines Balken-Overlay hätte in Jahren mit mehreren positiven Komponenten
# (z.B. 1980er: Geburtenüberschuss UND Einbürgerungen beide positiv) die Balken
# einfach übereinandergelegt statt addiert, sodass die sichtbare Balkenhöhe unter
# der echten Summe (= Netto-Veränderung) lag. Bestandesbereinigung (BFS-eigene
# Registerkorrektur, kein reales demografisches Ereignis) als vierte Komponente
# ergänzt, damit die Linie exakt auf dem Balkenstapel landet.
ax3 = axes[1, 0]
bar_width = 0.8

ch_bottom_pos = np.zeros(len(years))
ch_bottom_neg = np.zeros(len(years))
for label, vals, color, alpha in [
    ("Einbürgerungen", ch_einbuergerung, c_einb, 0.75),
    ("Natürliches Wachstum (Geburten − Todesfälle)", ch_geburt, "#e74c3c", 0.65),
    ("Wanderungssaldo Schweizer Staatsangehörige", ch_wanderung, "#9b59b6", 0.55),
    ("Bestandesbereinigung", ch_bereinigung, c_bereinigung, 0.6),
]:
    pos_vals = np.where(vals > 0, vals, 0)
    neg_vals = np.where(vals < 0, vals, 0)
    ax3.bar(years, pos_vals, bottom=ch_bottom_pos, width=bar_width, color=color, alpha=alpha, label=label)
    if np.any(neg_vals < 0):
        ax3.bar(years, neg_vals, bottom=ch_bottom_neg, width=bar_width, color=color, alpha=alpha)
    ch_bottom_pos += pos_vals
    ch_bottom_neg += neg_vals

ax3.plot(years, ch_veraenderung, "o-", color="black", linewidth=1.8, markersize=2.5,
         label="Netto-Veränderung", zorder=5)

ax3.axhline(y=0, color="black", linewidth=0.8)
ax3.axvline(x=2002, color="black", linestyle=":", alpha=0.3)
ax3.text(2003, max(ch_einbuergerung) * 0.95, "FZA", fontsize=7, color="black", alpha=0.6)
ax3.set_title("Veränderung Schweizer Staatsangehörige in der Schweiz", fontweight="bold")
ax3.set_ylabel("Personen")
ax3.legend(fontsize=6, loc="upper left")
ax3.grid(True, alpha=0.3, axis="y")
ax3.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: f"{x:,.0f}"))

# --- Plot 4: Jährliches Wachstum ausl. Bev. nach Herkunft (gestapelt) ---
ax4 = axes[1, 1]

group_colors = {
    "Deutschland": "#FFCC00",
    "Portugal": "#006600",
    "Kosovo": "#244AA5",
    "Übrige EU/EFTA": "#4682B4",
    "Übriges Europa": "#B8860B",
    "Rest der Welt": "#8B4513",
}

plot_years_nat = nat_stacked.index.values
bottom_pos = np.zeros(len(plot_years_nat))
bottom_neg = np.zeros(len(plot_years_nat))

for col in stack_order:
    if col not in nat_stacked.columns:
        continue
    vals = nat_stacked[col].values
    pos_vals = np.where(vals > 0, vals, 0)
    neg_vals = np.where(vals < 0, vals, 0)
    color = group_colors.get(col, "#999999")
    ax4.bar(plot_years_nat, pos_vals, bottom=bottom_pos, width=0.8, color=color, alpha=0.85, label=col)
    if np.any(neg_vals < 0):
        ax4.bar(plot_years_nat, neg_vals, bottom=bottom_neg, width=0.8, color=color, alpha=0.85)
    bottom_pos += pos_vals
    bottom_neg += neg_vals

ax4.plot(years[1:], veraenderung_foreign[1:], "o-", color="black", linewidth=1.8, markersize=2.5,
         label="Netto-Veränderung", zorder=5)

idx_2023_nat = np.where(plot_years_nat == 2023)[0][0]
total_2023 = bottom_pos[idx_2023_nat]
ax4.annotate(
    f"2023: ~{ukraine_s_effect_2023 // 1000}k Statuswechsel\nUkraine S → ständige Bev.",
    xy=(2023, total_2023),
    xytext=(2015, 105000),
    fontsize=6,
    arrowprops=dict(arrowstyle="->", color="#f39c12", lw=1.2),
    bbox=dict(boxstyle="round,pad=0.3", facecolor="#fef9e7", edgecolor="#f39c12", alpha=0.9),
    ha="left", va="top"
)

# 2010: Kosovo taucht erstmals als eigener BFS-Code auf (+63'465), vorher unter
# "Übriges Europa" (Serbien/Jugoslawien-Sammelkategorie) mitgezählt — der fast
# gleich grosse Rückgang dort (-63'583) ist eine Umbuchung, keine Abwanderung.
idx_2010_nat = np.where(plot_years_nat == 2010)[0][0]
neg_2010 = bottom_neg[idx_2010_nat]
ax4.annotate(
    "2010: Kosovo erstmals eigener BFS-Code\n(vorher unter Übriges Europa)",
    xy=(2010, neg_2010),
    xytext=(1993, -55000),
    fontsize=6,
    arrowprops=dict(arrowstyle="->", color="#f39c12", lw=1.2),
    bbox=dict(boxstyle="round,pad=0.3", facecolor="#fef9e7", edgecolor="#f39c12", alpha=0.9),
    ha="left", va="top"
)

ax4.axvline(x=2002, color="black", linestyle=":", alpha=0.3)
ax4.text(2002.5, 120000, "FZA", fontsize=7, color="black", alpha=0.6)
ax4.axhline(y=0, color="black", linewidth=0.5)
ax4.set_title("Jährliches Wachstum ausländische Bevölkerung nach Herkunft", fontweight="bold")
ax4.set_ylabel("Personen")
ax4.legend(fontsize=6.5, loc="upper left", ncol=1)
ax4.grid(True, alpha=0.3, axis="y")
ax4.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: f"{x:,.0f}"))

# Untere Panels (ax3, ax4) auf gemeinsame y-Achse skalieren, damit Balkenhöhen
# direkt vergleichbar sind (gleiche Personenzahl = gleiche visuelle Höhe).
shared_ymin = min(ch_bottom_neg.min(), bottom_neg.min())
shared_ymax = max(ch_bottom_pos.max(), bottom_pos.max())
shared_margin = (shared_ymax - shared_ymin) * 0.08
shared_ylim = (shared_ymin - shared_margin, shared_ymax + shared_margin)
ax3.set_ylim(shared_ylim)
ax4.set_ylim(shared_ylim)

plt.tight_layout(rect=[0, 0.02, 1, 0.94])
fig.text(0.5, 0.005,
         "Datenquelle: BFS – Demografische Bilanz nach Staatsangehörigkeit (ESPOP 1981–2010, STATPOP ab 2011) + "
         "Bevölkerungsszenarien 2025–2055 (Referenz/hoch/tief A/B/C-00-2025), live via PxWeb-API",
         ha="center", fontsize=7, color="gray")

plt.show()

# ============================================================
# BFS-SZENARIEN — ERGEBNISSE
# ============================================================
print(f"\n{'=' * 70}")
print("BFS-SZENARIEN — GESAMTBEVÖLKERUNG")
print(f"{'=' * 70}")
print("Basis 2024 (BFS definitiv):")
print(f"  Gesamtbevölkerung:   {total_pop[-1]:>12,.0f}")
print(f"  Ausländer:           {foreign_pop[-1]:>12,.0f}")
print(f"  Schweizer:           {swiss_pop[-1]:>12,.0f}")
print(f"  Ausländeranteil:     {anteil[-1]:>11.1f}%")

idx_2050 = np.where(scen_years == 2050)[0][0]
idx_end = len(scen_years) - 1
for name in scenarios:
    print(f"\n{name}:")
    print(f"  2050:                {scen_total[name][idx_2050]:>12,.0f}  ({scen_anteil[name][idx_2050]:.1f}% ausländisch)")
    print(f"  {scen_years[idx_end]}:                {scen_total[name][idx_end]:>12,.0f}  ({scen_anteil[name][idx_end]:.1f}% ausländisch)")

Was zeigen die Daten, was bringt die Zukunft?

An der Zeitreihe 1981–2024 fällt mir vor allem auf:

  • Die Gesamtbevölkerung in der Schweiz wächst aufgrund der Immigration.
  • Die Anzahl Schweizer Staatsangehöriger wächst aufgrund der Einbürgerungen.

Für die Initiative interessiert und auch die Zukunft. Deshalb zeigen die Plots drei BFS-Projektionen bis 2075: das Referenzszenario sowie die Varianten "hoch" und "tief". Für diese drei Varianten trifft das BFS verschiedene Grundannahmen.

Achtung: Das BFS meint beim Wanderungssaldo (links) den gesamten Netto-Wanderungssaldo (Einwanderung - Auswanderung). Darin versteckt sich unter anderem die Annahme, wie viele Schweizer auswandern. Die genauen Annahmen sind im Bericht zum Thema (rechts) aufgeführt.2

Zwei wichtige Punkte
  • Das BFS geht in allen drei Szenarien von einer durchgehenden Netto-Immigration von mindestens 37'500 Personen pro Jahr bis 2055. Im "tiefen" Szenario schrumpft die Schweizer Bevölkerung, wegen einer speziell tiefen Geburtenrate und weil Schweizer Staatsangehörige auswandern.

  • Der Wanderungssaldo ist eine Nettogrösse davon, wie viele Menschen ein- und auswandern. Im Referenzszenario stehen den 45'000–60'000 Personen Nettozuwanderung jährlich rund 155'000–180'000 Einwanderungen und 90'000–120'000 Auswanderungen gegenüber – über 250'000 Grenzübertritte pro Jahr, von denen sich die meisten gegenseitig aufheben. Die Bevölkerung wird in diesem Szenario fünf- bis sechsmal stärker umgewälzt, als der Saldo zeigt.

Annahmen für die Zukunft

Wieso geht das BFS davon aus, dass die Immigration ab 2030 nachlassen soll? Das entsteht durch eine Kombination aus Trendfortschreibung und qualitativen Annahmen zu fünf Faktoren:

  • Demografische Alterung in Europa: der eigentliche Treiber für den Rückgang ab 2030 – wenn die Babyboomer EU-weit in Rente gehen, steigt die Konkurrenz um Fachkräfte, und die Schweiz kann nicht mehr im gleichen Ausmass aus dem EWR rekrutieren.
  • Attraktivität der Schweiz: Lebensstandard, zentrale Lage, Steuern – in allen drei Szenarien als konstant hoch angenommen.
  • Wirtschaftliche Situation: Nachfrage nach Arbeitskräften, die der Binnenmarkt allein nicht decken kann.
  • Geopolitische Lage: beeinflusst Asylgesuche, wird aber nicht quantitativ modelliert.
  • Migrationspolitik: der politische Wille kann Zuwanderung fördern oder bremsen.

Das BFS bezeichnet seine Szenarien ausdrücklich nicht als Prognose, sondern als plausible Entwicklung – die nur eintritt, wenn die zugrunde gelegten Annahmen zutreffen.

Überalterung und ein Szenario ohne Migration

Zum Vergleich interessant: Das BFS rechnet auch eine reine Rechenübung ganz ohne Ein- und Auswanderung vor ("Variante A-08-2025").1 Ohne jede Migration würde die Schweizer Bevölkerung bis 2055 von 8,99 auf 8,06 Millionen schrumpfen (−10%), statt wie im Referenzszenario auf 10,47 Millionen zu wachsen.

PythonLoading editor…
import json
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pyodide.http import pyfetch

API_SCEN = "https://www.pxweb.bfs.admin.ch/api/v1/de/px-x-0104000000_102/px-x-0104000000_102.px"

async def pxweb_query_scenarios(szenarien, nationalities, komponenten):
    query = {
        "query": [
            {"code": "Szenario-Variante", "selection": {"filter": "item", "values": szenarien}},
            {"code": "Staatsangehörigkeit (Kategorie)", "selection": {"filter": "item", "values": nationalities}},
            {"code": "Geschlecht", "selection": {"filter": "item", "values": ["0"]}},
            {"code": "Alter", "selection": {"filter": "item", "values": ["0"]}},
            {"code": "Jahr", "selection": {"filter": "all", "values": ["*"]}},
            {"code": "Beobachtungseinheit", "selection": {"filter": "item", "values": komponenten}},
        ],
        "response": {"format": "json-stat2"},
    }
    resp = await pyfetch(API_SCEN, method="POST", headers={"Content-Type": "application/json"}, body=json.dumps(query))
    return await resp.json()

def jsonstat_to_long(d):
    dims = d["id"]
    sizes = d["size"]
    labels = [d["dimension"][dim]["category"]["label"] for dim in dims]
    codes = [list(d["dimension"][dim]["category"]["index"].keys()) for dim in dims]
    vals = [np.nan if v is None else v for v in d["value"]]
    arr = np.array(vals, dtype=float).reshape(sizes)
    rows = []
    for idx in np.ndindex(*sizes):
        val = arr[idx]
        if np.isnan(val):
            continue
        row = {dims[k]: labels[k][codes[k][idx[k]]] for k in range(len(dims))}
        row["DATA"] = val
        rows.append(row)
    return pd.DataFrame(rows)

# 0=Referenz, 1=hoch, 2=tief, 12=Variante A-08-2025 "keine Ein- und Auswanderungen"
scenarios = {
    "Referenz (A-00-2025)": ("Referenzszenario A-00-2025", "#2c3e50", "-"),
    "hoch (B-00-2025)": ("'hohes' Szenario B-00-2025", "#27ae60", "--"),
    "tief (C-00-2025)": ("'tiefes' Szenario C-00-2025", "#e74c3c", "--"),
    "keine Migration (A-08-2025)": ("Variante A-08-2025 'keine Ein- und Auswanderungen'", "#7f7f7f", ":"),
}
codes = ["0", "1", "2", "12"]

pop_json = await pxweb_query_scenarios(codes, ["0"], ["6"])
df_pop = jsonstat_to_long(pop_json)
df_pop["Jahr"] = df_pop["Jahr"].astype(int)
df_pop = df_pop[df_pop["Jahr"] <= 2055]
pop_pivot = df_pop.pivot_table(index="Jahr", columns="Szenario-Variante", values="DATA").sort_index()
years = pop_pivot.index.values

fig, ax = plt.subplots(figsize=(8, 5.5))
for name, (col, color, ls) in scenarios.items():
    ax.plot(years, pop_pivot[col].values / 1e6, ls, color=color, linewidth=2, label=name)
ax.set_title("Was wäre ohne Migration? Bevölkerungsentwicklung (Mio.)", fontweight="bold")
ax.set_ylabel("Millionen")
ax.legend(fontsize=8, loc="upper left")
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print(f"2055 Bevölkerung ohne Migration: {pop_pivot[scenarios['keine Migration (A-08-2025)'][0]].iloc[-1] / 1e6:.2f} Mio.")
print(f"2055 Bevölkerung Referenzszenario: {pop_pivot[scenarios['Referenz (A-00-2025)'][0]].iloc[-1] / 1e6:.2f} Mio.")

Wie stark sich das auf die Überalterung auswirkt, zeigt der Altersquotient (Personen im Rentenalter pro 100 Personen zwischen 20 und dem Rentenalter). Heute (2024, letzte effektive BFS-Zahl, nicht projiziert) liegt er bei 32,4. Mit denselben BFS-Daten lässt sich das bis 2055 für alle vier Szenarien durchrechnen – auch für Rentenalter 70 statt 65, als zweite Stellschraube neben der Migration:

Altersquotient (Rentenalter 65)Altersquotient (Rentenalter 70)
Heute (2024)32,421,7
Referenz (2055)44,931,3
Hoch (2055)44,831,8
Tief (2055)45,230,9
Keine Migration (2055)71,348,6

Fussnoten

  1. BFS, "Demografische Bilanz nach Staatsangehörigkeit" (px-x-0103010000_151) und "Szenarien zur Bevölkerungsentwicklung der Schweiz" (px-x-0104000000_102), https://www.pxweb.bfs.admin.ch/pxweb/de/px-x-0103010000_151/-/px-x-0103010000_151.px (15. August 2026) 2

  2. Links: BFS, "Szenarien zur Bevölkerungsentwicklung der Schweiz 2025–2055", https://bevoelkerungsszenarien.bfs.admin.ch/ (15. August 2026). Rechts: BFS, "Szenarien zur Bevölkerungsentwicklung der Schweiz und der Kantone 2025–2055", Neuchâtel 2025, Abschnitt 2.3.2 und Tabelle T6, BFS-Nummer 201-2500. https://www.bfs.admin.ch/bfs/de/home/statistiken/bevoelkerung/zukuenftige-entwicklung.assetdetail.34687099.html