# Library-Aufrufe kann man sich eigentlich sparen, aber... library(readr) library(lubridate) library(tidyr) library(stringr) library(dplyr) library(openxlsx) library(curl) # lies_aktuellen_stand.R # # Enthält die Funktion zum Lesen der aktuellen Daten. #---- Hilfsfunktionen ---- archiviere <- function(df,a_directory = "daten/stimmbezirke") { #' Schreibt das Dataframe mit den zuletzt geholten Stimmbezirks-Daten #' als Sicherungskopie in das angegebene Verzeichnis #' if (!dir.exists(a_directory)) { dir.create(a_directory) } fname = paste0(a_directory,"/", # Zeitstempel isolieren und alle Doppelpunkte # durch Bindestriche ersetzen str_replace_all(df %>% pull(zeitstempel) %>% last(), "\\:","_"), ".csv") write_csv(df,fname) cat(as.character(now())," - Daten archiviert als ",paste0(a_directory,fname)) } hole_letztes_df <- function(a_directory = "daten/stimmbezirke") { #' Schaut im angegebenen Verzeichnis nach der zuletzt angelegten Datei #' und holt die Daten zurück in ein df if (!dir.exists(a_directory)) return(tibble()) # Die zuletzt geschriebene Datei finden und einlesen neuester_file <- list.files(a_directory, full.names=TRUE) %>% file.info() %>% # Legt eine Spalte namens path an tibble::rownames_to_column(var = "path") %>% arrange(desc(ctime)) %>% head(1) %>% # Pfad wieder rausziehen pull(path) if (length(neuester_file)==0) { # Falls keine Daten archiviert, gibt leeres df zurück return(tibble()) } else { return(read_csv(neuester_file)) } } # Sind die beiden df abgesehen vom Zeitstempel identisch? # Funktion vergleicht die numerischen Werte - Spalte für Spalte. vergleiche_stand <- function(alt_df, neu_df) { #' Spaltenweiser Vergleich: Haben die Daten sich verändert? #' (Anders gefragt: ist die Summe aller numerischen Spalten gleich?) #' Wurde für die Feldmann-Wahl benötigt; bei OB-Wahlen eigentlich überflüssig neu_sum_df <- alt_df %>% summarize_if(is.numeric,sum,na.rm=T) alt_sum_df <- neu_df %>% summarize_if(is.numeric,sum,na.rm=T) # Unterschiedliche Spaltenzahlen? Dann können sie keine von Finns Männern sein. if (length(neu_sum_df) != length(alt_sum_df)) return(FALSE) # Differenzen? Dann können sie keine von Finns Männern sein. return(sum(abs(neu_sum_df - alt_sum_df))==0) } #--- CURL-Polling (experimentell!) # # Gibt das Änderungsdatum der Daten-Datei auf dem Wahlamtsserver zurück - # wenn es sich verändert hat, ist das das Signal, neue Daten zu holen. check_for_timestamp <- function(my_url) { # Erst checken: Wirklich eine Internet-Verbindung? # Sonst behandle als lokale Datei. if(str_detect(my_url,"^http")) { tmp <- curlGetHeaders(my_url, redirect = T, verify = F) # Redirect # if (stringr::str_detect(tmp[1]," 404")) { # Library(curl) h <- new_handle() # Das funktioniert, holt aber alle Daten -> hohe Last t <- curl_fetch_memory(my_url,handle=h)$modified %>% as_datetime() # } else { # t <- tmp[stringr::str_detect(tmp,"last-modified")] %>% # stringr::str_replace("last-modified: ","") %>% # parse_date_time("%a, %d %m %Y %H:%M:%S",tz = "CET") + hours(1) # } } else { # lokale Datei t = file.info(my_url)$mtime %>% as_datetime } return(t) } #---- Lese-Funktionen ---- # Das hier ist die Haupt-Lese-Funktion lies_stimmbezirke <- function(stand_url = stimmbezirke_url) { #' Versuche, Daten vom Wahlamtsserver zu lesen - und gib ggf. Warnung oder Fehler zurück #' Schreibt eine Meldung ins Logfile - zugleich ein Lesezeichen cat(as.character(now())," - Neue Daten lesen\n") # Touch logfile check = tryCatch( { stand_df <- read_delim(stand_url, delim = ";", escape_double = FALSE, locale = locale(date_names = "de", decimal_mark = ",", grouping_mark = "."), trim_ws = TRUE) %>% # Spalten umbenennen, Zeitstempel-Spalte einfügen mutate(zeitstempel=ts) %>% # Sonderregel: wir haben einen Zeitstempel, die "datum"-Spalte macht # Probleme, weil: starts_with("D"). select(-datum) %>% select(zeitstempel, nr = `gebiet-nr`, name = `gebiet-name`, meldungen_anz = `anz-schnellmeldungen`, meldungen_max = `max-schnellmeldungen`, # Ergebniszellen wahlberechtigt = A, # Mehr zum Wahlschein hier: https://www.bundeswahlleiter.de/service/glossar/w/wahlscheinvermerk.html waehler_regulaer = A1, waehler_wahlschein = A2, waehler_nv = A3, stimmen = B, stimmen_wahlschein = B1, ungueltig = C, gueltig = D, # neu: alle Zeilen mit Stimmen (D1..Dn) starts_with("D")) }, warning = function(w) {teams_warning(w,title="OB-Wahl: Datenakquise")}, error = function(e) {teams_warning(e,title="OB-Wahl: Datenakquise")}) return(stand_df) } aggregiere_stadtteildaten <- function(stimmbezirksdaten_df = stimmbezirksdaten_df) { #' Liest Stimmbezirke, gibt nach Ortsteil aggregierte Daten zurück #' (hier: kein Sicherheitscheck) stadtteildaten_df <- stimmbezirksdaten_df %>% left_join(stimmbezirke_df %>% select(nr,ortsteilnr,stadtteil), by="nr") %>% group_by(ortsteilnr) %>% # Fasse alle Spalten von meldungen_anz bis Ende der Tabelle zusammen - # mit der sum()-Funktion (NA wird wie null behandelt) summarize(zeitstempel = last(zeitstempel), nr = first(ortsteilnr), meldungen_anz = sum(meldungen_anz,na.rm =T), meldungen_max = sum(meldungen_max,na.rm = T), wahlberechtigt = sum(wahlberechtigt, na.rm = T), waehler_regulaer = sum(waehler_regulaer, na.rm = T), waehler_wahlschein = sum(waehler_wahlschein, na.rm = T), waehler_nv = sum(waehler_nv, na.rm = T), stimmen = sum(stimmen, na.rm = T), stimmen_wahlschein = sum(stimmen_wahlschein, na.rm = T), ungueltig = sum(ungueltig, na.rm = T), gueltig = sum(gueltig, na.rm = T), across(starts_with("D"), ~ sum(.,na.rm = T))) %>% mutate(across(where(is.numeric), ~ifelse(is.na(.), 0, .))) %>% # Stadtteilnamen, Geokoordinaten dazuholen left_join(stadtteile_df, by="nr") %>% # Wichtige Daten für bessere Lesbarkeit nach vorn relocate(zeitstempel,nr,name,lon,lat) # Sicherheitscheck: Warnen, wenn nicht alle Ortsteile zugeordnet if (nrow(stadtteildaten_df) != nrow(stadtteile_df)) teams_warnung("Nicht alle Stadtteile zugeordnet") if (nrow(stimmbezirke_df) != length(unique(stimmbezirke_df$nr))) teams_warnung("Nicht alle Stimmbezirke zugeordnet") cat("Stadtteildaten aggregiert.\n") return(stadtteildaten_df) } #---- Die Tabellen für die DW-Grafiken ergänzen ---- berechne_ergänzt <- function(stadtteildaten_df = stadtteildaten_df, top = top) { #' Ergänze die jeweils fünf führenden Kandidaten, ihre Prozentanteile ihre #' Stimmen und ihre Farbwerte. Und benenne die D1...Dn-Spalten nach #' Kandidat/in/Partei in der Form "Müller (ABC)" - gleichzeitig der Index für DW #' #' Gibt eine megalange Tabelle für Datawrapper zurück. # Zuerst ein temporäres Langformat, bei dem jede/d Kand in jedem Stadtteil eine Zeile # hat. Das brauchen wir 2x, um es wieder zusammenführen zu können. tmp_long_df <- stadtteildaten_df %>% pivot_longer(cols = starts_with("D"), names_to = "kand_nr", values_to = "kand_stimmen") %>% mutate(kand_nr = as.integer(str_extract(kand_nr,"[0-9]+"))) %>% # Ortsteil- bzw. Stimmbezirks-Gruppen, um dort nach Stimmen zu sortieren group_by(nr,name) %>% arrange(desc(kand_stimmen)) %>% mutate(Platz = row_number()) %>% left_join(kandidaten_df %>% select(kand_nr = Nummer, kand_name = Name, kand_partei = Parteikürzel, farbe= Farbwert), by="kand_nr") %>% mutate(kand = paste0(kand_name," (",kand_partei,")")) %>% mutate(prozent = if_else(gueltig != 0,kand_stimmen / gueltig * 100, 0)) ergänzt_df <- tmp_long_df %>% # Ist noch nach Stadtteil (name, nr) sortiert arrange(kand_nr) %>% # Alles weg, was verhindert, was individuell auf den Kand ist - außer # kand und Prozentwert select(-kand_stimmen, -kand_nr, -Platz, -kand_name, -kand_partei, -farbe) %>% # Kandidatennamen in die Spalten zurückverteilen pivot_wider(names_from = kand, values_from = prozent) %>% ungroup() %>% # und die zweite Hälfte dazu: left_join( tst <- tmp_long_df %>% # Brauchen nur die Kand-Ergebnisse - und den (Stadtteil-)name select(name, Platz, kand=kand_name,prozent,farbe) %>% # Nur die ersten (top) Plätze filter(Platz <= (top)) %>% #The Big Pivot: Breite die ersten (top) aus. pivot_wider(names_from = Platz, values_from = c(kand,prozent,farbe), names_glue = "{.value}{Platz}") %>% ungroup() %>% select(-nr), by="name") %>% # Sonderregelung: Wenn keine Stimmen, weise kand1-(top) NA zu (wg. Stadtteilen ohne Daten) mutate(across(starts_with("kand"), ~ if_else(meldungen_anz > 0, .,""))) %>% mutate(across(starts_with("farbe"), ~ if_else(meldungen_anz > 0, .,"#aaaaaa"))) cat("Ergänzte Stadtteildaten berechnet.\n") return(ergänzt_df) } berechne_kand_tabelle <- function(stimmbezirksdaten_df = stimmbezirksdaten_df) { # Nimmt die Stadtteildaten - oder auch die Wahllokale - und berechne daraus die # Nummer, Kandidat(in) in der Form "Müller (XYZ)", Parteikürzel, Stimmen, Prozent kand_tabelle_df <- stimmbezirksdaten_df %>% summarize(gueltig = sum(gueltig, na.rm = T), across(starts_with("D"), ~ sum(.,na.rm=TRUE))) %>% pivot_longer(cols=starts_with("D"),names_to = "nr", values_to = "Stimmen") %>% # Namen in Nr. umwandeln mutate(Nummer = as.integer(str_extract(nr,"[0-9]+"))) %>% left_join(kandidaten_df %>% select(Nummer, Name, Parteikürzel, Farbwert), by="Nummer") %>% mutate(name = paste0(Name," (",Parteikürzel,")")) %>% mutate(Prozent = Stimmen / gueltig * 100) %>% select(Nummer, `Kandidat/in` = name, Parteikürzel, Stimmen, Prozent) cat("Gesamttabelle alle Kandidaten berechnet.\n") return(kand_tabelle_df) } berechne_hochburgen <- function(stadtteildaten_df = stadtteildaten_df) { # Tabelle mit den drei stärksten und drei schwächsten Stadtteilen # im Vergleich zu GESAMT hochburgen_df <- stadtteildaten_df %>% select(name,gueltig,D1:ncol(.)) %>% # Eine Zeile für Frankfurt dazu bind_rows(stadtteildaten_df %>% select(name,gueltig,D1:ncol(.)) %>% summarize(gueltig = sum(gueltig, na.rm = T), across(starts_with("D"), ~ sum(.,na.rm=TRUE))) %>% mutate(name = "GESAMT")) %>% # Ins Langformat umformen, Nummer ist die Kandidatennummer pivot_longer(cols=starts_with("D"),names_to = "Nummer", values_to = "Stimmen") %>% mutate(Prozent = if_else(Stimmen == 0,0,Stimmen / gueltig * 100)) %>% # D1... in Integer umwandeln mutate(Nummer = as.numeric(str_extract(Nummer,"[0-9]+"))) %>% mutate(ist_gesamt = (name == "GESAMT")) %>% # Wichtig: "Currently, group_by() internally orders in ascending order." group_by(Nummer,ist_gesamt) %>% arrange(desc(Prozent)) %>% mutate(Platz = row_number()) %>% filter(Platz <= 3 | Platz > (nrow(stadtteile_df) - 3)) %>% mutate(Platz = if_else(ist_gesamt, as.integer(0),row_number())) %>% ungroup(ist_gesamt) %>% arrange(Platz) %>% # Namen dazuholen left_join(kandidaten_df %>% select (Nummer, Vorname, Name, Parteikürzel), by = "Nummer") %>% mutate(`Kandidat/in` = if_else(ist_gesamt, paste0(Vorname," ",Name," (",Parteikürzel,")"), "")) %>% # sortieren mutate(sort = 7* Nummer + Platz) %>% ungroup() %>% arrange(sort) %>% select(Nummer, `Kandidat/in`, Stadtteil = name, Prozent) cat("Hochburgen nach Kandidaten berechnet.\n") return(hochburgen_df) } #---- Haupt-Funktion ---- # # hole_wahldaten <- function() { # Hole und archiviere die Stimmbezirks-Daten; # erzeuge ein df mit den Stimmen nach Stadtteil. stimmbezirksdaten_df <<- lies_stimmbezirke(stimmbezirke_url) gezaehlt <<- stimmbezirksdaten_df %>% pull(meldungen_anz) %>% sum(.) archiviere(stimmbezirksdaten_df,paste0("daten/",wahl_name,"/")) kand_tabelle_df <<- berechne_kand_tabelle(stimmbezirksdaten_df) stadtteildaten_df <<- aggregiere_stadtteildaten(stimmbezirksdaten_df) ergänzt_df <<- berechne_ergänzt(stadtteildaten_df,top) hochburgen_df <<- berechne_hochburgen(stadtteildaten_df) # Neue Daten: Die Stimmdaten-zeilen, die ausgezählt sind. neue_daten <<- stimmbezirksdaten_df %>% # Filtere auf alle gezählten Stimmbezirke filter(meldungen_anz == 1) %>% # Ziehe die ab, die schon in den alten Daten gezählt waren anti_join(alte_daten %>% filter(meldungen_anz == 1) %>% select(name), by="name") alte_daten <<- stimmbezirksdaten_df # Aktualisiere die Karten (bzw. warne, wenn keine neuen da.) if (nrow(neue_daten)==0) { # teams_warning("Neue Stimmbezirk-Daten, aber keine neuen Ortsdaten?",title=wahl_name) cat("Frischer Zeitstempel, aber keine neu ausgezählten Stimmbezirke") } check = tryCatch( { # Die Ergebnistabellen mit allen Stimmen/Top-Kandidaten und Ergebnistabelle. aktualisiere_top(kand_tabelle_df,top) aktualisiere_tabelle_alle(kand_tabelle_df) }, warning = function(w) {teams_warning(w,title=paste0(wahl_name,": Grafiken A"))}, error = function(e) {teams_warning(e,title=paste0(wahl_name,": Grafiken A"))} ) ergebnistabelle_df <<- aktualisiere_ergebnistabelle(stadtteildaten_df) # Jetzt erst mal die Teams-Meldung absetzen. meldung_s <- paste0(nrow(neue_daten), " Stimmbezirke neu ausgezählt ", "(insgesamt ",gezaehlt, " von ",stimmbezirke_n,")<br>", "<br><strong>DERZEITIGER STAND: GANZE STADT</strong><br>", # Oberste Zeile der Ergebnistabelle ausgeben ergebnistabelle_df %>% head(1) %>% pull(Wahlbeteiligung), "<br>", ergebnistabelle_df %>% head(1) %>% pull(Ergebnis)) # Neue Stadtteile? Dann neue_stadtteile <- stadtteildaten_df %>% # Ausgezählte Stadtteile ausfiltern filter(meldungen_anz == meldungen_max) %>% # ...und schauen, ob da ein neuer dabei ist inner_join(neue_daten %>% # Neu gezählte Stimmbezirks-Meldung um Stadtteile ergänzen left_join(stimmbezirke_df, by="nr") %>% select(name = stadtteil) %>% unique(), by="name") # if(nrow(neue_stadtteile)>0) { for (s in neue_stadtteile %>% pull(nr)) { # Isoliere den Stadtteil, dessen Nummer wir gerade anschauen stadtteil <- stadtteildaten_df %>% filter(nr == s) meldung_s <- paste0(meldung_s, "<br><br><strong>Ausgezählter Stadtteil: ", stadtteil$name, "</strong><br>", ergebnistabelle_df %>% filter(nr == s) %>% pull(Wahlbeteiligung), "<br>", ergebnistabelle_df %>% filter(nr == s) %>% pull(Ergebnis)) } # Stadtteil neu ausgezählt? } meldung_s <- paste0(meldung_s,"<br><br>", generiere_socialmedia()) teams_meldung(meldung_s,title=wahl_name) check = tryCatch( { aktualisiere_karten(ergänzt_df) aktualisiere_hochburgen(hochburgen_df) }, warning = function(w) {teams_warning(w,title=paste0(wahl_name,": Grafiken B"))}, error = function(e) {teams_warning(e,title=paste0(wahl_name,": Grafiken B"))} ) }