Source Code: Wiener Gemeinderatswahl 2015

Die meisten Kriterien für einen hohen Nachrichtenwert dürfte dieser Beitrag wohl verfehlen. Die Wahl ist seit Monaten geschlagen und obwohl ein spannungsgeladenes Kopf-an-Kopf-Rennen prognostiziert wurde, kam es nur zu vergleichsweise sanften Verschiebungen. Ob das ganze Spektakel dabei mehr als rein politische Vorderbühne war, mehr als der hoffnungslose Versuch ein Profil ohne Relief zu schaffen, darf jede*r für sich selbst entscheiden. Wenn die Wahlen bisher aber auch noch keine grundsätzlichen Erneuerungen und Reformen mit sich gebracht haben, so nutzt das ganze Spektakel zumindest als Basis mir die Zeit an einem regnerischen Sonntag zu vertreiben.

Nachdem ich am Wochenende beim Durchstöbern der open data Plattform Österreichs auf die Sprengelergebnisse der Wienwahl gestoßen bin, wollte ich mich nach dem medialen Hick Hack um die Mandatsvergabe daran versuchen die Ergebnisse selbst zu berechnen und mit dem ggmap-Package von R zu visualisieren. Beim nächsten wochenendlichen Regenschauer wird dann versucht das Reproduzierte in eine Shiny-App zu packen und Veränderungen des Wahlausgangs durch die umstrittene Wahlzahl interaktiv nachvollziehbar zu machen. Vorerst reicht die Schlechtwetterfront glücklicherweise aber nur aus, um den ersten Teil des Projekts wachstumsfördernd zu tränken: die Berechnung und (statische) Darstellung der Ergebnisse.

In einem ersten Schritt werden dazu die abgegebenen Stimmen auf Sprengelebene eingelesen und zu Wahlkreisen aggregiert. In einem zweiten Schritt folgt ein Skript zur Berechnung der Grund- und Restmandate auf Wahlkreisebene, bevor die vergebenen Mandate im zweiten Durchgang berücksichtigt werden und als bunte Wienkarte Gestalt annehmen.

Datenkosmetik

Die Berechnung an sich benötigt keine zusätzlichen R-Packages. Magrittr und data.table werden lediglich zur komfortableren Code-Gestaltung und zur einfacheren Datenmanipulation geladen. Ggmap wird benötigt, um den Zahlen ein kartographisches Gesicht zu verleihen.

# benötigte packages laden
library(magrittr)
library(data.table)
library(ggmap)
library(knitr)

Das Einlesen und Strukturieren der Daten wird hier unnötig umständlich angegangen. Während mensch sich einige der Arbeitsschritte sparen könnte, habe ich sie – aus Gründen der Nachvollziehbarkeit – trotzdem angeführt. Die Rohdaten gibt es hier. In den Syntax-Kommentaren zu den einzelnen Variablen finden sich Kurzbeschreibungen des Inhalts bzw. Erläuterungen zur gewählten Vorgehensweise. Falls sich jemand fragen sollte, wie es in einzelnen Sprengeln mehr abgegebene Stimmen als Wahlberechtigte geben kann, der*die sei an dieser Stelle auf eine kleine Erläuterung zu den Daten verwiesen.

# Daten einlesen
url <- "http://www.wien.gv.at/politik/wahlen/ogd/gr151_99999999_9999_spr.csv"
data <- read.csv(url,header=T,sep=";")
# Daten strukturieren
data <- data.table(data) %>% subset(select=c(NUTS1:FREIE)) # Leerspalte löschen
data$NUTS1 <- as.factor(data$NUTS1) # Gruppe von Bundesländern AT1:1545
data$NUTS2 <- as.factor(data$NUTS2) # Bundesland AT13:1545
data$NUTS3 <- as.factor(data$NUTS3) # Gruppe von Bezirken AT130:1545
data$DISTRICT_CODE <- as.factor(data$DISTRICT_CODE) # Gemeindebezirkszahl 90101-92301
data$T <- as.character(data$T)      # Hierarchietyp: 1=Wahlkreisverband, 2=Wahlkreis, 3=Bezirk, 4=Sprengel
data$WK <- as.character(data$WK)    # Wahlkreise
data$BZ <- as.character(data$BZ)    # Bezirkszahl 1-23 (9x Wahlkartenerfassungsbezirk für Wahlkreis x) 
data$SPR <- as.character(data$SPR)  # Sprengelnummer 0-158, 0=Spitalssprengel (keine Wahlberechtigten)

Je nach Anzahl der Wahlberechtigten, gibt es unterschiedliche Mandatszahlen für die einzelnen Wahlkreise. Warum sich diese nicht schon in die eigentliche Quelldatei integriert wurden, bleibt mal dahingestellt. Die zu vergebenden Mandate je Wahlkreis lassen sich aber relativ schnell recherchieren. In Summe hat Wien 100 Mandate zu vergeben. Wahlkreise sind – um das auch noch erwähnt zu haben – bis auf die kleineren Innenbezirke – weitgehend Deckungsgleich mit den allgemeinen Bezirksgrenzen.

# Mandate pro Wahlkreis
dmmy <- data.table("WK"=1:18,"WK.Mandate"=c(
    7, # Zentrum (Innere Stadt, Wieden, Margareten, Mariahilf)
    5, # Innen-West (Neubau, Josefstadt, Alsergrund)
    5, # Leopoldstadt
    5, # Landstraße
    10, # Favoriten
    6, # Simmering
    5, # Meidling
    3, # Hietzing
    5, # Penzing
    3, # Rudolfsheim-Fünfhaus
    5, # Ottakring
    3, # Hernals
    3, # Währing
    4, # Döbling
    4, # Brigittenau
    10, # Floridsdorf
    11, # Donaustadt
    6)) # Liesing

Berechnung der Grundmandate

Nachdem die ersten Vorbereitungsschritte gestapft wurden, kann zu den Berechnungen angesetzt werden: die Grundmandate oder die “billigen” Mandate. Um diesen Berechnungsschritt wurde in der jüngeren Vergangenheit Wiens wild gestritten. Parteiübergreifend wurden Päkte und Notariatsakte unterzeichnet, in Nacht und Nebel Aktionen wechselten abgesägte Abgeordnete das Parteischiff und brachten grüne Fregatten zum Kentern. Der ganze Spuk? Die Stimmen, um ein Grundmandat zu erhalten (die Wahlzahl), bestimmen sich nicht rein über die verfügbaren Mandate innerhalb eines Wahlkreises. Ein zusätzlicher Faktor verzerrt hier etwas die Verhältnisse. Aktuell berechnet sich die Wahlzahl anhand der gültig abgegebenen Stimmen dividiert durch die verfügbaren Mandate plus eins. Durch den höheren Divisor (Mandate plus eins) wird die Wahlzahl bzw. die benötigte Stimmenanzahl für ein (Grund-)Mandat kleiner. Ein Grundmandat ist damit “billiger” zu ergattern, als ein Mandat im zweiten Durchgang. Begünstigt werden von dieser Regel besonders Parteien, die in einzelnen Bezirken unverhältnismäßig stark vertreten sind bzw. Parteien mit einem soliden Stand in den großen Flächenbezirken. Diese stellen ja ohnehin schon einen hohen Anteil der 100 zu vergebenden Mandate des Gemeinderats.

# Berechnung der Grundmandate
# Wahlzahl: gültige Stimmen/(verfügbare Mandate + Bonus)
bonus <- 1
dmmy$GS <- data[,sum(ABG.-UNG.),by=WK]$V1 # gültige Stimmen je WK
dmmy$WZ <- divide_by(dmmy$GS,dmmy$WK.Mandate+bonus) # Wahlzahl je WK
# Grundmandate
dmmy <- data[,lapply(list(SPOE,FPOE,OEVP,GRUE,NEOS,WWW,ANDAS,GFW,SLP,WIFF,M,FREIE),
                     sum),by=WK] %>% # erhaltene Stimmen in WK je Partei
     subset(select=c(2:length(.))) %>% # Spalte mit WK fallen lassen
     divide_by_int(dmmy$WZ) %>% # Grundmandate: ganzzahlige Division Stimmen/WZ
     setnames(colnames(data)[13:24]) %>% # Spaltenüberschriften formatieren
     cbind(dmmy,.) # Daten zusammenführen
dmmy$VGM <- rowSums(dmmy[,5:ncol(dmmy),with=FALSE]) # vergebene Grundmandate
dmmy$RGM <- dmmy$WK.Mandate - dmmy$VGM # Restmandate 

Zweiter Durchgang: Verteilung der Restmandate

Nachdem die Grundmandate durch den obigen Code verteilt wurden, muss in einem zweiten Schritt mit den Reststimmen auch noch etwas passieren. Am Kampf um die Reststimmen nehmen all jene Parteien teil, die zumindest eine von zwei Bedingungen erfüllen. Die Parteien müssen im gesamten Wahlgebiet mindestens fünf Prozent der Stimmen oder aber in zumindest einem Wahlkreis ein Grundmandat erreicht haben.

Der Wahlzahlbonus von plus eins begünstigt damit nicht per se größere Parteien auf Stadtebene. Belohnt wird durch den höheren Divisor vielmehr eine gewisse Verankerung der Parteien innerhalb der jeweiligen Bezirksgrenzen. Ein gutes Abschneiden auf dieser Ebene sichert Vorteile auf gesamtstädtischer Ebene. Politischen Kräften, die zur Wahl antreten, aber mitunter nur lokal stark vertreten sind, wird es durch den Bonus sogar ermöglicht ein Hintertürchen zum Gemeinderat zu finden, dessen Eingang grundsätzlich ein 5-Prozent-Hürde schützt: Wer es nicht schafft, im gesamten Stadtgebiet zumindest 5 Prozent der Wähler*innen hinter sich zu vereinen, darf vor dem, nicht aber im Rathaus Platz nehmen. Als Ausnahme zu dieser Regel gelten aber all jene, die zumindest in einem Bezirk ein Mandat erobern konnten. Das so festgehaltene im Hinterkopf, lässt sich de facto aber trotzdem der Schluss herstellen, dass große Parteien durch den Bonus in der bisherigen Wahlgeschichte überproportional viele Mandate im Vergleich zu den gültig abgegebenen Stimmen erhalten haben.

Nachdem nun zuvor bestimmt wurde, welche Parteien 2015 ein Grundmandat erhalten haben, müssen an dieser Stelle auch noch all jene Parteien berücksichtigt werden, die in der gesamten Stadt zumindest fünf Prozent der Stimmen erhalten haben. Die so zusammengesetzte Liste an Parteien wartet dann im zweiten Durchgang auf die Vergabe der Reststimmen. Wie schon bei der Aufteilung der Gemeinderatssitze auf die einzelnen Bezirke, kommt hier der d’Hondt’sche-Teiler zur Anwendung.

# Verteilung der Restmandate
# Bedingung: 5% der Stimmen im gesamten Gebiet oder min. 1 Grundmandat
gm <- colSums(dmmy)[5:16] %>% .[which(.>0)] # Parteien mit Grundmandat
gm.partei <- which(gm>0)
rs <- data[,lapply(list(SPOE,FPOE,OEVP,GRUE,NEOS,WWW,ANDAS,GFW,SLP,WIFF,M,FREIE),
                  sum),by=WK] %>% # erhaltene Stimmen in WK je Partei
    subset(select=c(2:length(.))) %>% # Spalte mit WK fallen lassen
    subtract((dmmy[,5:16,with=F]*dmmy$WZ)) %>% # Reststimmen
    colSums() # pro Partei

# Parteien über 5 % Grenze
thld <- colSums(data[,13:24,with=FALSE]) %>% 
    divide_by(sum(dmmy$GS)) %>% # Stimmen je Partei gesamt
    .[which(.>=0.05)] # Parteien, mit wienweitem Ergebnis >= 5 %
thld.partei <- which(thld>=0.05)

# Parteien für Restmandate
select <- append(thld.partei,gm.partei) %>% .[!duplicated(.)]

# Reststimmen für "relevante" Parteien
rs <- data.table(matrix(rs[select],nrow=1)) %>% 
    setnames(colnames(dmmy)[5:16][select])

# Reststimmen berechnen
dmmy.rs <- numeric()
for (i in 1:100){ # d'Hondt-Teiler
    dmmy.rs <- append(dmmy.rs,rs/i)
}

# Wahlzahl: x-größtes Element der gereihten d'Hondt-Teiler-Reststimmen
wz <- unlist(dmmy.rs) %>% sort(decreasing=TRUE) %>% 
    .[sum(dmmy$RGM)]
x <- which(unlist(dmmy.rs)>=wz)
x <- sapply(unique(names(x)),function(f) length(which(names(x)==f))) # Restmandate

# Gesamtzahl der Mandate berechnen
gm <- data.table("Partei"=names(gm),"Grundmandate"=gm)
x <- data.table("Partei"=names(x),"Mandate"=x)
select <- which(x$Partei %in% gm$Partei)
x$Mandate[select] <- x$Mandate[select] + gm$Grundmandate # Mandate gesamt
x$Grundmandate <- 0 
x$Grundmandate[select] <- gm$Grundmandate

Nachdem nun die Grund- und Restmandate berechnet wurden, bleibt nichts weiter zu tun, als die Ergebnisse zusammenzuführen und schon zeigt sich die Verteilung der aktuell vergebenen Gemeinderatssitze.

Partei Mandate Grundmandate
SPOE 44 38
FPOE 34 29
OEVP 7 0
GRUE 10 3
NEOS 5 0

Sprengelergebnisse

Um die schlicht gehaltene Tabelle der vergebenen Gemeinderatssitze durch ihr zugrundeliegendes Lokalkolorit aufzupeppen und die Ergebnisse punktgenauer betrachten zu können, benötigen wir die einzelnen Sprengelergebnisse. Die zuvor berechneten Grund- und Restmandate helfen uns hier wenig weiter; also geht es zurück zum Ausgangspunkt – zu den kosmetisch behandelten Rohdaten.

In einem ersten Schritt werden die jeweiligen Stimmanteile auf Sprengelebene berechnet. Um diese Ergebnisse in einem zweiten Schritt auf einer Karte visualisieren zu können, erhält jeder Sprengel eine gesonderte ID – eine Kombination aus Bezirkskennzahl und Sprengelkennzahl. Da die Sprengelkennzahl in jedem Bezirk von neu beginnt, was zu Überschneidungen führen kann, ist eine Verbindung mit der Bezirkskennzahl erfordelich, um eine eindeutige Zuordnung zu ermöglichen.

Zur Datenbereinigung werden nun Sprengel ohne Stimmabgaben gelöscht. Solange wir noch etwas vom Szenario einer 0-prozentigen Wahlbeteiligung entfernt sind, betrifft dies nur “unechte” Sprengel wie beispielsweise Spitäler etc., in denen Wahlberechtigte aus unterschiedlichen Wahlbezirken zusammengefasst werden.

Abschließend werden jene Parteien mit den meisten Stimmen innerhalb eines Sprengels als Sieger*innen erkoren. Das Ausmaß des jeweiligen Sieges wird hier aber ebenso wenig erfasst, wie eventuelle Gleichstände zwischen zwei oder mehr Parteien. Ich bin nicht ganz sicher, was bei Gleichständen tatsächlich passiert. Spekulierend gehe ich aber von folgeder Logik aus: je weiter links sich der Name einer Partei in meiner Datentabelle findet, desto eher wird ihr ein Gleichstand als Sieg zugerechnet. Ein Gleichstand zwischen einer Partei in Spalte eins und zwei, wird zu Gunsten der Partei in Spalte eins ausgelegt. Ein Gleichstand zwischen Spalte drei und fünf zu Gunsten der Partei in Spalte drei. Dieser Algorithmus schmeichelt in der Ergebnisdarstellung daher am stärkste der SPÖ, dann der FPÖ und so weiter. Zum Glück investieren die echten Wahlhelfer*innen etwas mehr Energie in die Berechnung der Ergebnisse.

# Karte erstellen
data <- subset(data, select=c(WK:FREIE))              # Leerspalte löschen
data$SPRID <- as.character(                           # Eindeutige Sprengel-ID erstellen (z.B.: 01001)
    paste(as.numeric(data$BZ),formatC(as.numeric(data$SPR)
                                      ,width=3,flag="0"),sep=""))
# Sprengelsiege berechnen
spr <- cbind(data$ABG.-data$UNG., # gültige Stimmen
             data[,c(7:(ncol(data)-1)),with=FALSE]/(data$ABG.-data$UNG.), # Stimmanteile je Partei
             data[,ncol(data),with=FALSE]) %>% # SPRID
    data.table() %>% 
    setnames(c("GS",colnames(data)[7:ncol(data)])) # Spalten formatieren
spr <- spr[spr$GS!=0] # Bezirke ohne Stimmabgabe löschen (0er Sprengel u andere komische?)
spr$S <- sapply(1:nrow(spr),function(f)  # Siegerspalte pro Sprengel
    which(spr[f,2:13,with=F]==max(spr[f,2:13,with=F]))[1]) # erstes Elemnet wählen.

Abschließend werden nun noch eine Wienkarte und die dazugehörigen Sprengelgrenzen geladen. Die Grenzen werden von Wahl zu Wahl neu gezogen. Eine Definition der Wahlsprengel findet sich lobenswerter Weise wieder auf der open data Plattform. Ein Minimum an Formatierungsaufwand bleibt mensch aber auch hier nicht erspart. Hinweise, warum die Polygone der Sprengelgrenzen so verschachtelt-verstringt sind, sind übrigens jederzeit willkommen. Bis dahin gehe ich mal einfach davon aus, dass die Mehrzahl der User*innen – im Gegensatz zu mir – mit diesem Format tatsächlich etwas anfangen kann.

spr.poly <- read.csv("http://data.wien.gv.at/daten/geo?service=WFS&request=GetFeature&version=1.1.0&typeName=ogdwien:WAHLSPRGR2015OGD&srsName=EPSG:4326&outputFormat=csv",header=T) # Polygone für Wahlsprengel
# Polygone in passendes Format bringen:
# - unbenötigte Zeichen löschen (POLYGON, MULTI, '(',')')
# - LON und LAT voneinander trennen (" ")
# - in data.table-Format überführen
dmmy.poly <- lapply(spr.poly$SHAPE,function(f) f %>%
                        as.character() %>% gsub("POLYGON |\\(|\\)|MULTI","",.) %>%
                        strsplit(",") %>% unlist() %>% strsplit(" ") %>%
                        unlist() %>% .[.!=""] %>% as.numeric() %>%
                        matrix(ncol=2,byrow=T) %>% set_colnames(c("lon","lat")) %>%
                        data.frame)
# Daten in passende Struktur überführen: data.table mit SPRENGEL-ID als Schlüssel
poly <- data.table()
for (i in 1:length(dmmy.poly)){
    dmmy <- data.table(cbind(as.character(spr.poly$SPRENGEL[i]),dmmy.poly[[i]]))
    poly <- rbindlist(list(poly,dmmy))
}
setnames(poly, c("SPRID","LON","LAT"))

Nachdem nun die geographischen Längen- und Breitengrade ausgelesen, entworren und mit einer Sprengel-ID gekennzeichnet wurden, die mit jener in der Datentabelle der Wahlergebnisse korrespondiert, kann zum letzten Feinschliff angesetzt werden: die einzelnen Sprengel werden in die Farben der jeweiligen Siegerparteien gekennzeichnet.

Als Kontext schaffenden Hintergrund laden wir eine allgemeine Karte von Wien. Mit der geom_path-Funktion von ggmap spannen wir ein Liniennetz (die Sprengelgrenzen gemäß der zuvor aufbereiteten Tabelle) über die gesamte Stadt. Hier ist es wichtig, der Funktion Anhaltspunkte zur Gruppierung der Daten zu geben. Im Grunde geben wir die Anweisung, die Sprengelgrenzen je unabhängig voneinander zu zeichnen. Ohne diese Ergänzung wird – ohne abzusetzen – eine durchgängige Linie gezeichnet, die durch alle gegebenen Breitengrade führt. Anschaulicher, als diese umständliche Beschreibung ist vermutlich das Ergebnis einer fehlenden Gruppierung – mehr Kinderzeichnung, als Wahlergebnis (Abb.1).

Deutlich klarer erscheint das Stadtbild demgegenüber, wird die Gruppierung gesetzt (vgl. Abb.2). Um zum Abschluss auch die Verteilung der Sieger-Parteien darzustellen werden die entsprechenden Farben mit der geom_polygon-Funktion zugewiesen und schon haben wir unser DIY-Funktionenset zur Berechnung und Darstellung der Wahlergebnisse (Abb. 3/4).

# Daten zusammenführen
spr$SPRID <- as.factor(spr$SPRID)
select <- sapply(poly$SPRID, function(f) which(spr$SPRID %in% f))
poly$PARTEI <- spr$S[select]
poly$PARTEI[poly$PARTEI==1] <- "#a5041e" # rot
poly$PARTEI[poly$PARTEI==2] <- "#041ea5" # blau
poly$PARTEI[poly$PARTEI==3] <- "#000000" # schwarz
poly$PARTEI[poly$PARTEI==4] <- "#1ea504" # grün

# Ausgabe der Karte mit Hintergrund
vie.map <- get_map(location="vienna",source="stamen",maptype="toner",zoom=10) # Karte laden
ggmap(vie.map) +
    geom_polygon(data=poly,
                 aes(x=LON,y=LAT,group=SPRID),
                 fill=poly$PARTEI,
                 alpha=0.7) +
    geom_path(data=poly,
              aes(x=LON,y=LAT,group=SPRID),
              colour="grey",
              alpha=0.2,size=0.3) +
    coord_fixed(1.5) + 
    theme()

# Ausgabe der Karte ohne Hintergrund
ggplot() +                                      
    geom_polygon(data=poly,                      # Linien ziehen
                 aes(x=LON,y=LAT,group=SPRID),
                 fill=poly$PARTEI,
                 alpha=0.7) +
    geom_path(data=poly,                         # Flächen füllen
              aes(x=LON,y=LAT,group=SPRID),
              colour="grey",
              alpha=0.2,size=0.3) +
    coord_fixed(1.5) +                           # lon/lat-Verhältnis definieren (sonst verzerrt)
    theme(panel.border = element_blank(),        # Karte & Co im Hintergrund ausblenden
          panel.background = element_blank(),
          legend.key = element_blank(),
          axis.ticks = element_blank(),
          axis.text.y = element_blank(),
          axis.text.x = element_blank(),
          panel.grid = element_blank(),
          text = element_blank())

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google Foto

Du kommentierst mit Deinem Google-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s

Erstelle kostenlos eine Website oder ein Blog auf WordPress.com.

Nach oben ↑

%d Bloggern gefällt das: