diff --git a/.gitignore b/.gitignore index fae8299aeacc95d89cb63617ccaa5351800bf891..5cc2df59b7f1e078c4a262222b7765c6a85c236d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # Session Data files .RData +.DS_Store # User-specific files .Ruserdata @@ -37,3 +38,13 @@ vignettes/*.pdf # R Environment Variables .Renviron + +# This file +.gitignore + +# Test and sample data +/testdaten/ +/vorlagen/ +/daten/ +/png/ +/R/Vorbereitung/ diff --git a/R/aktualisiere_karten.R b/R/aktualisiere_karten.R index 28626b76e347cde275a6d73884451345badcdb84..406c3bbc5804856d22415b61676d8f428c00a35d 100644 --- a/R/aktualisiere_karten.R +++ b/R/aktualisiere_karten.R @@ -1,49 +1,443 @@ #' aktualisiere_karten.R +#' +#' Die Funktionen, um die Grafiken zu aktualisieren - und Hilfsfunktionen +#' +#---- Generiere den Fortschrittsbalken ---- -aktualisiere_karten <- function(wl_url = stimmbezirke_url) { - # Lies Ortsteil-Daten ein und vergleiche - neue_orts_df <- lies_gebiet(wl_url) %>% - aggregiere_stadtteile() %>% - mutate(quorum = ifelse(wahlberechtigt == 0, - 0, - ja / wahlberechtigt * 100)) %>% - mutate(status = ifelse(meldungen_anz == 0, - "KEINE DATEN", - paste0(ifelse(meldungen_anz < meldungen_max, - "TREND ",""), - ifelse(ja < nein, - "NEIN", - ifelse(quorum < 30, - "JA", - "JA QUORUM"))) - )) - alte_orts_df <- hole_letztes_df("daten/ortsteile") - # Datenstand identisch? Dann brich ab. - if(vergleiche_stand(alte_orts_df,neue_orts_df)) { - return(FALSE) + +generiere_auszählungsbalken <- function(anz = gezaehlt,max_s = stimmbezirke_n,ts = ts) { + fortschritt <- floor(anz/max_s*100) + annotate_str <- paste0("Ausgezählt sind ", + # Container Fake-Balken + "<span style='height:24px;display: flex;justify-content: space-around;align-items: flex-end; width: 100%;'>", + # Vordere Pufferzelle 70px + "<span style='width:70px; text-align:center;'>", + anz, + "</span>", + # dunkelblauer Balken + "<span style='width:", + fortschritt, + "%; background:#002747; height:16px;'></span>", + # grauer Balken + "<span style='width:", + 100-fortschritt, + "%; background:#CCC; height:16px;'></span>", + # Hintere Pufferzelle 5px + "<span style='width:5px;'></span>", + # Ende Fake-Balken + "</span>", + "<br>", + " von ",max_s, + " Stimmbezirken - ", + "<strong>Stand: ", + format.Date(ts, "%d.%m.%y, %H:%M Uhr"), + "</strong>" + ) + +} + +generiere_auszählung_nurtext <- function(anz = gezaehlt,max_s = stimmbezirke_n,ts = ts) { + fortschritt <- floor(anz/max_s*100) + annotate_str <- paste0("Ausgezählt: ", + anz, + " von ",max_s, + " Stimmbezirken - ", + "<strong>Stand: ", + format.Date(ts, "%d.%m.%y, %H:%M Uhr"), + "</strong>" + ) + +} + +#---- Hilfsfunktionen: Switcher generieren, Farben anpassen ---- +# Funktion gibt Schwarz zurück, wenn Farbe hell ist, und Weiß, wenn sie +# relativ dunkel ist. +font_colour <- function(colour) { + # convert color to hexadecimal format and extract RGB values + hex <- substr(colour, 2, 7) + r <- strtoi(paste0("0x",substr(hex, 1, 2))) + g <- strtoi(paste0("0x",substr(hex, 3, 4))) + b <- strtoi(paste0("0x",substr(hex, 5, 6))) + + # suggested by chatGPT: + # calculate the brightness of the color using the formula Y = 0.2126*R + 0.7152*G + 0.0722*B + brightness <- 0.2126 * r + 0.7152 * g + 0.0722 * b + # compare the brightness to a reference value and return the result + return(ifelse(brightness > 128,"#000000","#FFFFFF")) +} + +aufhellen <- function(Farbwert, heller = 128) { + #' Funktion gibt einen um 0x404040 aufgehellten Farbwert zurück + hex <- substr(Farbwert, 2, 7) + r <- strtoi(paste0("0x",substr(hex, 1, 2))) + heller + g <- strtoi(paste0("0x",substr(hex, 3, 4))) + heller + b <- strtoi(paste0("0x",substr(hex, 5, 6))) + heller + if (r > 255) r <- 255 + if (g > 255) g <- 255 + if (b > 255) b <- 255 + return(paste0("#",as.hexmode(r),as.hexmode(g),as.hexmode(b))) + +} + +# Generiere den Linktext für den Switcher +link_text <- function(id,id_colour,text) { + lt = paste0("<a target='_self' href='https://datawrapper.dwcdn.net/", + id,"' style='background:",id_colour, + "; padding:1px 3px; border-radius:3px; color:",font_colour(id_colour), + "; cursor:pointer;' rel='nofollow noopener noreferrer'>", + text,"</a> ") + return(lt) +} + +# Gibt einen String mit HTML/CSS zurück. +# Nutzt die Kandidierenden-Datei +generiere_switcher <- function(switcher_df,selected = 0) { + # Ist der Switcher auf 0 - der Stärkste-Kandidaten-Übersichtskarte? + if (selected == 0) { + text <- link_text(karte_sieger_id,"#F0F0FF","<strong>Sieger nach Stadtteil</strong>") } else { - # Zeitstempel holen - archiviere(neue_orts_df,"daten/ortsteile") - ts <- neue_orts_df %>% pull(zeitstempel) %>% last() - # Datentabelle übertragen - dw_data_to_chart(neue_orts_df,choropleth_id) - dw_data_to_chart(neue_orts_df,symbol_id) - dw_data_to_chart(neue_orts_df,tabelle_id) - # Anmerkungen aktualisieren - wahlberechtigt <- neue_orts_df %>% pull(wahlberechtigt) %>% sum() - # Prozentsatz ausgezählte Stimmen: abgerundet auf ganze Prozent - ausgezählt <- floor(wahlberechtigt / ffm_waehler *100) - annotate_str <- generiere_auszählungsbalken(ausgezählt, - anz = neue_orts_df %>% pull(meldungen_anz) %>% sum(), - max = neue_orts_df %>% pull(meldungen_max) %>% sum(), - ts = ts) - dw_edit_chart(symbol_id,annotate=annotate_str) - dw_edit_chart(choropleth_id,annotate=annotate_str) - dw_edit_chart(tabelle_id,annotate=annotate_str) - dw_publish_chart(symbol_id) - dw_publish_chart(choropleth_id) - dw_publish_chart(tabelle_id) - return(TRUE) + text <- link_text(karte_sieger_id,"#333333","Sieger nach Stadtteil") + } + for (i in 1:nrow(switcher_df)) { + if (i == selected) { + switcher_df$html[i] <- link_text(switcher_df$dw_id[i], + "#F0F0FF", + paste0("<strong>",switcher_df$Name[i],"</strong>")) + } else { + switcher_df$html[i] <- link_text(switcher_df$dw_id[i], + switcher_df$Farbwert[i], + switcher_df$Name[i]) + } + + } + return(paste0(text,paste0(switcher_df$html,collapse=""))) +} + +# HTML-Code für die Tooltipp-Mouseovers generieren +# Für alle Karten: Den jeweiligen Kandidaten in den Titel, +# orientieren an top (Anzahl der Top-Kandidaten), +# Bargraph-Code generieren. +# +# Die Mouseovers stehen in den Schlüsseln +# - visualize[["tooltip"]][["title"]] +# - visualize[["tooltip"]][["body"]] + +karten_titel_html <- function(kandidat_s) { + # TBD +} + +karten_body_html <- function(top = 5) { + # TBD + text <- "<p style='font-weight: 700;'>Ausgezählt: {{ meldungen_anz }} von {{ meldungen_max }} Wahllokalen{{ meldungen_max != meldungen_anz ? ' - Trendergebnis' : ''}}</p>" + # Generiere String mit allen Prozent-Variablen + prozent_s <- paste0(paste0("prozent",1:top),collapse=",") + # Tabellenöffnung + text <- text %>% paste0("<table style='width: 100%; border-spacing: 0.1em; border-collapse: collapse;'><thead>") + for (i in 1:top) { + text <- text %>% paste0("<tr><th>{{ kand",i, + " }}</th><td style='width: 120px; height=16px;'>", + "<div style='width: {{ ROUND(((prozent",i, + " / MAX(", + prozent_s, + ")) *100),1) }}%; background-color: {{ farbe",i, + " }}; padding-bottom: 5%; padding-top: 5%; border-radius: 5px;'></div></div></td>", + "<td style='padding-left: 3px; text-align: right; font-size: 0.8em;'>{{ FORMAT(prozent",i, + ", '0.0%') }}</td></tr>") + } + # Tabelle abbinden + text <- text %>% paste0( + "</thead></table>", + "<p>Wahlberechtigte: {{ FORMAT(wahlberechtigt, '0,0') }}, abgegebene Stimmen: {{ FORMAT(stimmen, '0,0') }}, Briefwahl: {{ FORMAT(stimmen_wahlschein, '0,0') }}, ungültig {{ FORMAT(ungueltig, '0,0') }}" + ) +} + + +# Schreibe die Switcher und die Farbtabellen in alle betroffenen Datawrapper-Karten +vorbereitung_alle_karten <- function() { + # Vorbereiten: df mit Kandidierenden und Datawrapper-ids + # Alle Datawrapper-IDs in einen Vektor extrahieren + id_df <- config_df %>% + filter(str_detect(name,"_kand[0-9]+")) %>% + mutate(Nummer = as.integer(str_extract(name,"[0-9]+"))) %>% + select(Nummer,dw_id = value)# + # Mach aus der Switcher-Tabelle eine globale Variable + switcher_df <<- kandidaten_df %>% + select(Nummer, Vorname, Name, Parteikürzel, Farbwert) %>% + left_join(id_df, by="Nummer") + text_pre <- "<strong>Wählen Sie eine Karte über die Felder:</strong><br>" + text_post <- "<br><br>Klick auf den Stadtteil zeigt, wer dort führt" + balken_text <- generiere_auszählungsbalken(gezaehlt,stimmbezirke_n,ts) + dw_intro=paste0(text_pre,generiere_switcher(switcher_df,0),text_post) + # Farbskala und Mouseover anpassen + metadata_chart <- dw_retrieve_chart_metadata(karte_sieger_id) + visualize <- metadata_chart[["content"]][["metadata"]][["visualize"]] + # Farbwerte für die Kandidierenden + # erst mal löschen + visualize[["colorscale"]][["map"]] <- NULL + visualize[["colorscale"]][["map"]] <- setNames(as.list(kandidaten_df$Farbwert), + kandidaten_df$Name) + # Karten-Tooltipp anpassen + # visualize[["tooltip"]][["title"]] <- karten_title_html(kandidat_s) + visualize[["tooltip"]][["body"]] <- karten_body_html(top) + + dw_edit_chart(karte_sieger_id,intro = dw_intro, annotate = balken_text, visualize = visualize) + # dw_data_to_chart() + # dw_publish_chart(karte_sieger_id) + + # Jetzt die n Choroplethkarten für alle Kandidaten + # Müssen als Kopie angelegt sein. + for (i in 1:nrow(switcher_df)) { + dw_intro <- paste0(text_pre,generiere_switcher(switcher_df,0),text_post) + titel_s <- paste0(switcher_df$Vorname[i]," ", + switcher_df$Name[i]," (", + switcher_df$Parteikürzel[i],") - ", + "Ergebnis nach Stadtteil") + kandidat_s <- paste0(switcher_df$name[i], + " (", + switcher_df$Parteikürzel[i]) + metadata_chart <- dw_retrieve_chart_metadata(switcher_df$dw_id[i]) + visualize <- metadata_chart[["content"]][["metadata"]][["visualize"]] + # Zwei Farben: Parteifarbe bei Pos. 1, aufgehellte Parteifarbe + # (zu RGB jeweils 0x40 addiert) bei Pos. 0 + visualize[["colorscale"]][["colors"]][[2]]$color <- switcher_df$Farbwert[i] + visualize[["colorscale"]][["colors"]][[2]]$position <- 1 + visualize[["colorscale"]][["colors"]][[1]]$color <- aufhellen(switcher_df$Farbwert[i]) + visualize[["colorscale"]][["colors"]][[1]]$position <- 0 + # Karten-Tooltipp anpassen + # visualize[["tooltip"]][["title"]] <- karten_title_html(kandidat_s) + visualize[["tooltip"]][["body"]] <- karten_body_html(top) + dw_edit_chart(switcher_df$dw_id[i], + title = titel_s, + intro = dw_intro, + visualize = visualize, + annotate = balken_text) + # dw_data_to_chart() + # dw_publish_chart(switcher_df$dw_id) + } +} + +#---- Generiere und pushe die Grafiken für Social Media +generiere_socialmedia <- function() { + # Fairly straightforward. Rufe zwei Datawrapper-Karten über die API auf, + # generiere aus ihnen PNGs, benenne sie mit Zeitstempel, schiebe die auf + # den Bucket und gib einen Text mit Link zurück. + # + # Die beiden Karten sind: + # - die Aufmacher-Grafik = top_id + # - die nüchterne Balkengrafik mit allen 20 Kandidat:innen S9BbQ + # + # Die Funktion aktualisiert KEINE Daten, sondern nimmt das, was gerade im + # Datawrapper ist. Das ggf extra mit dw_data_to_chart(meine_df,social1_id,parse_dates =TRUE) + # + # Setzt gültigen Zeitstempel ts voraus! + + # Erste Grafik ist sowieso aktuell und wird nur anders exportiert. + # dw_data_to_chart(tag_df,social1_id,parse_dates =TRUE) + social1_png <- dw_export_chart(social1_id,type = "png",unit="px",mode="rgb", scale = 1, + width = 640, height = 640, plain = TRUE, transparent = T) + social1_fname <- paste0("png/social1_",format.Date(ts,"%Y-%m-%d--%H%Mh"),".png") + # Zweite Grafik muss aktualisiert und vermetadatet werden + # Metadaten anpassen: Farbcodes für Parteien + metadata_chart <- dw_retrieve_chart_metadata(social2_id) + # Save the visualise path + visualize <- metadata_chart[["content"]][["metadata"]][["visualize"]] + visualize[["color-category"]][["map"]] <- + setNames(as.list(kandidaten_df$Farbwert), + paste0(kandidaten_df$Name," (", + kandidaten_df$Parteikürzel,")")) + dw_edit_chart(chart_id = social2_id, visualize = visualize) + dw_data_to_chart(kand_tabelle_df, chart_id = social2_id) + social2_png <- dw_export_chart(social2_id,type = "png",unit="px",mode="rgb", scale = 1, + width = 640, height = 640, plain = TRUE, transparent = T) + social2_fname <- paste0("png/social2_",format.Date(ts,"%Y-%m-%d--%H%Mh"),".png") + # PNG-Dateien generieren... + if (!dir.exists("png")) {dir.create("png")} + magick::image_write(social1_png,social1_fname) + magick::image_write(social2_png,social2_fname) + #...und auf den Bucket schieben. + if (SERVER) { + system(paste0('gsutil -h "Cache-Control:no-cache, max_age=0" ', + 'cp ',social1_fname,' gs://d.data.gcp.cloud.hr.de/', social1_fname)) + system(paste0('gsutil -h "Cache-Control:no-cache, max_age=0" ', + 'cp ',social2_fname,' gs://d.data.gcp.cloud.hr.de/', social2_fname)) } -} \ No newline at end of file + linktext <- paste0("<a href='https://d.data.gcp.cloud.hr.de/",social1_fname, + "'>Download Social-Grafik 1 (5 stärkste)</a><br/>", + "<a href='https://d.data.gcp.cloud.hr.de/",social2_fname, + "'>Download Social-Grafik 2 (alle Stimmen)</a><br/>") + return(linktext) +} + +#---- Datawrapper-Grafiken generieren ---- +aktualisiere_top <- function(kand_tabelle_df,top=5) { + daten_df <- kand_tabelle_df %>% + arrange(desc(Prozent)) %>% + select(`Kandidat/in`,Stimmenanteil = Prozent) %>% + head(top) + # Daten pushen + dw_data_to_chart(daten_df,chart_id = top_id) + # Intro_Text nicht anpassen. + # Balken reinrendern + balken_text <- generiere_auszählungsbalken(gezaehlt,stimmbezirke_n,ts) + # Metadaten anpassen: Farbcodes für Parteien + metadata_chart <- dw_retrieve_chart_metadata(top_id) + # Save the visualise path + visualize <- metadata_chart[["content"]][["metadata"]][["visualize"]] + # Der Schlüssel liegt unter custom-colors als Liste + visualize[["custom-colors"]] <- + setNames(as.list(kandidaten_df$Farbwert), + paste0(kandidaten_df$Name," (", + kandidaten_df$Parteikürzel,")")) + dw_edit_chart(chart_id = top_id,annotate = balken_text, visualize=visualize) + dw <- dw_publish_chart(chart_id = top_id) +} + +aktualisiere_tabelle_alle <- function(kand_tabelle_df) { + dw_data_to_chart(kand_tabelle_df, chart_id = tabelle_alle_id) + balken_text <- generiere_auszählung_nurtext(gezaehlt,stimmbezirke_n,ts) + # Metadaten anpassen: Farbcodes für Parteien + metadata_chart <- dw_retrieve_chart_metadata(tabelle_alle_id) + # Save the visualise path + visualize <- metadata_chart[["content"]][["metadata"]][["visualize"]] + visualize[["columns"]][["Prozent"]][["customColorBarBackground"]] <- NULL + visualize[["columns"]][["Stimmen"]][["customColorBarBackground"]] <- + setNames(as.list(kandidaten_df$Farbwert), + kandidaten_df$Nummer) + # Irrtümlich waren die Werte auch noch in visualize[["custom-color"]] gespeichert. + visualize[["custom-colors"]] <- NULL + visualize[["color-category"]] <- NULL + dw_edit_chart(chart_id = tabelle_alle_id, annotate = balken_text, visualize = visualize) + dw_publish_chart(chart_id = tabelle_alle_id) +} + +aktualisiere_karten <- function(ergänzt_df) { + # Als erstes die Übersichtskarte + cat("Aktualisiere Karten\n") + # Die noch überhaupt nicht gezählten Bezirke ausfiltern + ergänzt_f_df <- ergänzt_df %>% filter(meldungen_anz > 0) + balken_text = generiere_auszählungsbalken(gezaehlt,stimmbezirke_n,ts) + dw_edit_chart(chart_id = karte_sieger_id, annotate = balken_text) + dw_data_to_chart(ergänzt_f_df,chart_id = karte_sieger_id) + dw <- dw_publish_chart(karte_sieger_id) + # Jetzt die Choropleth-Karten für alle Kandidierenden + for (i in 1:nrow(switcher_df)) { + dw_edit_chart(chart_id=switcher_df$dw_id[i],annotate = balken_text) + dw_data_to_chart(ergänzt_f_df, chart_id = switcher_df$dw_id[i]) + dw <- dw_publish_chart(switcher_df$dw_id[i]) + } + cat("Karten neu publiziert\n") +} + +aktualisiere_hochburgen <- function(hochburgen_df) { + # Das ist ziemlich geradeheraus. + dw_data_to_chart(hochburgen_df, chart_id = hochburgen_id) + balken_text <- generiere_auszählung_nurtext(gezaehlt,stimmbezirke_n,ts) + # Metadaten anpassen: Farbcodes für Parteien + metadata_chart <- dw_retrieve_chart_metadata(hochburgen_id) + # Save the visualise path + visualize <- metadata_chart[["content"]][["metadata"]][["visualize"]] + # Die Farben für die Kandidaten werden in dieser Tabelle nur für die Balkengrafiken + # in der Spalte "Prozent" benötigt und richten sich nach der Nummer. + visualize[["columns"]][["Prozent"]][["customColorBarBackground"]] <- + setNames(as.list(kandidaten_df$Farbwert), + kandidaten_df$Nummer) + # Irrtümlich waren die Werte auch noch in visualize[["custom-color"]] gespeichert. + visualize[["custom-colors"]] <- NULL + dw_edit_chart(chart_id = hochburgen_id, annotate = balken_text, visualize = visualize) + dw_publish_chart(chart_id = hochburgen_id) + cat("Hochburgen-Grafik neu publiziert\n") +} + +aktualisiere_ergebnistabelle <- function(stadtteildaten_df) { + # Nr des Stadtteils, Stadtteil, Wahlbeteiligung (Info), Ergebnis + # Wahlbeteiligung und Ergebnis sind jeweils HTML-Text mit den Daten + # Unleserlich, aber funktional + e_tmp_df <- stadtteildaten_df %>% + select(nr,name,meldungen_anz:ncol(.)) %>% + # Nach Stadtteil sortieren + arrange(name) %>% + mutate(sort = row_number()) + # Nochmal ansetzen, um als erste Zeile das Gesamtergebnis einzusetzen + ergebnistabelle_df <- e_tmp_df %>% summarize(nr = 0, sort = 0, + name = "GESAMTERGEBNIS", + across(meldungen_anz:ncol(.), ~sum(.,na.r =FALSE))) %>% + bind_rows(e_tmp_df) %>% + # Mit den Kandidaten-Namen anreichern + # 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]+"))) %>% + left_join(kandidaten_df %>% select (Nummer, Vorname, Name, Parteikürzel), + by = "Nummer") %>% + mutate(`Kandidat/in` = paste0(Name," (",Parteikürzel,")")) %>% + # Kandidaten-Tabelle wieder zurückpivotieren + select(-Vorname, -Name, -Parteikürzel, -Nummer) %>% + # Nach Stadtteil gruppieren + group_by(sort,nr,name) %>% + # Zusätzliche Variable: Stadtteil gezählt oder TREND? + mutate(trend = meldungen_anz < meldungen_max) %>% + # Big bad summary - jeweils Daten aus den Spalten generieren + summarize(Stadtteil = paste0("<strong>",first(name), + "</strong><br><br>", + # TREND oder ERGEBNIS? + if_else(first(trend), + paste0("TREND: ", + first(meldungen_anz)," von ", + first(meldungen_max)," Stimmbezirken ausgezählt"), + #...oder alles ausgezählt? + paste0("Alle ", + first(meldungen_max), + " Stimmbezirke ausgezählt"))), + Wahlbeteiligung = paste0("Wahlberechtigt: ", + # Wenn noch nicht ausgezählt, leer lassen + if_else(first(trend),"", + first(wahlberechtigt) %>% + format(big.mark = ".", decimal.mark =",")), + "<br>", + "abg. Stimmen: ",first(stimmen) %>% format(big.mark = ".", decimal.mark =","), + " (", + # Nicht gezählte Bezirke haben 0 Wahlberechtigte + if_else(first(trend),"--", + (first(stimmen)/first(wahlberechtigt) *100) %>% + round(digits=1) %>% format(decimal.mark=",", nsmall = 1)), + "%)<br>", + "davon Briefwahl: ", + first(stimmen_wahlschein) %>% format(big.mark = ".", decimal.mark =","), + " (", + # Falls noch nicht alles ausgezählt, keinen Prozentwert angeben + if_else(first(trend), + "--", + (first(stimmen_wahlschein) / first(stimmen) * 100) %>% + round(digits = 1) %>% format(decimal.mark=",", nsmall = 1)), + "%)<br>", + "<br>Ungültig: ", + first(ungueltig) %>% format(big.mark = ".", decimal.mark =","), + "<br>Gültige Stimmen: ", + first(gueltig) %>% format(big.mark = ".", decimal.mark =",")), + # Hier geschachtelte paste0-Aufrufe: + # Der innere baut einen Vektor mit allen Kandidaten plus Ergebnissen + # Der äußere fügt diesen Vektor zu einem String zusammen (getrennt durch <br>) + Ergebnis = paste0(paste0("<strong>",`Kandidat/in`,"</strong>: ", + Stimmen %>% format(big.mark=".",decimal.mark=","), + " (", + Prozent %>% round(1) %>% format(decimal.mark=",", nsmall=1),"%)"), + collapse="<br>") + ) %>% + ungroup() %>% + arrange(sort) %>% + select(-name,-sort) + dw_data_to_chart(ergebnistabelle_df %>% select(-nr), chart_id = tabelle_stadtteile_id) + # Trendergebnis? Schreibe "Trend" oder "Endergebnis" in den Titel + gezählt <- e_tmp_df %>% pull(meldungen_anz) %>% sum(.) + stimmbezirke_n <- e_tmp_df %>% pull(meldungen_max) %>% sum(.) + ts <- stadtteildaten_df %>% pull(zeitstempel) %>% first() + titel_s <- paste0(ifelse(gezählt < stimmbezirke_n,"TREND: ",""), + "Ergebnisse nach Stadtteil") + dw_edit_chart(chart_id = tabelle_stadtteile_id,title = titel_s, + annotate=generiere_auszählung_nurtext(gezählt,stimmbezirke_n,ts)) + dw_publish_chart(tabelle_stadtteile_id) + cat("Ergebnistabelle nach Stadtteil publiziert\n") + return(ergebnistabelle_df) +} diff --git a/R/generiere_testdaten.R b/R/generiere_testdaten.R deleted file mode 100644 index 3eed9587e7a82d8e51b2252b2ce46d563e810348..0000000000000000000000000000000000000000 --- a/R/generiere_testdaten.R +++ /dev/null @@ -1,140 +0,0 @@ -#' generiere_testdaten.R -#' -#' Macht aus den Templates für Ortsteil- und Wahllokal-Ergebnisse -#' jeweils eine Serie von fiktiven Livedaten, um das Befüllen der -#' Grafiken testen zu können. -#' - -require(tidyr) -require(dplyr) -require(readr) - -# Alles weg, was noch im Speicher rumliegt -rm(list=ls()) - -source("R/lies_aktuellen_stand.R") - -#---- Funktion zum Testdaten-Löschen ---- -lösche_testdaten <- function(){ - q <- tolower(readline(prompt = "Testdaten löschen - sicher? ")) - if (!(q %in% c("j","y","ja"))) { return() } - # Datenarchiv weg - if (file.exists("daten/fom_df.rds")){ - file.remove("daten/fom_df.rds") - } - # Testdaten - testdaten_files <- list.files("testdaten", full.names=TRUE) - for (f in testdaten_files) { - # Grausam, I know. - if (str_detect(f,"ortsteile[0-9]+\\.csv") | - str_detect(f,"wahllokale[0-9]+\\.csv")) { - file.remove(f) - } - } -} - -# Vorlagen laden -vorlage_wahllokale_df <- read_delim("testdaten/Open-Data-06412000-Buergerentscheid-zur-Abwahl-des-Oberbuergermeisters-der-Stadt-Frankfurt-am-Main_-Herrn-Peter-Feldmann-Stimmbezirk.csv", - delim = ";", escape_double = FALSE, - locale = locale(date_names = "de", - decimal_mark = ",", - grouping_mark = "."), - trim_ws = TRUE) - -wahllokale_max <- sum(vorlage_wahllokale_df$`max-schnellmeldungen`) - -# Konstanten für die Simulation - werden jeweils um bis zu +/-25% variiert -c_wahlberechtigt = 510000 / wahllokale_max # Gleich große Wahlbezirke -c_wahlbeteiligung = 0.3 # Wahlbeteiligung um 30%, wird im Lauf der "Wahl" erhöht (kleinere WL sind schneller ausgezählt) -c_wahlschein = 0.25 # 25% Briefwähler -c_nv = 0.05 # 0,5% wählen "spontan" und sind nicht verzeichnet (nv) im Wählerverzeichnis -c_ungültig = 0.01 # 1% Ungültige -c_nein = 0.15 # unter den gültigen: 85% Ja-Stimmen (Varianz also von ca 81-89%) - -variiere <- function(x = 1) { - # Variiert den übergebenen Wert zufällig um -25% bis +25%: - # Zufallswerte zwischen 0,75 und 1,25 erstellen und multiplizieren - # - # Die Length-Funktion ist wichtig - sonst erstellt runif() nur einen - # Zufallswert, mit dem alle Werte von x multipliziert werden. - return(floor(x * (runif(length(x),0.75,1.25)))) -} - - - -i = 1 -# Schleife für die Wahllokale: Solange noch nicht alle "ausgezählt" sind... -while(sum(vorlage_wahllokale_df$`anz-schnellmeldungen`) < wahllokale_max) { - # ...splitte das df in die gemeldeten (meldungen_anz == 1) und nicht gemeldeten Zeilen - tmp_gemeldet_df <- vorlage_wahllokale_df %>% filter(`anz-schnellmeldungen` == 1) - # Die Variable rand wird als Anteil von 20 Meldungen an debn noch offenen Wahllokale berechnet - rand <- 20 / (nrow(vorlage_wahllokale_df) - nrow(tmp_gemeldet_df)) - tmp_sample_df <- vorlage_wahllokale_df %>% - filter(`anz-schnellmeldungen` == 0) %>% - # Bei den noch nicht ausgefüllten "Meldungen" mit einer Wahrscheinlichkeit - # von rand in die Gruppe sortieren, die neu "gemeldet" wird - mutate(sample = (runif(nrow(.)) < rand)) - tmp_offen_df <- tmp_sample_df %>% - filter(sample == 0) %>% - # sample-Variable wieder raus - select(-sample) - tmp_neu_df <- tmp_sample_df %>% - filter(sample == 1) %>% - select(-sample) %>% - # Alle als gemeldet markieren - mutate(`anz-schnellmeldungen` = 1) %>% - # Und jetzt der Reihe nach (weil die Werte z.T. aufeinander aufbauen) - # Wahlberechtigte - mutate(A = floor(c_wahlberechtigt * runif(nrow(.),0.75,1.25))) %>% - # Wahlschein - mutate(A2 = floor(A * c_wahlschein * runif(nrow(.),0.75,1.25))) %>% - # Nicht verzeichnet - mutate(A3 = floor(A * c_nv * runif(nrow(.),0.75,1.25))) %>% - # Regulär Wahlberechtigte (ohne Wahlschein oder nv) - mutate(A1 = A - A2 - A3) %>% - # Abgegebene Stimmen - mutate(B = floor(A * c_wahlbeteiligung * runif(nrow(.),0.75,1.25))) %>% - # davon mit Wahlschein - mutate(B1 = floor(B * c_wahlschein * runif(nrow(.),0.75,1.25))) %>% - # davon ungültig - mutate(C = floor(B * c_ungültig * runif(nrow(.),0.75,1.25))) %>% - # gültig - mutate(D = B - C) %>% - # davon ja - mutate(D2 = floor(D * c_nein *runif(nrow(.),0.75,1.25))) %>% - mutate(D1 = D - D2) - # Kurze Statusmeldung - cat("Neu gemeldet:",nrow(tmp_neu_df),"noch offen:",nrow(tmp_offen_df)) - # Phew. Aktualisierte Testdatei zusammenführen und anlegen. - vorlage_wahllokale_df <- tmp_gemeldet_df %>% - bind_rows(tmp_neu_df) %>% - bind_rows(tmp_offen_df) %>% - # wieder in die Reihenfolge nach Wahllokal-Nummer - arrange(`gebiet-nr`) - - write_csv2(vorlage_wahllokale_df, - paste0("testdaten/wahllokale", - sprintf("%02i",i), - ".csv"), - escape = "backslash") - # Generiere die passende Ortsteil-Meldung - # Geht aus irgeneindem Grund nicht, aber wir brauchens ja auch nicht. - # ortsteile_df <- zuordnung_wahllokale_df %>% - # select(`gebiet-name` = name,ortsteilnr) %>% - # left_join(vorlage_wahllokale_df,by="gebiet-name") %>% - # # Zuordnung der Wahllokale - # group_by(ortsteilnr) %>% - # # Das crasht - WTF??? - # summarize(across(7:18, ~ sum(.,na.rm = T))) %>% - # left_join(stadtteile_df %>% select(ortsteilnr = nr,name),by="ortsteilnr") %>% - # rename(`gebiet-nr` = ortsteilnr) %>% - # mutate(`gebiet-name` = name) %>% - # select(-ortsteilnr) - - i <- i+1 - # Wahlbeteiligung schrittweise ein wenig anheben - um zu simulieren, - # dass "kleinere" Wahllokale zuerst ausgezählt werden - c_wahlbeteiligung <- c_wahlbeteiligung + 0.002 -} - - diff --git a/R/its_alive.R b/R/its_alive.R new file mode 100644 index 0000000000000000000000000000000000000000..750c52b4049059d72c746cdd1602abae866eecb4 --- /dev/null +++ b/R/its_alive.R @@ -0,0 +1,41 @@ +# its_alive.R - Watchdog-Skript für update_all.R +# +# Wird per CRON-Job aufgerufen: Wenn das letzte Update der Log-Datei "wahl.log" +# länger als x Minuten zurückliegt, löst das Skript einen Alarm über Teams aus. + +# Mehr als die Datumsfunktionen brauchen wir nicht +library(lubridate) +library(this.path) + +# Das Projektverzeichnis "obwahl" als Arbeitsverzeichnis wählen +# Aktuelles Verzeichnis als workdir +setwd(this.path::this.dir()) +# Aus dem R-Verzeichnis eine Ebene rauf +setwd("..") + +# Teams-Funktionen einbinden +source("R/messaging.R") + +# Maximales Alter in Sekunden? +max_alter = 120 + +# Startzeit festhalten +ts = now() + +# Gibt es überhaupt eine Logdatei? + +if (file.exists("obwahl.log")) { + metadaten <- file.info("obwahl.log") + # Berechne Alter der Logdatei in Sekunden + alter = as.integer(difftime(ts,metadaten$mtime,units="secs")) + if (alter > max_alter) + { + cat("WATCHDOG its_alive.R: obwahl.log seit ",alter," Sekunden unverändert") + cat("Benenne obwahl.log um in obwahl_crash.log") + file.rename("obwahl.log","obwahl_crash.log") + teams_error("PROGRAMM STEHEN GEBLIEBEN? obwahl.log ist seit ",alter," Sekunden unverändert") + } +} else { + # Tue nichts. + cat("its_alive.R: obwahl.log im Arbeitsverzeichnis",getwd(),"nicht gefunden") +} diff --git a/R/lies_aktuellen_stand.R b/R/lies_aktuellen_stand.R index 70b7bca9985413a10982c16b486276077c7c0b19..2ffc5fcd4cadd952b90337bfabecd094bd80fb06 100644 --- a/R/lies_aktuellen_stand.R +++ b/R/lies_aktuellen_stand.R @@ -1,56 +1,40 @@ +# 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. -#---- Vorbereitung ---- -# Statische Daten einlesen -# (das später durch ein schnelleres .rda ersetzen) +#---- Hilfsfunktionen ---- -# Enthält drei Datensätze: -# - opendata_wahllokale_df mit der Liste aller Stimmwahlbezirke nach Wahllokal -# - statteile_df: Stadtteil mit Namen und laufender Nummer, Geokoordinaten, Ergebnissen 2018 -# - zuordnung_stimmbezirke: Stimmbezirk-Nummer (als int und String) -> Stadtteilnr. - -load ("index/index.rda") - -# Konfiguration auslesen und in Variablen schreiben -config_df <- read_csv("index/config.csv") -for (i in c(1:nrow(config_df))) { - # Erzeuge neue Variablen mit den Namen und Werten aus der CSV - assign(config_df$name[i], - # Kann man den Wert auch als Zahl lesen? - # Fieses Regex sucht nach reiner Zahl oder Kommawerten. - # Keine Exponentialschreibweise! - ifelse(grepl("^[0-9]*\\.*[0-9]+$",config_df$value[i]), - # Ist eine Zahl - wandle um - as.numeric(config_df$value[i]), - # Keine Zahl - behalte den String - config_df$value[i])) -} - - -#---- Daten ins Archiv schreiben oder daraus lesen 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) } - write_csv(df, - paste0(a_directory,"/", - # Zeitstempel isolieren und alle Doppelpunkte - # durch Bindestriche ersetzen - str_replace_all(df %>% pull(zeitstempel) %>% last(), - "\\:","_"), - ".csv")) + 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 @@ -67,11 +51,56 @@ hole_letztes_df <- function(a_directory = "daten/stimmbezirke") { } } +# 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 ---- -lies_gebiet <- function(stand_url = stimmbezirke_url) { - ts <- now() - # Versuch Daten zu lesen - und gib ggf. Warnung oder Fehler zurück + +# 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, @@ -80,7 +109,10 @@ lies_gebiet <- function(stand_url = stimmbezirke_url) { grouping_mark = "."), trim_ws = TRUE) %>% # Spalten umbenennen, Zeitstempel-Spalte einfügen - mutate(zeitstempel=ts) %>% + 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`, @@ -96,57 +128,243 @@ lies_gebiet <- function(stand_url = stimmbezirke_url) { stimmen_wahlschein = B1, ungueltig = C, gueltig = D, - ja = D1, - nein = D2) + # neu: alle Zeilen mit Stimmen (D1..Dn) + starts_with("D")) + }, - warning = function(w) {teams_warning(w,title="Feldmann: Datenakquise")}, - error = function(e) {teams_warning(e,title="Feldmann: Datenakquise")}) - # Spalten umbenennen, + warning = function(w) {teams_warning(w,title="OB-Wahl: Datenakquise")}, + error = function(e) {teams_warning(e,title="OB-Wahl: Datenakquise")}) return(stand_df) } - -# Sind die beiden df abgesehen vom Zeitstempel identisch? -# Funktion vergleicht die numerischen Werte - Spalte für Spalte. -vergleiche_stand <- function(alt_df, neu_df) { - 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) -} - -#' Liest Stimmbezirke, gibt nach Ortsteil aggregierte Daten zurück -#' (hier: kein Sicherheitscheck) -aggregiere_stadtteile <- function(stimmbezirke_df) { - ortsteile_df <- stimmbezirke_df %>% - left_join(zuordnung_stimmbezirke_df,by=c("nr","name")) %>% - group_by(ortsteilnr) %>% +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), - across(meldungen_anz:nein, ~ sum(.,na.rm = T))) %>% - rename(nr = ortsteilnr) %>% - # Stadtteilnamen, 2018er Ergebnisse, Geokoordinaten dazuholen + 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") %>% - # Nach Ortsteil sortieren - arrange(nr) %>% # Wichtige Daten für bessere Lesbarkeit nach vorn relocate(zeitstempel,nr,name,lon,lat) # Sicherheitscheck: Warnen, wenn nicht alle Ortsteile zugeordnet - if (nrow(ortsteile_df) != nrow(stadtteile_df)) teams_warnung("Nicht alle Ortsteile zugeordnet") - if (nrow(zuordnung_stimmbezirke_df) != length(unique(stimmbezirke_df$nr))) teams_warnung("Nicht alle Stimmbezirke zugeordnet") - return(ortsteile_df) + 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) } -lies_stadtteil_direkt <- function(stand_url = ortsteile_url) { - neu_df <- lies_gebiet(stand_url) %>% - # nr bei Ortsteil-Daten leer/ignorieren - select(!nr) %>% - # Stadtteilnr., Geodaten und Feldmann-2018-Daten reinholen: - left_join(stadtteile_df, by=c("name")) %>% - mutate(trend = (meldungen_anz < meldungen_max), - quorum_erreicht = (ja >= (wahlberechtigt * 0.3))) - return(neu_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"))} + ) +} \ No newline at end of file diff --git a/R/lies_konfiguration.R b/R/lies_konfiguration.R new file mode 100644 index 0000000000000000000000000000000000000000..b36523afcf71e436a4663c4bce56ab973a57ac7a --- /dev/null +++ b/R/lies_konfiguration.R @@ -0,0 +1,87 @@ +#---- Vorbereitung ---- +# Statische Daten einlesen +# (das später durch ein schnelleres .rda ersetzen) + +# Enthält drei Datensätze: +# - opendata_wahllokale_df mit der Liste aller Stimmwahlbezirke nach Wahllokal +# - statteile_df: Stadtteil mit Namen und laufender Nummer, Geokoordinaten, Ergebnissen 2018 +# - zuordnung_stimmbezirke: Stimmbezirk-Nummer (als int und String) -> Stadtteilnr. + +# load ("index/index.rda") + + +# Konfiguration auslesen und in Variablen schreiben +# +# Generiert für jede Zeile die genannte Variable mit dem Wert value +# +# Derzeit erwartet das Programm: +# - wahl_name - Name der Wahl ("obwahl_ffm", "obwahl_kassel_stichwahl" etc.) +# - stimmbezirke_url - URL auf Ergebnisdaten +# - kandidaten_fname - Dateiname der Kandidierenden-Liste (s.u.) +# - datawrapper_fname - Dateiname für die Datawrapper-Verweis-Datei +# - stadtteile_fname +# - zuordnung_fname +# - startdatum - wann beginne ich zu arbeiten? +# - wahlberechtigt - Zahl der Wahlberechtigen (kommt Sonntag) +# - briefwahl - Zahl der Briefwahlstimmen (kommt Sonntag) + +if (TEST) { + config_df <- read_csv("index/config_test.csv") +} else { + config_df <- read_csv("index/config.csv") +} +for (i in c(1:nrow(config_df))) { + # Erzeuge neue Variablen mit den Namen und Werten aus der CSV + assign(config_df$name[i], + # Kann man den Wert auch als Zahl lesen? + # Fieses Regex sucht nach reiner Zahl oder Kommawerten. + # Keine Exponentialschreibweise! + ifelse(grepl("^[0-9]*\\.*[0-9]+$",config_df$value[i]), + # Ist eine Zahl - wandle um + as.numeric(config_df$value[i]), + # Keine Zahl - behalte den String + config_df$value[i])) +} + +lies_daten <- function(fname) { + if (toupper(str_extract(fname,"(?<=\\.)[A-zA-Z]+$")) %in% c("XLS","XLSX")) { + # Ist offensichtlich eine Excel-Datei. Lies das erste Sheet. + return(read.xlsx(fname)) + } else { + # Geh von einer CSV-Datei aus. + first_line <- readLines(fname, n = 1) + commas <- str_split(first_line, ",") %>% unlist() %>% length() + semicolons <- str_split(first_line, ";") %>% unlist() %>% length() + if (commas > semicolons) { + return(read_csv(fname)) + } else { + # Glaube an das Gute im Menschen: Erwarte UTF-8 und deutsche Kommasetzung. + return(read_csv2(fname, + locale = locale( + date_names = "de", + date_format = "%Y-%m-%d", + time_format = "%H:%M:%S", + decimal_mark = ",", + grouping_mark = ".", + encoding = "UTF-8", + asciify = FALSE + ))) + } + + } +} +# Stadtteilname und -nr; Geokoordinaten. Später: Ergebnisse +# der 2021er Kommunalwahl +stadtteile_df <- lies_daten(paste0("index/",wahl_name,"/",stadtteile_fname)) +# Zuordnung Stimmbezirk (Wahllokale und Briefwahl-Bezirke) -> Stadtteil +stimmbezirke_df <- lies_daten(paste0("index/",wahl_name,"/",zuordnung_fname)) %>% + # Nummer-Spalten in numerische INdizes umwandeln + mutate(ortsteilnr = as.integer(ortsteilnr)) %>% + mutate(nr = as.integer(nr)) %>% + left_join(stadtteile_df %>% select(nr=1,stadtteil=2), by=c("ortsteilnr"="nr")) +# Kandidat:innen-Index +kandidaten_df <- lies_daten(paste0("index/",wahl_name,"/",kandidaten_fname)) + +# Läufst du auf dem Server? +SERVER <- dir.exists("/home/jan_eggers_hr_de") + diff --git a/R/main.R b/R/main.R new file mode 100644 index 0000000000000000000000000000000000000000..6ddc7f1dc1e503b82b444b007c20fada3aff5b2b --- /dev/null +++ b/R/main.R @@ -0,0 +1,117 @@ +library(pacman) + +# Laden und ggf. installieren +p_load(this.path) +p_load(readr) +p_load(lubridate) +p_load(tidyr) +p_load(stringr) +p_load(dplyr) +p_load(DatawRappr) +p_load(curl) +p_load(magick) +p_load(openxlsx) +p_load(R.utils) + +rm(list=ls()) + +TEST = TRUE +DO_PREPARE_MAPS = TRUE + + + +# Aktuelles Verzeichnis als workdir +setwd(this.path::this.dir()) +# Aus dem R-Verzeichnis eine Ebene rauf +setwd("..") + +# Logfile anlegen, wenn kein Test +if (!TEST) { + logfile = file("obwahl.log") + sink(logfile, append=T) + sink(logfile, append=T, type="message") + +} + +# Messaging-Funktionen einbinden +source("R/messaging.R") + + +# Hole die Konfiguration und die Index-Daten +check = tryCatch( + { + source("R/lies_konfiguration.R") + }, + warning = function(w) {teams_warning(w,title="OBWAHL: Warnung beim Lesen der Konfigurationsdatei")}, + error = function(e) {teams_error(e,title="OBWAHL: Konfigurationsdatei nicht gelesen!")}) + +# Funktionen einbinden +# Das könnte man auch alles hier in diese Datei schreiben, aber ist es übersichtlicher. +source("R/lies_aktuellen_stand.R") +source("R/aktualisiere_karten.R") + +#---- MAIN ---- +# Vorbereitung +gezaehlt <- 0 # Ausgezählte Stimmbezirke +ts <- as_datetime(startdatum) # ts, Zeitstempel, der letzten gelesenen Daten +stimmbezirke_n <- nrow(stimmbezirke_df) # Anzahl aller Stimmbezirke bei der Wahl +alte_daten <- lies_stimmbezirke(stimmbezirke_url) # Leere Stimmbezirke +# Grafiken einrichten: Farbwerte und Switcher für die Karten +# Richtet auch die globale Variable switcher ein, deshalb brauchen wir sie + + +if (DO_PREPARE_MAPS) { + check = tryCatch( + vorbereitung_alle_karten(), + warning = function(w) {teams_warning(w,title=paste0(wahl_name,": Vorbereitung"))}, + error = function(e) {teams_warning(e,title=paste0(wahl_name,": Vorbereitung"))} + ) +} else { + # Alle Datawrapper-IDs in einen Vektor extrahieren + id_df <- config_df %>% + filter(str_detect(name,"_kand[0-9]+")) %>% + mutate(Nummer = as.integer(str_extract(name,"[0-9]+"))) %>% + select(Nummer,dw_id = value)# + # Mach aus der Switcher-Tabelle eine globale Variable + # Nur die Globale switcher_df definieren (mit den IDs der DW-Karten zum Kandidaten/Farbwert) + switcher_df <- kandidaten_df %>% + select(Nummer, Vorname, Name, Parteikürzel, Farbwert) %>% + left_join(id_df, by="Nummer") +} + +# Schleife. +# Arbeitet so lange, bis alle Wahlbezirke ausgezählt sind. +# Als erstes immer einmal durchlaufen lassen. +while (gezaehlt < stimmbezirke_n) { + check = tryCatch( + { # Zeitstempel der Daten holen + ts_daten <- check_for_timestamp(stimmbezirke_url) + }, + warning = function(w) {teams_warning(w,title=paste0(wahl_name,": CURL-Polling"))}, + error = function(e) {teams_warning(e,title=paste0(wahl_name,": CURL-Polling"))} + ) + # Neuere Daten? + if (ts_daten > ts) { + ts <- ts_daten + hole_wahldaten() + } else { + # Logfile erneuern und 15 Sekunden schlafen + system("touch obwahl.log") + if (TEST) cat("Warte...\n") + Sys.sleep(15) + } +} +# Titel der Grafik "top" umswitchen +dw_edit_chart(top_id,title="Ergebnis: Wahlsieger") +dw_publish_chart(top_id) + +# Logging beenden +if (!TEST) { + sink() + sink(type="message") + file.rename("obwahl.log","obwahl_success.log") +} +teams_meldung(wahl_name," erfolgreich abgeschlossen.") + + +# EOF \ No newline at end of file diff --git a/R/main_oneshot.R b/R/main_oneshot.R new file mode 100644 index 0000000000000000000000000000000000000000..d4914a895036cd0f446a0b0e3a48dfa802061d8a --- /dev/null +++ b/R/main_oneshot.R @@ -0,0 +1,95 @@ +library(pacman) + +# Laden und ggf. installieren +p_load(this.path) +p_load(readr) +p_load(lubridate) +p_load(tidyr) +p_load(stringr) +p_load(dplyr) +p_load(DatawRappr) +p_load(curl) +p_load(magick) +p_load(openxlsx) + +rm(list=ls()) + +TEST = TRUE +DO_PREPARE_MAPS = FALSE + + + +# Aktuelles Verzeichnis als workdir +setwd(this.path::this.dir()) +# Aus dem R-Verzeichnis eine Ebene rauf +setwd("..") + +# Logfile anlegen, wenn kein Test +# if (!TEST) { +# logfile = file("obwahl.log") +# sink(logfile, append=T) +# sink(logfile, append=T, type="message") +# +# } + +# Messaging-Funktionen einbinden +source("R/messaging.R") + + +# Hole die Konfiguration und die Index-Daten +check = tryCatch( + { + source("R/lies_konfiguration.R") + }, + warning = function(w) {teams_warning(w,title="OBWAHL: Warnung beim Lesen der Konfigurationsdatei")}, + error = function(e) {teams_error(e,title="OBWAHL: Konfigurationsdatei nicht gelesen!")}) + +# Funktionen einbinden +# Das könnte man auch alles hier in diese Datei schreiben, aber ist es übersichtlicher. +source("R/lies_aktuellen_stand.R") +source("R/aktualisiere_karten.R") + +#---- MAIN ---- +# Vorbereitung +gezaehlt <- 0 # Ausgezählte Stimmbezirke +ts <- as_datetime(startdatum) # ts, Zeitstempel, der letzten gelesenen Daten +stimmbezirke_n <- nrow(stimmbezirke_df) # Anzahl aller Stimmbezirke bei der Wahl +alte_daten <- lies_stimmbezirke(stimmbezirke_url) # Leere Stimmbezirke +# Grafiken einrichten: Farbwerte und Switcher für die Karten +# Richtet auch die globale Variable switcher ein, deshalb brauchen wir sie + + +if (DO_PREPARE_MAPS) { + check = tryCatch( + vorbereitung_alle_karten(), + warning = function(w) {teams_warning(w,title=paste0(wahl_name,": Vorbereitung"))}, + error = function(e) {teams_warning(e,title=paste0(wahl_name,": Vorbereitung"))} + ) +} else { + # Alle Datawrapper-IDs in einen Vektor extrahieren + id_df <- config_df %>% + filter(str_detect(name,"_kand[0-9]+")) %>% + mutate(Nummer = as.integer(str_extract(name,"[0-9]+"))) %>% + select(Nummer,dw_id = value)# + # Mach aus der Switcher-Tabelle eine globale Variable + # Nur die Globale switcher_df definieren (mit den IDs der DW-Karten zum Kandidaten/Farbwert) + switcher_df <- kandidaten_df %>% + select(Nummer, Vorname, Name, Parteikürzel, Farbwert) %>% + left_join(id_df, by="Nummer") +} + +# One-shot. +check = tryCatch( + { # Zeitstempel der Daten holen + ts_daten <- check_for_timestamp(stimmbezirke_url) + }, + warning = function(w) {teams_warning(w,title=paste0(wahl_name,": CURL-Polling"))}, + error = function(e) {teams_warning(e,title=paste0(wahl_name,": CURL-Polling"))} + ) + # Zeitstempel aktualisieren, Datenverarbeitung anstoßen +ts <- ts_daten +# Hole die neuen Daten + +hole_wahldaten() + +# EOF \ No newline at end of file diff --git a/R/messaging.R b/R/messaging.R index b2b93b0e1470153dc8f474648344f2546ad7dfc2..77807cabdd5cdf51c44314c1398d25a8c7c75a46 100644 --- a/R/messaging.R +++ b/R/messaging.R @@ -9,15 +9,18 @@ library(teamr) #' #' Kommunikation mit Teams #' -#' Webhook wird als URL im Environment gespeichert. Wenn nicht dort, dann +#' Webhook wird als URL im Environment gespeichert. Wenn nicht dort, dann Datei im Nutzerverzeichnis ~/key/ einlesen. +#' MSG-Funktion schreibt alles in die Logdatei und auf den Bildschirm. (Vgl. Corona.) + + # Webhook schon im Environment? -if (Sys.getenv("WEBHOOK_REFERENDUM") == "") { - t_txt <- read_file("../key/webhook_referendum.key") +if (Sys.getenv("WEBHOOK_OBWAHL") == "") { + t_txt <- read_file("~/key/webhook_obwahl.key") Sys.setenv(WEBHOOK_REFERENDUM = t_txt) } -teams_meldung <- function(...,title="Feldmann-Update") { +teams_meldung <- function(...,title="OB-Wahl-Update") { cc <- teamr::connector_card$new(hookurl = t_txt) cc$title(paste0(title," - ",lubridate::with_tz(lubridate::now(), "Europe/Berlin"))) @@ -29,13 +32,13 @@ teams_meldung <- function(...,title="Feldmann-Update") { teams_error <- function(...) { alert_str <- paste0(...) - teams_meldung(title="Feldmann: FEHLER: ", ...) + teams_meldung(title="OB-Wahl: FEHLER: ", ...) stop(alert_str) } teams_warning <- function(...) { alert_str <- paste0(...) - teams_meldung("Feldmann: WARNUNG: ",...) + teams_meldung("OB-Wahl: WARNUNG: ",...) warning(alert_str) } diff --git a/R/update_all.R b/R/update_all.R deleted file mode 100644 index e0dd5d6d73505c1a8d37304dcf8eb2236688b0f4..0000000000000000000000000000000000000000 --- a/R/update_all.R +++ /dev/null @@ -1,203 +0,0 @@ -library(pacman) - -# Laden und ggf. installieren -p_load(this.path) -p_load(readr) -p_load(lubridate) -p_load(tidyr) -p_load(stringr) -p_load(dplyr) -p_load(DatawRappr) - -rm(list=ls()) - -# Aktuelles Verzeichnis als workdir -setwd(this.path::this.dir()) -# Aus dem R-Verzeichnis eine Ebene rauf -setwd("..") - - -source("R/messaging.R") -source("R/lies_aktuellen_stand.R") -source("R/aktualisiere_karten.R") -source("R/generiere_balken.R") - - -#----aktualisiere_fom() ---- -# fom ist das "Feldmann-o-meter", die zentrale Grafik mit dem Stand der Auszählung. - -aktualisiere_fom <- function(wl_url = stimmbezirke_url) { - - # Einlesen: Feldmann-o-meter-Daten so far. - # Wenn die Daten noch nicht existieren, generiere ein leeres df. - if(file.exists("daten/fom_df.rds")) { - fom_df <- readRDS("daten/fom_df.rds") - } else { - # Leeres df mit einer Zeile - fom_df <- tibble(zeitstempel = as_datetime(startdatum), - meldungen_anz = 0, - meldungen_max = 575, - # Ergebniszellen - wahlberechtigt = 0, - # Mehr zum Wahlschein hier: https://www.bundeswahlleiter.de/service/glossar/w/wahlscheinvermerk.html - waehler_regulaer = 0, - waehler_wahlschein = 0, - waehler_nv = 0, - stimmen = 0, - stimmen_wahlschein = 0, - ungueltig = 0, - gueltig = 0, - ja = 0, - nein = 0) - # SAVE kann man sich schenken; df ist schneller neu erzeugt - # save(feldmann_df,"daten/feldmann_df.rda") - } - # Daten zur Sicherheit sortieren, dann die letzte Zeile rausziehen - letzte_fom_df <- fom_df %>% - arrange(zeitstempel) %>% - tail(1) - # Neue Daten holen (mit Fehlerbehandlung) - stimmbezirke_df <- lies_gebiet(wl_url) - neue_fom_df <- stimmbezirke_df %>% - # Namen raus - select(-name,-nr) %>% - # Daten aufsummieren - summarize(zeitstempel = last(zeitstempel), - across(2:ncol(.), ~ sum(.,na.rm=T))) - # Alte und neue Daten identisch? Dann brich ab. - if (vergleiche_stand(letzte_fom_df,neue_fom_df)) { - return(FALSE) - } else { - # Archiviere die Rohdaten - archiviere(stimmbezirke_df,"daten/stimmbezirke/") - # Ergänze das fom_df um die neuen Daten und sichere es - fom_df <- fom_df %>% bind_rows(neue_fom_df) - saveRDS(fom_df,"daten/fom_df.rds") - # Bilde das Dataframe - # Sende die Daten an Datawrapper und aktualisiere - fom_dw_df <- fom_df %>% - mutate(ausgezählt = meldungen_anz / meldungen_max *100) %>% - mutate(prozent30 = NA) %>% - mutate(quorum = ja / wahlberechtigt * 100) %>% - select(ausgezählt, wahlberechtigt, ungueltig, ja, nein, quorum, prozent30) %>% - # Noch den Endpunkt der 30-Prozent-Linie - bind_rows(tibble(ausgezählt = 100, prozent30 = ffm_waehler * 0.3)) - dw_data_to_chart(fom_dw_df,fom_id) - # Parameter setzen - alles_ausgezählt <- (neue_fom_df$meldungen_max == neue_fom_df$meldungen_anz) - if (neue_fom_df$meldungen_anz == 0) { - quorum = 0 - feldmann_str <- "Es liegen noch keine Auszählungsdaten des Bürgerentscheids vor." - } else { - quorum <- (neue_fom_df$ja / neue_fom_df$wahlberechtigt * 100) - if (quorum >= 30) { - if (alles_ausgezählt ) { - feldmann_str <- "Peter Feldmann ist als OB abgewählt." - } else { - feldmann_str <- "Nach dem derzeitigen Auszählungsstand wäre Peter Feldmann als OB abgewählt." - } - } else { - if (alles_ausgezählt ) { - feldmann_str <- "Peter Feldmann bleibt OB von Frankfurt." - } else { - feldmann_str <- "Nach dem derzeitigen Auszählungsstand bliebe Peter Feldmann OB von Frankfurt." - } - } - } - - # Breite des Balkens: Wenn das Quorum erreicht ist, hat er die volle Breite, - # wenn nicht, einen Anteil von 30%, um die Entfernung von der Markierung zu zeigen - - # Jetzt die Beschreibungstexte mit den Fake-Balkengrafiken generieren - beschreibung_str <- paste0( - "Die Abwahl ist beschlossen, wenn mindestens 30 Prozent aller Wahlberechtigten mit "Ja" stimmen.<br/><br>", - "<b style='font-weight:700;font-size:120%;'>", - # Erste dynamisch angepasste Textstelle: Bleibt Feldmann? - feldmann_str, - "</b><br/><br>", - generiere_balken(wb = neue_fom_df$wahlberechtigt, - ja = neue_fom_df$ja, - nein = neue_fom_df$nein, - auszählung_beendet = alles_ausgezählt)) - annotate_str <- generiere_auszählungsbalken( - ausgezählt = floor(neue_fom_df$wahlberechtigt / ffm_waehler * 100), - anz = neue_fom_df$meldungen_anz, - max = neue_fom_df$meldungen_max, - ts = neue_fom_df$zeitstempel) - briefwahl_anz <- stimmbezirke_df %>% filter(str_detect(nr,"^9")) %>% - pull(meldungen_anz) %>% sum() - briefwahl_max <- stimmbezirke_df %>% filter(str_detect(nr,"^9")) %>% - nrow() - annotate_str <- paste0("<strong>Derzeit sind ", - briefwahl_anz, - " von ", - briefwahl_max, - " Briefwahl-Stimmbezirken ausgezählt.</strong><br/><br/>", - annotate_str) - dw_edit_chart(fom_id,intro = beschreibung_str,annotate = annotate_str) - dw_publish_chart(fom_id) - return(TRUE) - } -} - - -#---- MAIN ---- -# Ruft aktualisiere_fom() auf -# (die dann wieder aktualisiere_karten() aufruft) -check = tryCatch( - { - neue_daten <- aktualisiere_fom(stimmbezirke_url) - }, - warning = function(w) {teams_warning(w,title="Feldmann: fom")}, - error = function(e) {teams_warning(e,title="Feldmann: fom")}) -# Neue Daten? Dann aktualisiere die Karten -if (neue_daten) { - check = tryCatch( - { - neue_daten <- aktualisiere_karten(stimmbezirke_url) - }, - warning = function(w) {teams_warning(w,title="Feldmann: Karten")}, - error = function(e) {teams_warning(e,title="Feldmann: Karten")}) - if (neue_daten) { - # Alles OK, letzte Daten nochmal holen und ausgeben - fom_df <- readRDS("daten/fom_df.rds") %>% - arrange(zeitstempel) %>% - tail(1) - if(fom_df$meldungen_anz > 0) { - stimmbezirke_df <- lies_gebiet(stimmbezirke_url) - briefwahl_anz <- stimmbezirke_df %>% filter(str_detect(nr,"^9")) %>% - pull(meldungen_anz) %>% sum() - briefwahl_max <- stimmbezirke_df %>% filter(str_detect(nr,"^9")) %>% - nrow() - fom_update_str <- paste0( - "<strong>Update OK</strong><br/><br/>", - fom_df$meldungen_anz, - " von ", - fom_df$meldungen_max," Stimmbezirke ausgezählt.<br> ", - "Derzeit sind ", - briefwahl_anz, - " von ", - briefwahl_max, - " Briefwahl-Stimmbezirken ausgezählt.<br/>", - "<ul><li><strong>Quorum zur Abwahl ist derzeit", - ifelse(fom_df$ja / fom_df$wahlberechtigt < 0.3, " nicht ", " "), - "erreicht</strong></li>", - "<li><strong>Anteil der Ja-Stimmen an den Wahlberechtigten: ", - format(fom_df$ja / fom_df$wahlberechtigt * 100,decimal.mark=",",big.mark=".",nsmall=1, digits=3),"%", - "</li><li>Ja-Stimmen: ", - format(fom_df$ja,decimal.mark=",",big.mark="."), - "</li><li>Nein-Stimmen: ", - format(fom_df$nein,decimal.mark=",",big.mark="."), - "</li><li>Verhältnis Ja:Nein: ", - format(fom_df$ja / (fom_df$ja + fom_df$nein) * 100,decimal.mark=",",big.mark=".",nsmall=1, digits=3),"% : ", - format(fom_df$nein / (fom_df$ja + fom_df$nein) *100,decimal.mark=",",big.mark=".",nsmall=1, digits=3),"%</li></ul>" - - ) - teams_meldung(fom_update_str,title="Feldmann-Referendum") - - } - } else { - teams_warning("Neue Stimmbezirk-Daten, aber keine neuen Ortsdaten?") - } -} -# Auch hier TRUE zurückbekommen;; alles OK? \ No newline at end of file diff --git a/README.md b/README.md index b56eda9e4e778d815d162f3c68c3a50b71e370c7..f74ad16c0f3903e3067922758129d3ca147f8952 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,25 @@ # obwahlen PRE -**DIES IST IM AUGENBLICK NUR EINE NOCH NICHT ANGEPASSTE KOPIE DES REFERENDUMS-CODES** - bitte nicht nutzen und wundern! Anpassung spätestens zur [1. Runde der OB-Wahl in Frankfurt am 5. März 2023](https://frankfurt.de/aktuelle-meldung/meldungen/direktwahl-oberbuergermeisterin-oberbuergermeister-frankfurt/). - R-Code, um den Auszählungsstand hessischer Bürgermeisterwahlen in Echtzeit abzurufen und mit Datawrapper darzustellen ## Ordnerstruktur - **R** enthält den Code -- **index** enthält Index-, Konfigurations-, und Template-Dateien +- **index** enthält die Konfigurationsdatei index.csv und Unterordner mit den Indexdateien: Kandidaten, Stadtteile, Stimmbezirke, Datawrapper-Zuordnungen. - **daten** wird vom Code beschrieben und enthält den aktuellen Datenstand. ## Daten aufarbeiten ### Ziele -Folgende Grafiken wären denkbar: -* Balkengrafik Ergebnis nach derzeitigem Auszählungsstand mit "Fortschrittsbalken" -* Choropleth Stadtteil-Sieger +Grafiken: +* Säulengrafik erste fünf; Ergebnis nach derzeitigem Auszählungsstand mit "Fortschrittsbalken" + +* Balkengrafik alle +* Choropleth Stadtteil-Sieger (mit Switcher alle, die gewonnen haben) * Choropleth Ergebnis nach Kandidat -* Choropleth Wahlbeteiligung -* Choropleth Briefwahl +* Tabelle nach Kandidaten (3 beste, 3 schlechteste Stadtteile) * Tabelle nach Stadtteil -* Tabelle nach Kandidaten (Erste drei? fünf?) ### Konfiguration @@ -41,15 +39,20 @@ Aggregation auf Stadtebene - Fortschrittsbalken ausgezählte Stimmen (mit akt. Briefwahlstimmendaten) -## Struktur des Codes +## Struktur des Codes: Was tut was? + +(siehe ["Sitemap"](./sitemap.md) für den Code) + + +# TODO -### Hauptroutinen -- **update_all.R** ist das Skript für den CRON-Job. Es pollt nach Daten, ruft die Abruf-, Aggregations- und Auswertungsfunktionen auf und gibt Meldungen aus. -- **lies_aktuellen_stand.R** enthält Funktionen, die die Daten lesen, aggregieren und archivieren -- **aktualisiere_karten.R** enthält die Funktionen zur Datenausgabe -- **messaging.R** enthält Funktionen, die Teams-Updates und -Fehlermeldungen generieren +- Upload aufs Repository -### Hilfsfunktionen +## NTH -- **generiere_testdaten.R** ist ein Skript, das zufällige, aber plausible CSV-Daten auf Stimmbezirks-Ebene zum Testen generiert +- Umschalten Top5-Titel Ergebnis +- Zusatzfeature: Briefwahlprognostik - wieviele Stimmen fehlen vermutlich noch? +- Shapefiles KS, DA verbessern +- Datensparsamere Alternativ-CURL-Poll-Datei (zB mit dem Gesamtergebnis) +- Mehr Licht in den Choropleth-Karten farbabhängig diff --git a/howto_shapefiles.md b/howto_shapefiles.md new file mode 100644 index 0000000000000000000000000000000000000000..b60bcfdb77d78f2857ed9873e4595b74e196b904 --- /dev/null +++ b/howto_shapefiles.md @@ -0,0 +1,26 @@ +1. Shapefile in QGIS importieren + +2. GEOJSON im richtigen Koordinatensystem erstellen +Dazu Rechtsklick auf den Layer; Koordinatensystem WGS84, exportieren + +3. Stadtteile generieren +Menü "Vektor", "Geometrieverarbeitungswerkzeuge", "Auflösen" - und dann in der Dialogbox auswählen "Felder auflösen [optional]", und dann die Attribute hinzufügen, nach denen zusammengeführt werden soll. + +In KS beispielsweise gab es die + + +- Rechtsklick auf den Layer; Exportieren als GEOJSON - nicht vergessen, das Bezugssystem auf WGS84 umzustellen! +- Rechtsklick auf den Layer; Export als XLSX - ggf. Geo-Attribute abschalten + +4. Mittelpunkte der Stadtteile + +Menü "Vektor", "Geometrie-Werkzeuge", "Zentroide" + +Dann noch Geokoordinaten der Zentroidpunkte: Rechte Seite die Toolbox, dort "Vektortabelle" aufklappen, "X/Y-Felder zu Attributen hinzufügen" + +- Rechtsklick auf den neu erzeugten Layer, exportieren als XLSX bzw CSV + +5. CSV-/XLSX-Dateien putzen + +- Brauchen eine Stadtteil-Datei mit nr,name,lon,lat (erzeugt aus den Zentroiden) +- Brauchen einen Wahlbezirks-Zuordnung diff --git a/index/config.csv b/index/config.csv index 04090781806af46ccd34af31eaf562a4a8cb9201..45880e515f46a13f9512ce861df01a77b3ac9e85 100644 --- a/index/config.csv +++ b/index/config.csv @@ -1,8 +1,23 @@ name,value,comment -stimmbezirke_url,https://votemanager-ffm.ekom21cdn.de/2022-11-06/06412000/praesentation/Open-Data-06412000-Buergerentscheid-zur-Abwahl-des-Oberbuergermeisters-der-Stadt-Frankfurt-am-Main_-Herrn-Peter-Feldmann-Stimmbezirk.csv?ts=1667662273015,URL Daten-CSV Stimmbezirke -ffm_waehler,508182,Wahlamt -fom_id,bIm87,Datawrapper-ID Feldmann-o-meter -choropleth_id,UwKOO,Datawrapper-ID Stadtteile Choropleth-Karte -symbol_id,RWqrf,Datawrapper-ID Stadtteile Symbole (absolute Stimmen) -tabelle_id,hLqMi,Datawrapper-ID Tabelle Stadtteile -startdatum,2022-11-06 18:00:00 CET,Beginn der Auszählung +wahl_name,obwahl_ks_2023,Welche Wahl? +stimmbezirke_url,https://votemanager-ks.ekom21cdn.de/2023-03-12/06611000/daten/opendata/Open-Data-06611000-Direktwahl-zur-Oberbuergermeisterin-zum-Oberbuergermeister-Wahlbezirk.csv?ts=1678486050153,URL Daten-CSV Stimmbezirke +wahlberechtigt,147463,Anzahl Wahlberechtigte lt. Wahlamt (kommt Sonntag) +briefwahl,39092,Anzahl Briefwahlstimmen lt. Wahlamt (kommt Sonntag) +top,6,Anzahl der Top-Kandidaten in den Darstellungen +kandidaten_fname,kandidaten.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet" +zuordnung_fname,wahlbezirke.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet" +stadtteile_fname,stadtteile.csv,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet" +startdatum,2023-03-12 16:00:00,Beginn der Auszählung +top_id,Ts1oS, +karte_sieger_id,O9wPT, +karte_kand1_id,hM9SE,Schöller +karte_kand2_id,07CR4,Carqueville +karte_kand3_id,whgzp,Kühne-Hörmann +karte_kand4_id,5CpYu,Bock +karte_kand5_id,pc6vH,Käufler +karte_kand6_id,sEJhl,Geselle +tabelle_alle_id,EQ4dd, +hochburgen_id,GMTSJ, +tabelle_stadtteile_id,q7yjs, +social1_id,Ts1oS,5 stärkste +social2_id,S9BbQ,Alle Stimmen angepasst diff --git a/index/config_test.csv b/index/config_test.csv new file mode 100644 index 0000000000000000000000000000000000000000..8c2e436731078e59a1d0619b113f72e81e26311d --- /dev/null +++ b/index/config_test.csv @@ -0,0 +1,23 @@ +name,value,comment +wahl_name,obwahl_ks_2023,Welche Wahl? +stimmbezirke_url,https://www.eggers-elektronik.de/files/test.csv,URL Daten-CSV Stimmbezirke +wahlberechtigt,147463,Anzahl Wahlberechtigte lt. Wahlamt (kommt Sonntag) +briefwahl,39092,Anzahl Briefwahlstimmen lt. Wahlamt (kommt Sonntag) +kandidaten_fname,kandidaten.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet" +zuordnung_fname,wahlbezirke.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet" +stadtteile_fname,ks-stadtteile.csv,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet" +startdatum,2023-01-01 18:00:00 CET,Beginn der Auszählung +top,6, +top_id,028Fp, +karte_sieger_id,7gscI, +karte_kand1_id,hM9SE,Schöller +karte_kand2_id,07CR4,Carqueville +karte_kand3_id,whgzp,Kühne-Hörmann +karte_kand4_id,5CpYu,Bock +karte_kand5_id,pc6vH,Käufler +karte_kand6_id,sEJhl,Geselle +tabelle_alle_id,PLwHI, +hochburgen_id,Im2PX, +tabelle_stadtteile_id,BM8kD, +social1_id,028Fp,5 stärkste +social2_id,S9BbQ,Alle Stimmen angepasst diff --git a/index/ffm_config.csv b/index/ffm_config.csv new file mode 100644 index 0000000000000000000000000000000000000000..2f97d66ece55a4ae2800db52ad5c9972853d3f90 --- /dev/null +++ b/index/ffm_config.csv @@ -0,0 +1,37 @@ +name,value,comment +wahl_name,obwahl_ffm_2023,Welche Wahl? +stimmbezirke_url,https://votemanager-ffm.ekom21cdn.de/2023-03-05/06412000/daten/opendata/Open-Data-06412000-OB-Wahl-Wahlbezirk.csv?ts=1677904123448,URL Daten-CSV Stimmbezirke +wahlberechtigt,508182,Anzahl Wahlberechtigte lt. Wahlamt (kommt Sonntag) +briefwahl,250000,Anzahl Briefwahlstimmen lt. Wahlamt (kommt Sonntag) +kandidaten_fname,kandidaten.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet" +datawrapper_fname,datawrapper.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet" +zuordnung_fname,zuordnung_wahllokale.csv,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet" +stadtteile_fname,stadtteile.csv,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet" +startdatum,2023-03-05 16:00:00,Beginn der Auszählung +top5_id,2DYBQ, +karte_sieger_id,ANKmx, +karte_kand1_id,RcvQp,Rottmann (Grüne) +karte_kand2_id,jrm2v,Becker (CDU) +karte_kand3_id,bKR8r,Josef (SPD) +karte_kand4_id,etN3J,Mehler-Würzbach (Linke) +karte_kand5_id,3mydT,Pürsün (FDP) +karte_kand6_id,K3aCw,Lobenstein (AfD) +karte_kand7_id,vtG4Y,Pfeiffer (BFF) +karte_kand8_id,tRHeI,Tanczos (PARTEI) +karte_kand9_id,v4Y5m,Schwichtenberg (Gartenpartei) +karte_kand10_id,g3iBN,Wirth (unabh.) +karte_kand11_id,4LxcN,Camara (FPF) +karte_kand12_id,RZDF7,Pauli (unabh.) +karte_kand13_id,F86gf,Junghans (unabh.) +karte_kand14_id,bLPXL,Xu (unabh.) +karte_kand15_id,Ktufa,Wolff (unabh.) +karte_kand16_id,MO41j,Akhtar (Todenhöfer) +karte_kand17_id,ccrfL,Großenbach (Basis) +karte_kand18_id,q2S6m,Pawelski (unabh.) +karte_kand19_id,697CL,Schulte (unabh.) +karte_kand20_id,3lMmu,Eulig (unabh.) +tabelle_alle_id,7kRPR, +hochburgen_id,oB3KH, +tabelle_stadtteile_id,LiXnz, +social1_id,2DYBQ,5 stärkste +social2_id,S9BbQ,Alle Stimmen angepasst diff --git a/index/ffm_config_test.csv b/index/ffm_config_test.csv new file mode 100644 index 0000000000000000000000000000000000000000..07d5d1ab78591ee0f2e7376fb6d28c043eb752fa --- /dev/null +++ b/index/ffm_config_test.csv @@ -0,0 +1,37 @@ +name,value,comment +wahl_name,obwahl_ffm_2023,Welche Wahl? +stimmbezirke_url,testdaten/dummy.csv,URL Daten-CSV Stimmbezirke +wahlberechtigt,508182,Anzahl Wahlberechtigte lt. Wahlamt (kommt Sonntag) +briefwahl,250000,Anzahl Briefwahlstimmen lt. Wahlamt (kommt Sonntag) +kandidaten_fname,kandidaten.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet" +datawrapper_fname,datawrapper.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet" +zuordnung_fname,zuordnung_wahllokale.csv,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet" +stadtteile_fname,stadtteile.csv,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet" +startdatum,2023-01-01 18:00:00 CET,Beginn der Auszählung +top5_id,028Fp, +karte_sieger_id,7gscI, +karte_kand1_id,hM9SE,Rottmann (Grüne) +karte_kand2_id,hM9SE,Becker (CDU) +karte_kand3_id,07CR4,Josef (SPD) +karte_kand4_id,07CR4,Mehler-Würzbach (Linke) +karte_kand5_id,07CR4,Pürsün (FDP) +karte_kand6_id,07CR4,Lobenstein (AfD) +karte_kand7_id,07CR4,Pfeiffer (BFF) +karte_kand8_id,07CR4,Tanczos (PARTEI) +karte_kand9_id,07CR4,Schwichtenberg (Gartenpartei) +karte_kand10_id,07CR4,Wirth (unabh.) +karte_kand11_id,07CR4,Camara (FPF) +karte_kand12_id,07CR4,Pauli (unabh.) +karte_kand13_id,07CR4,Junghans (unabh.) +karte_kand14_id,07CR4,Xu (unabh.) +karte_kand15_id,07CR4,Wolff (unabh.) +karte_kand16_id,07CR4,Akhtar (Todenhöfer) +karte_kand17_id,07CR4,Großenbach (Basis) +karte_kand18_id,07CR4,Pawelski (unabh.) +karte_kand19_id,07CR4,Schulte (unabh.) +karte_kand20_id,07CR4,Eulig (unabh.) +tabelle_alle_id,PLwHI, +hochburgen_id,Im2PX, +tabelle_stadtteile_id,BM8kD, +social1_id,028Fp,5 stärkste +social2_id,S9BbQ,Alle Stimmen angepasst diff --git a/index/obwahl_ffm_2023/kandidaten.xlsx b/index/obwahl_ffm_2023/kandidaten.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..11bdb590d825bbe4cd94aab1bb12e0e987c7a954 Binary files /dev/null and b/index/obwahl_ffm_2023/kandidaten.xlsx differ diff --git a/index/obwahl_ffm_2023/stadtteile.csv b/index/obwahl_ffm_2023/stadtteile.csv new file mode 100644 index 0000000000000000000000000000000000000000..b07902a0b1c6a861a2489ce79713d62c611f502f --- /dev/null +++ b/index/obwahl_ffm_2023/stadtteile.csv @@ -0,0 +1,45 @@ +nr,name,lon,lat +1,Altstadt,8.682385346400634,50.11059669873516 +2,Innenstadt,8.682664888207869,50.113790989177375 +4,Westend-Süd,8.6594393465439925,50.11682467903111 +5,Westend-Nord,8.666488952498467,50.128769620533795 +6,Nordend-West,8.684596043386934,50.13022858244322 +7,Nordend-Ost,8.69761974358955,50.127318654892264 +8,Ostend,8.719218276147204,50.11554639352114 +9,Bornheim,8.712407551447747,50.13090801018288 +10,Gutleut- und Bahnhofsviertel,8.652137942960298,50.099479845695974 +11,Gallus,8.636377745265355,50.10300477630784 +12,Bockenheim,8.632922516089874,50.12128753657858 +13,Sachsenhausen-Nord,8.684579993638577,50.10051804775371 +14,Sachsenhausen-Süd und Flughafen,8.629663957345496,50.0607635296547 +16,Oberrad,8.727138168461476,50.09922899999497 +17,Niederrad,8.636199605275262,50.081631798202295 +18,Schwanheim,8.572652070944704,50.081760019105545 +19,Griesheim,8.600109548586545,50.09781734654457 +20,Rödelheim,8.603076601631098,50.127692431637506 +21,Hausen,8.626134516198546,50.13524298077671 +22,Praunheim,8.61444644716483,50.14547905112678 +24,Heddernheim,8.64020132453368,50.158128239125205 +25,Niederursel,8.616911198776547,50.16683966510584 +26,Ginnheim,8.648134546192246,50.14388748058928 +27,Dornbusch,8.670541998003081,50.14434313041997 +28,Eschersheim,8.659950213724542,50.16002839395001 +29,Eckenheim,8.683795236784233,50.148564823086005 +30,Preungesheim,8.697198159142667,50.15544843144313 +31,Bonames,8.665887880254154,50.18258113675648 +32,Berkersheim,8.702941786636124,50.17015956481773 +33,Riederwald,8.73274589886058,50.12667040584185 +34,Seckbach,8.726644066440096,50.147246840458955 +35,Fechenheim,8.762275115113775,50.12551773891441 +36,Höchst,8.539657322936813,50.098523172532 +37,Nied,8.57676379509509,50.103479453362766 +38,Sindlingen,8.51273746688725,50.07800492013246 +39,Zeilsheim,8.495768400332896,50.097784690964886 +40,Unterliederbach,8.525490184772172,50.10992510336304 +41,Sossenheim,8.574019745075416,50.12010605434964 +42,Nieder-Erlenbach,8.709219115856458,50.20871646737095 +43,Kalbach-Riedberg,8.639008245521376,50.1846309062546 +44,Harheim,8.689844680792895,50.18583895746724 +45,Nieder-Eschbach,8.668094243977997,50.20106152063871 +46,Bergen-Enkheim,8.766772308170399,50.15913246211432 +47,Frankfurter Berg,8.673427563622,50.169778678198924 diff --git a/index/zuordnung_wahllokale.csv b/index/obwahl_ffm_2023/zuordnung_wahllokale.csv similarity index 100% rename from index/zuordnung_wahllokale.csv rename to index/obwahl_ffm_2023/zuordnung_wahllokale.csv diff --git a/index/obwahl_ks_2023/kandidaten.xlsx b/index/obwahl_ks_2023/kandidaten.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e10b674797487ee43e2033972e604fe8f5e8c83b Binary files /dev/null and b/index/obwahl_ks_2023/kandidaten.xlsx differ diff --git "a/index/obwahl_ks_2023/ks-stadtteile-mit-d\303\266nchelandschaft.csv" "b/index/obwahl_ks_2023/ks-stadtteile-mit-d\303\266nchelandschaft.csv" new file mode 100644 index 0000000000000000000000000000000000000000..55d79178ae67d8e7dfaf41be60ed3a8967e9e4d9 --- /dev/null +++ "b/index/obwahl_ks_2023/ks-stadtteile-mit-d\303\266nchelandschaft.csv" @@ -0,0 +1,25 @@ +nr,name,lon,lat +1,Mitte,9.4894928677,51.3151211858 +2,Südstadt,9.4882009490,51.3005087449 +3,Vorderer Westen,9.4640787357,51.3163544480 +4,Wehlheiden,9.4654088136,51.3056032748 +5,Bad Wilhelmshöhe,9.4011506791,51.3116605680 +6,Brasselsberg,9.3972256665,51.2912775844 +7,Süsterfeld-Helleböhn,9.4469292484,51.2964342813 +8,Harleshausen,9.4151173612,51.3391059157 +9,Kirchditmold,9.4391198473,51.3249890808 +10,Rothenditmold,9.4771802265,51.3287075207 +11,Nord-Holland,9.4937754140,51.3323742823 +12,Philippinenhof-Warteberg,9.4941950806,51.3457423064 +13,Fasanenhof,9.5123139521,51.3347662999 +14,Wesertor,9.5148784085,51.3221887268 +15,Wolfsanger-Hasenhecke,9.5457989433,51.3396806416 +16,Bettenhausen,9.5384097499,51.3044330002 +17,Forstfeld,9.5418612035,51.2915878853 +18,Waldau,9.5100487565,51.2868080527 +19,Niederzwehren,9.4709628876,51.2783155707 +20,Oberzwehren,9.4443563894,51.2747255123 +21,Nordshausen,9.4245341169,51.2790202220 +22,Jungfernkopf,9.4607713668,51.3409314760 +23,Unterneustadt,9.5160276635,51.3119155114 +25,Dönchelandschaft (ortsbezirksfrei),9.4320779328,51.2918551834 diff --git a/index/obwahl_ks_2023/ks-stadtteile.csv b/index/obwahl_ks_2023/ks-stadtteile.csv new file mode 100644 index 0000000000000000000000000000000000000000..ddb583ea89f10e3957368e62487ab0c38976dd13 --- /dev/null +++ b/index/obwahl_ks_2023/ks-stadtteile.csv @@ -0,0 +1,24 @@ +nr,name,lon,lat +1,Mitte,9.4894928677,51.3151211858 +2,Südstadt,9.4882009490,51.3005087449 +3,Vorderer Westen,9.4640787357,51.3163544480 +4,Wehlheiden,9.4654088136,51.3056032748 +5,Bad Wilhelmshöhe,9.4011506791,51.3116605680 +6,Brasselsberg,9.3972256665,51.2912775844 +7,Süsterfeld-Helleböhn,9.4469292484,51.2964342813 +8,Harleshausen,9.4151173612,51.3391059157 +9,Kirchditmold,9.4391198473,51.3249890808 +10,Rothenditmold,9.4771802265,51.3287075207 +11,Nord-Holland,9.4937754140,51.3323742823 +12,Philippinenhof-Warteberg,9.4941950806,51.3457423064 +13,Fasanenhof,9.5123139521,51.3347662999 +14,Wesertor,9.5148784085,51.3221887268 +15,Wolfsanger-Hasenhecke,9.5457989433,51.3396806416 +16,Bettenhausen,9.5384097499,51.3044330002 +17,Forstfeld,9.5418612035,51.2915878853 +18,Waldau,9.5100487565,51.2868080527 +19,Niederzwehren,9.4709628876,51.2783155707 +20,Oberzwehren,9.4443563894,51.2747255123 +21,Nordshausen,9.4245341169,51.2790202220 +22,Jungfernkopf,9.4607713668,51.3409314760 +23,Unterneustadt,9.5160276635,51.3119155114 diff --git a/index/obwahl_ks_2023/wahlbezirke.xlsx b/index/obwahl_ks_2023/wahlbezirke.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..83f1863a5b2b328129005c5cc33f5690644392c6 Binary files /dev/null and b/index/obwahl_ks_2023/wahlbezirke.xlsx differ diff --git a/index/stadtteile.csv b/index/stadtteile.csv deleted file mode 100644 index 66dba9e3eb2e8c5a481e3be95da03d9da6e1352d..0000000000000000000000000000000000000000 --- a/index/stadtteile.csv +++ /dev/null @@ -1,45 +0,0 @@ -nr,name,lon,lat,wahlberechtigt_2018,waehler_2018,gueltig_2018,feldmann_2018 -1,Altstadt,8.682385346400634,50.11059669873516,2772,1035,1030,427 -2,Innenstadt,8.682664888207869,50.113790989177375,4343,1074,1066,435 -4,Westend-Süd,8.6594393465439925,50.11682467903111,13376,5835,5814,1814 -5,Westend-Nord,8.666488952498467,50.128769620533795,6887,2761,2751,1064 -6,Nordend-West,8.684596043386934,50.13022858244322,22988,10779,10743,4368 -7,Nordend-Ost,8.69761974358955,50.127318654892264,17390,7852,7817,3578 -8,Ostend,8.719218276147204,50.11554639352114,20946,8411,8358,3873 -9,Bornheim,8.712407551447747,50.13090801018288,22232,9682,9633,4954 -10,Gutleut- und Bahnhofsviertel,8.652137942960298,50.099479845695974,6802,2113,2103,831 -11,Gallus,8.636377745265355,50.10300477630784,23958,6529,6488,2926 -12,Bockenheim,8.632922516089874,50.12128753657858,27090,10377,10332,4546 -13,Sachsenhausen-Nord,8.684579993638577,50.10051804775371,23441,9940,9897,4128 -14,Sachsenhausen-Süd und Flughafen,8.629663957345496,50.0607635296547,20710,8739,8700,3702 -16,Oberrad,8.727138168461476,50.09922899999497,9206,3109,3093,1491 -17,Niederrad,8.636199605275262,50.081631798202295,16799,5412,5366,2712 -18,Schwanheim,8.572652070944704,50.081760019105545,13936,5261,5217,2524 -19,Griesheim,8.600109548586545,50.09781734654457,14910,3670,3632,1921 -20,Rödelheim,8.603076601631098,50.127692431637506,12446,4518,4487,2108 -21,Hausen,8.626134516198546,50.13524298077671,4518,1814,1796,889 -22,Praunheim,8.61444644716483,50.14547905112678,11093,4365,4337,2109 -24,Heddernheim,8.64020132453368,50.158128239125205,11867,4579,4546,2385 -25,Niederursel,8.616911198776547,50.16683966510584,10458,3843,3818,1797 -26,Ginnheim,8.648134546192246,50.14388748058928,11185,4310,4285,2101 -27,Dornbusch,8.670541998003081,50.14434313041997,13527,6140,6102,2671 -28,Eschersheim,8.659950213724542,50.16002839395001,11166,5005,4982,2159 -29,Eckenheim,8.683795236784233,50.148564823086005,9638,3457,3425,1764 -30,Preungesheim,8.697198159142667,50.15544843144313,10222,3983,3965,2025 -31,Bonames,8.665887880254154,50.18258113675648,4467,1566,1549,862 -32,Berkersheim,8.702941786636124,50.17015956481773,2607,1137,1129,515 -33,Riederwald,8.73274589886058,50.12667040584185,3209,1070,1063,669 -34,Seckbach,8.726644066440096,50.147246840458955,7419,2926,2909,1381 -35,Fechenheim,8.762275115113775,50.12551773891441,10381,2571,2551,1475 -36,Höchst,8.539657322936813,50.098523172532,9859,2527,2506,1251 -37,Nied,8.57676379509509,50.103479453362766,12659,3730,3708,1975 -38,Sindlingen,8.51273746688725,50.07800492013246,5763,1768,1748,1011 -39,Zeilsheim,8.495768400332896,50.097784690964886,8012,2328,2299,1199 -40,Unterliederbach,8.525490184772172,50.10992510336304,10485,3201,3176,1533 -41,Sossenheim,8.574019745075416,50.12010605434964,10126,2684,2652,1270 -42,Nieder-Erlenbach,8.709219115856458,50.20871646737095,3544,1808,1796,665 -43,Kalbach-Riedberg,8.639008245521376,50.1846309062546,12409,5359,5333,2349 -44,Harheim,8.689844680792895,50.18583895746724,3548,1846,1836,601 -45,Nieder-Eschbach,8.668094243977997,50.20106152063871,8041,2979,2950,1204 -46,Bergen-Enkheim,8.766772308170399,50.15913246211432,13431,5774,5742,2583 -47,Frankfurter Berg,8.673427563622,50.169778678198924,5409,2049,2030,978 diff --git a/sitemap.md b/sitemap.md new file mode 100644 index 0000000000000000000000000000000000000000..316479bb14452ca8288f6f499b0c777e6a8fed85 --- /dev/null +++ b/sitemap.md @@ -0,0 +1,41 @@ +# Was ist wo? Die Sitemap für das Projekt + +## Programme +...liegen im Ordner "R": + +* **main.R**: Hauptprogramm, das alle Funktionen aufruft: Lies die Konfigurationsdateien ein. Schaue nach neuen Daten, lade sie herunter, verarbeite sie, gibt sie aus, versendet Teams-Messages, loggt jede Aktion in der Datei "obwahl.log"" mit. Die anderen R-Dateien sind über die "source()"-Funktion eingebunden - als Includes, gewissermaßen. + * **lies_konfiguration.R ** liest die Konfiguration für das Programm (Start der Wahl, Daten über Wahlberechtigte, Datawrapper-IDs) und liest die Index-Dateien ein: Kandidaten, Stadtteile, Wahllokale, Kommunalwahl-Ergebnisse. + * **lies_aktuellen_stand.R**: Funktionen, um auf neue Daten zu überprüfen, sie herunterzuladen und zu archivieren. + * **aktualisiere_karten.R**: Funktionen, um die Datawrapper-Karten zu aktualisieren. + * **messaging.R**: Funktionen, die über Teams Status- und Fehlermeldungen ausgeben. +* **its_alive.R**: Programm, das über einen CRON-Job alle zwei Minuten aufgerufen wird - und nachschaut, wann das letzte mal etwas in die Logdatei "obwahl.log" geschrieben wurde. Wenn das länger als zwei Minuten her ist, ist das Skript vermutlich stehen geblieben - und das Skript versendet einen Alarm über Teams. + +Im Ordner "R" gibt es einen Unterordner "Vorbereitung", der diese Skripte enthält: + +* generiere_testdaten.R +* teste-curl-polling.R - Testskript, um veränderte Daten über einen CURL-Aufruf so schnell wie möglich zu erkennen (sekundengenau) +* prepare.R - generiert die Datawrapper-Skripte und -karten und Indexdaten. + +## Wo man welche Funktionen findet (und was sie tun) +### lies_aktuellen_stand.R + +- archiviere(dir) - Hilfsfunktion, schreibt geholte Stimmbezirks-Daten auf die Festplatte +- hole_letztes_df(dir) - Hilfsfunktion, holt die zuletzt geschriebene Stimmbezirks-Datei aus dem Verzeichnis zurück +- check_for_timestamp(url) - liest das Änderungsdatum der Datei unter der URL aus +- lies_stimmbezirke(url) - liest aus der Datei unter der URL die Stimmbezirke +- aggregiere_stadtteildaten(stimmbezirke_df) - aggregiert auf Ortsteil-Ebene +- berechne_führende + + + +### aktualisiere_karten.R +## Wie das Programm arbeitet + +```main.R``` wird einmal aufgerufen und arbeitet dann, bis die Wahl vorbei ist +oder ein Fehler auftritt: + +- Lies zunächst die Konfigurationsdatei ein und hole Index-Dateien +- Starte eine Schleife, solange nicht alle Stimmbezirke ausgezählt sind + - Checke, ob sich der Zeitstempel der Daten verändert hat (```check_timestamp()```) + - Lies sie ein (```lies_gebiet()```) +