From 9fa45f85b5ba4827d3a9f9bb704a076a06f2d4ef Mon Sep 17 00:00:00 2001 From: untergeekDE <jan@eggers-elektronik.de> Date: Sat, 11 Mar 2023 15:32:24 +0100 Subject: [PATCH] =?UTF-8?q?V1.0=20-=20getestet=20f=C3=BCr=20Kassel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 11 + R/aktualisiere_karten.R | 482 ++++++++++++++++-- R/generiere_testdaten.R | 140 ----- R/its_alive.R | 41 ++ R/lies_aktuellen_stand.R | 374 +++++++++++--- R/lies_konfiguration.R | 87 ++++ R/main.R | 117 +++++ R/main_oneshot.R | 95 ++++ R/messaging.R | 15 +- R/update_all.R | 203 -------- README.md | 37 +- howto_shapefiles.md | 26 + index/config.csv | 29 +- index/config_test.csv | 23 + index/ffm_config.csv | 37 ++ index/ffm_config_test.csv | 37 ++ index/obwahl_ffm_2023/kandidaten.xlsx | Bin 0 -> 6920 bytes index/obwahl_ffm_2023/stadtteile.csv | 45 ++ .../zuordnung_wahllokale.csv | 0 index/obwahl_ks_2023/kandidaten.xlsx | Bin 0 -> 6265 bytes ...tadtteile-mit-d\303\266nchelandschaft.csv" | 25 + index/obwahl_ks_2023/ks-stadtteile.csv | 24 + index/obwahl_ks_2023/wahlbezirke.xlsx | Bin 0 -> 10250 bytes index/stadtteile.csv | 45 -- sitemap.md | 41 ++ 25 files changed, 1394 insertions(+), 540 deletions(-) delete mode 100644 R/generiere_testdaten.R create mode 100644 R/its_alive.R create mode 100644 R/lies_konfiguration.R create mode 100644 R/main.R create mode 100644 R/main_oneshot.R delete mode 100644 R/update_all.R create mode 100644 howto_shapefiles.md create mode 100644 index/config_test.csv create mode 100644 index/ffm_config.csv create mode 100644 index/ffm_config_test.csv create mode 100644 index/obwahl_ffm_2023/kandidaten.xlsx create mode 100644 index/obwahl_ffm_2023/stadtteile.csv rename index/{ => obwahl_ffm_2023}/zuordnung_wahllokale.csv (100%) create mode 100644 index/obwahl_ks_2023/kandidaten.xlsx create mode 100644 "index/obwahl_ks_2023/ks-stadtteile-mit-d\303\266nchelandschaft.csv" create mode 100644 index/obwahl_ks_2023/ks-stadtteile.csv create mode 100644 index/obwahl_ks_2023/wahlbezirke.xlsx delete mode 100644 index/stadtteile.csv create mode 100644 sitemap.md diff --git a/.gitignore b/.gitignore index fae8299..5cc2df5 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 28626b7..406c3bb 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 3eed958..0000000 --- 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 0000000..750c52b --- /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 70b7bca..2ffc5fc 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 0000000..b36523a --- /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 0000000..6ddc7f1 --- /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 0000000..d4914a8 --- /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 b2b93b0..77807ca 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 e0dd5d6..0000000 --- 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 b56eda9..f74ad16 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 0000000..b60bcfd --- /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 0409078..45880e5 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 0000000..8c2e436 --- /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 0000000..2f97d66 --- /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 0000000..07d5d1a --- /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 GIT binary patch literal 6920 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 literal 0 HcmV?d00001 diff --git a/index/obwahl_ffm_2023/stadtteile.csv b/index/obwahl_ffm_2023/stadtteile.csv new file mode 100644 index 0000000..b07902a --- /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 GIT binary patch literal 6265 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 I>CN5$0Y}@)O#lD@ literal 0 HcmV?d00001 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 0000000..55d7917 --- /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 0000000..ddb583e --- /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 GIT binary patch literal 10250 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 R4h|jqdInu#+#bTv{{yp(uV?@O literal 0 HcmV?d00001 diff --git a/index/stadtteile.csv b/index/stadtteile.csv deleted file mode 100644 index 66dba9e..0000000 --- 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 0000000..316479b --- /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()```) + -- GitLab