How to Map German Federal Election Results in Python

On this page

Problem statement

A German Bundestag election produces two results for every constituency. The first vote (Erststimme) elects a constituency member. The second vote (Zweitstimme) decides how many seats each party gets nationally. Mapping it means choosing which vote to show, reading a results file with German number formats in a long, nested layout, and joining it to the 299 constituencies. Since the 2023 electoral reform, there is a third thing to map: constituency winners who did not get a seat, because their party's second-vote result did not cover them.

This page reads the official results of the federal election of 23 February 2025 from the Federal Returning Officer (Bundeswahlleiterin). It joins them to the official constituency geometry and maps the first-vote winners, the second-vote leaders and the AfD vote. It also finds the constituencies left without a directly elected member.

Quick answer

Read the long-format CSV with ; separators and decimal commas, keep constituency rows, and pivot to one row per constituency and party:

d = pd.read_csv("data/kerg2.csv", sep=";", skiprows=9, dtype=str)
d["votes"] = pd.to_numeric(d.Anzahl, errors="coerce")
d["share"] = pd.to_numeric(d.Prozent.str.replace(",", "."), errors="coerce")       # German decimal comma
wk = d[(d.Gebietsart == "Wahlkreis") & (d.Gruppenart != "System-Gruppe")].assign(WKR_NR=lambda x: x.Gebietsnummer.astype(int))
second = wk[wk.Stimme == "2"].pivot_table(index="WKR_NR", columns="Gruppenname", values="share")
gdf = geo.join(second[["CDU", "CSU", "AfD", "SPD", "GRÜNE", "Die Linke"]], on="WKR_NR")
print(f"{len(gdf)} constituencies joined, {gdf.AfD.isna().sum()} without results; AfD share {gdf.AfD.min():.1f}–{gdf.AfD.max():.1f}%")
299 constituencies joined, 0 without results; AfD share 6.3–46.7%
Three maps of Germany's 299 constituencies in the 2025 Bundestag election. The first-vote map is CDU black in the west, CSU grey in Bavaria and AfD blue across the east, with SPD red pockets in Lower Saxony, the Ruhr and the Saarland area, and amber outlines on 23 constituencies whose winner got no seat. The second-vote map is similar but with far fewer SPD constituencies. The AfD share map is dark across the five eastern states and light in the west. Three maps of Germany's 299 constituencies in the 2025 Bundestag election. The first-vote map is CDU black in the west, CSU grey in Bavaria and AfD blue across the east, with SPD red pockets in Lower Saxony, the Ruhr and the Saarland area, and amber outlines on 23 constituencies whose winner got no seat. The second-vote map is similar but with far fewer SPD constituencies. The AfD share map is dark across the five eastern states and light in the west.
The 2025 Bundestag election by constituency: first-vote winners, largest second-vote party and AfD second-vote share. Data: © Die Bundeswahlleiterin, Wiesbaden 2025, dl-de/by-2-0.

The file layout, first and second votes, winners

1. Understand the file layout

kerg2.csv has a nine-line preamble, then one row per area, group and vote. The area types are federal, state and constituency; the groups are parties, independents and system groups (electorate, turnout, invalid votes):

print(d.Gebietsart.value_counts().to_dict())
print(d.Gruppenart.value_counts().to_dict())
fed = d[(d.Gebietsart == "Bund") & (d.Stimme == "2") & (d.Gruppenart == "Partei")].nlargest(8, "votes")
print(fed[["Gruppenname", "votes", "share"]].round(2).to_string(index=False))
{'Wahlkreis': 14776, 'Land': 774, 'Bund': 67}
{'Partei': 12950, 'System-Gruppe': 2528, 'Einzelbewerber/Wählergruppe': 139}
Gruppenname      votes  share
        CDU 11196374.0  22.55
        AfD 10328780.0  20.80
        SPD  8149124.0  16.41
      GRÜNE  5762380.0  11.61
  Die Linke  4356532.0   8.77
        CSU  2964028.0   5.97
        BSW  2472947.0   4.98
        FDP  2148757.0   4.33

Read the numbers as text first. Anzahl uses no thousands separator, but Prozent uses a decimal comma, and pandas would otherwise read 22,550824 as text or the wrong number.

2. First vote and second vote are different maps

first = wk[wk.Stimme == "1"].loc[lambda x: x.groupby("WKR_NR").votes.idxmax()].set_index("WKR_NR")
lead2 = wk[wk.Stimme == "2"].loc[lambda x: x.groupby("WKR_NR").votes.idxmax()].set_index("WKR_NR")
print("first-vote winners:", first.Gruppenname.value_counts().to_dict())
print("second-vote leaders:", lead2.Gruppenname.value_counts().to_dict())
split = (first.Gruppenname != lead2.Gruppenname)
print(f"constituencies where they differ: {split.sum()}; most common:",
      pd.Series(list(zip(first.Gruppenname[split], lead2.Gruppenname[split]))).value_counts().head(3).to_dict())
first-vote winners: {'CDU': 143, 'CSU': 47, 'AfD': 46, 'SPD': 45, 'GRÜNE': 12, 'Die Linke': 6}
second-vote leaders: {'CDU': 170, 'AfD': 49, 'CSU': 47, 'SPD': 17, 'GRÜNE': 9, 'Die Linke': 7}
constituencies where they differ: 34; most common: {('SPD', 'CDU'): 26, ('GRÜNE', 'Die Linke'): 2, ('GRÜNE', 'CDU'): 2}

The first-vote map favours parties with strong local candidates; the second-vote map shows party support. In 2025 the two differ in 34 constituencies, 26 of them where an SPD candidate won the constituency while the CDU led the party vote.

3. Find the winners without a seat

Under the reformed electoral law, a party keeps only as many constituency seats as its second-vote share entitles it to. Winners beyond that are not elected, and the results file marks it in the Gewählt column:

lost = first[first["Gewählt"] == "–"]
print(f"{len(lost)} constituency winners not elected:", lost.Gruppenname.value_counts().to_dict())
print(lost.Gebietsname.head(6).tolist())
23 constituency winners not elected: {'CDU': 15, 'AfD': 4, 'CSU': 3, 'SPD': 1}
['Flensburg – Schleswig', 'Rostock – Landkreis Rostock II', 'Bremen I', 'Oberhavel – Havelland II', 'Halle', 'Leipzig I']
Constituency winners by first vote and leading party by second vote, Bundestag election 2025. two votes, two mapspartyfirst-vote winnerssecond-vote leaderwinners not electedCDU14317015CSU47473AfD46494SPD45171GRÜNE / Die Linke12 / 69 / 70299 constituencies; first and second vote differ in 34 spatialworkflow.io
Two votes, two maps; they differ in 34 constituencies.

4. Map the second-vote share

gdf["east"] = gdf.LAND_NR.isin(["12", "13", "14", "15", "16"])          # the five eastern states, Berlin excluded
print(gdf.groupby("east").AfD.describe()[["count", "mean", "min", "max"]].round(1).to_string())
ax = gdf.to_crs(25832).plot(column="AfD", cmap="Blues", legend=True, figsize=(6, 8), edgecolor="white", linewidth=0.2)
ax.set_axis_off(); ax.figure.savefig("data/afd_2025.png", dpi=150)
       count  mean   min   max
east                          
False  251.0  18.1   6.3  31.2
True    48.0  36.6  18.5  46.7

The AfD map is a picture of the former inner-German border: the 48 constituencies of the five eastern states average 36.6% (18.5–46.7%), the 251 others 18.1%, and none outside the east passes 31.2%.

Turnout and the winner's margin

The data used on this page

Final results of the 2025 Bundestag election (kerg2.csv) and constituency geometry (btw25_geometrie_wahlkreise_shp_geo) from Die Bundeswahlleiterin, © Die Bundeswahlleiterin, Wiesbaden 2025, Datenlizenz Deutschland – Namensnennung 2.0.

from pathlib import Path

import geopandas as gpd
import matplotlib
matplotlib.use("Agg")
import numpy as np
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
BW = "https://www.bundeswahlleiterin.de/"
SOURCES = {"kerg2.csv": BW + "bundestagswahlen/2025/ergebnisse/opendata/btw25/csv/kerg2.csv",
           "btw25_wk_geo.zip": BW + "dam/jcr/556bec9c-be80-4818-a368-fe6596f15f08/btw25_geometrie_wahlkreise_shp_geo.zip"}
for name, url in SOURCES.items():
    if not Path("data", name).exists():
        Path("data", name).write_bytes(requests.get(url, timeout=300).content)
geo = gpd.read_file("data/btw25_wk_geo.zip")
print(Path("data/kerg2.csv").read_text(encoding="utf-8-sig").splitlines()[0])
print(f"{len(geo)} constituencies, CRS {geo.crs}")
(c) Die Bundeswahlleiterin, Wiesbaden 2025;;;;;;;;;;;;;;;;
299 constituencies, CRS EPSG:4326

Example 1: turnout by constituency

sysg = d[(d.Gebietsart == "Wahlkreis") & (d.Gruppenart == "System-Gruppe")]
t = sysg.pivot_table(index="Gebietsnummer", columns="Gruppenname", values="votes", aggfunc="first")
t["turnout"] = t["Wählende"] / t["Wahlberechtigte"] * 100
print(f"turnout {t.turnout.min():.1f}–{t.turnout.max():.1f}%, median {t.turnout.median():.1f}%")
print("lowest:", d.set_index("Gebietsnummer").Gebietsname.drop_duplicates().reindex(t.turnout.nsmallest(3).index).tolist())
turnout 73.4–88.0%, median 82.9%
lowest: ['Duisburg II', 'Bremen II – Bremerhaven', 'Gelsenkirchen']
Mean AfD second-vote share in the eastern and western constituencies, 2025. AfD second vote, east and west5 eastern states, mean36.6% (48 constituencies)eastern minimum18.5%rest, mean18.1% (251)rest, maximum31.2%025%50%Berlin counted with the rest spatialworkflow.io
The east-west divide in the AfD vote.

Example 2: the second-vote winner's margin

top2 = wk[wk.Stimme == "2"].sort_values("votes", ascending=False).groupby("WKR_NR").head(2)
margin = top2.groupby("WKR_NR").share.agg(lambda s: s.iloc[0] - s.iloc[1])
gdf["margin2"] = margin
print(f"closest second-vote race: {margin.min():.2f} points; widest {margin.max():.1f} points")
print(gdf.nsmallest(3, "margin2")[["WKR_NAME", "margin2"]].round(2).to_string(index=False))
closest second-vote race: 0.02 points; widest 27.2 points
                                  WKR_NAME  margin2
                     Helmstedt – Wolfsburg     0.02
Delmenhorst – Wesermarsch – Oldenburg-Land     0.03
                Berlin-Marzahn-Hellersdorf     0.12

Constituencies and proportional representation combined

German federal elections combine constituencies and proportional representation. The first vote produces 299 local winners and the second vote the national party shares that decide the size of each parliamentary group. Before 2023, a party that won more constituencies than its second-vote share justified kept them as overhang seats, and other parties received balancing seats. The 2023 reform fixed the Bundestag at 630 seats and removed overhang. A constituency winner now enters parliament only if the party's second-vote result covers that seat.

How a constituency winner can miss out on a seat under the 2023 electoral law. from first vote to seat, 2025win theconstituencymost first votesparty seatsfrom second votesrank winnersby first-vote sharecovered?seats ≥ rankseat or not23 not electedBundestag fixed at 630 seats; no overhang spatialworkflow.io
How the 2023 reform decides whether a constituency winner sits.

Mapping it well means being explicit about which vote is shown. A first-vote map tells you about candidates and local strongholds. A second-vote map tells you about party support, and the national result is built from it. The 2025 innovation, constituencies without an elected winner, is invisible on both unless you map it on purpose.

CDU and CSU, decimal commas and redistricting

Common problems reading and mapping German election results. Bundestag mapping problemsSymptomFixshares read as textreplace the decimal commaheader row not foundskip the 9-line preambleCSU missing outside Bavariaexpected: CDU/CSU never competemap shows a winner with no seatcheck Gewählt == “–”kerg2.csv, Bundeswahlleiterin 2025 spatialworkflow.io
Mapping problems with German election data.
  • CDU and CSU never compete in the same constituency: the CSU stands only in Bavaria, the CDU everywhere else.
  • Decimal commas and ; separators are standard in German official CSV files; read numbers as text and convert.
  • Constituency boundaries are revised between elections; join results and geometry from the same election.
  • The file has a preamble of nine lines with licence and status; skiprows=9 reaches the header.
  • Independent candidates appear as their own group type (Einzelbewerber/Wählergruppe); keep them in first-vote totals.
  • Berlin is sometimes grouped with the east and sometimes not; state your choice.

FAQ

Where can I download German election results as CSV?

From the Federal Returning Officer (Bundeswahlleiterin) open data pages; kerg2.csv has every constituency, state and federal result in long format.

Should I map the first vote or the second vote?

The second vote for party support and the national outcome; the first vote for constituency winners and candidate effects. Label the map with which one it is.

Why did some German constituency winners not get a seat in 2025?

Under the 2023 reform, a party keeps only as many constituency seats as its second-vote share covers; 23 winners in 2025 were not elected.

How do I read German CSV numbers in pandas?

Use sep=";" and convert decimal commas, e.g. pd.to_numeric(s.str.replace(",", ".")), or pass decimal="," to read_csv.