From 58dc5374139d469702596778efb953f8b1ce2cc7 Mon Sep 17 00:00:00 2001 From: untergeekDE <jan@eggers-elektronik.de> Date: Sun, 26 Mar 2023 17:45:33 +0200 Subject: [PATCH] Leicht angepasst nach Kassel und Darmstadt --- R/Analyse/Auswertung.R | 153 ++++++++++++++++++ R/aktualisiere_karten.R | 24 +++ R/lies_aktuellen_stand.R | 23 ++- R/lies_konfiguration.R | 14 +- R/main.R | 5 +- R/main_oneshot.R | 34 +++- R/messaging.R | 5 +- README.md | 37 ++++- howto_shapefiles.md | 12 +- index/obwahl_ffm_2023/ffm_config.csv | 37 +++++ index/obwahl_ffm_2023/ffm_config_test.csv | 37 +++++ index/obwahl_ks_2023/config.csv | 23 +++ index/obwahl_ks_2023/config_test.csv | 23 +++ index/obwahl_ks_2023/kandidaten.xlsx | Bin 6265 -> 6261 bytes .../obwahl_ks_2023/parteien-kommunalwahl.xlsx | Bin 0 -> 6155 bytes 15 files changed, 402 insertions(+), 25 deletions(-) create mode 100644 R/Analyse/Auswertung.R create mode 100644 index/obwahl_ffm_2023/ffm_config.csv create mode 100644 index/obwahl_ffm_2023/ffm_config_test.csv create mode 100644 index/obwahl_ks_2023/config.csv create mode 100644 index/obwahl_ks_2023/config_test.csv create mode 100644 index/obwahl_ks_2023/parteien-kommunalwahl.xlsx diff --git a/R/Analyse/Auswertung.R b/R/Analyse/Auswertung.R new file mode 100644 index 0000000..a93516b --- /dev/null +++ b/R/Analyse/Auswertung.R @@ -0,0 +1,153 @@ +# Auswertung nach Stadtteilen vs. Kommunalwahlergebnis 2021 + + +# Parteienliste +parteien_df <- read.xlsx("index/obwahl_da_2023/parteien-kommunalwahl.xlsx") + +# Kommunaldaten: Stadtverordnetenwahl +kommunal_url <- "https://votemanager-da.ekom21cdn.de/2021-03-14/06411000/praesentation/Open-Data-06411000-Stadtverordnetenwahl-Wahlbezirk.csv?ts=1679256774922" +k_stimm_df <- lies_stimmbezirke(kommunal_url) %>% + # Die Spalten D1-D14 enthalten die Gesamtergebnisse der Parteien. + # Unzählige weitere Spalten enthalten die Ergebnisse für jeden Kandidaten + # auf den sehr, sehr langen Wahllisten. + select(zeitstempel, + nr, + name, + meldungen_anz, + meldungen_max, + wahlberechtigt, + waehler_regulaer, + waehler_wahlschein, + waehler_nv, + stimmen, + stimmen_wahlschein, + ungueltig, + gueltig, + matches("D[0-9]+$")) %>% + mutate(nr = as.integer(str_extract(nr,"[0-9]+"))) + + +k_stadtteile_df <- k_stimm_df %>% + left_join(stimmbezirke_df %>% select(nr,ortsteilnr,stadtteil), + by="nr") %>% + group_by(ortsteilnr) %>% + # Fasse alle Spalten von meldungen_anz bis Ende der Tabelle zusammen - + # mit der sum()-Funktion (NA wird wie null behandelt) + summarize(zeitstempel = last(zeitstempel), + nr = first(ortsteilnr), + meldungen_anz = sum(meldungen_anz,na.rm =T), + meldungen_max = sum(meldungen_max,na.rm = T), + wahlberechtigt = sum(wahlberechtigt, na.rm = T), + waehler_regulaer = sum(waehler_regulaer, na.rm = T), + waehler_wahlschein = sum(waehler_wahlschein, na.rm = T), + waehler_nv = sum(waehler_nv, na.rm = T), + stimmen = sum(stimmen, na.rm = T), + stimmen_wahlschein = sum(stimmen_wahlschein, na.rm = T), + ungueltig = sum(ungueltig, na.rm = T), + gueltig = sum(gueltig, na.rm = T), + across(starts_with("D"), ~ sum(.,na.rm = T))) %>% + mutate(across(where(is.numeric), ~ifelse(is.na(.), 0, .))) %>% + # Stadtteilnamen, Geokoordinaten dazuholen + left_join(stadtteile_df, by="nr") %>% + # Wichtige Daten für bessere Lesbarkeit nach vorn + relocate(zeitstempel,nr,name,lon,lat) + +# Sicherheitscheck: Warnen, wenn nicht alle Ortsteile zugeordnet +if (nrow(stadtteildaten_df) != nrow(stadtteile_df)) teams_warnung("Nicht alle Stadtteile zugeordnet") +if (nrow(stimmbezirke_df) != length(unique(stimmbezirke_df$nr))) teams_warnung("Nicht alle Stimmbezirke zugeordnet") + + tmp_long_df <- k_stadtteile_df %>% + pivot_longer(cols = starts_with("D"), names_to = "partei_nr", values_to = "partei_stimmen") %>% + mutate(partei_nr = as.integer(str_extract(partei_nr,"[0-9]+"))) %>% + # Ortsteil- bzw. Stimmbezirks-Gruppen, um dort nach Stimmen zu sortieren + group_by(nr,name) %>% + arrange(desc(partei_stimmen)) %>% + mutate(Platz = row_number()) %>% + left_join(parteien_df %>% select(partei_nr = Nummer, + partei = Parteikürzel, + farbe= Farbwert), by="partei_nr") %>% + mutate(prozent = if_else(gueltig != 0,partei_stimmen / gueltig * 100, 0)) + k_ergänzt_df <- tmp_long_df %>% + # Ist noch nach Stadtteil (name, nr) sortiert + arrange(partei_nr) %>% + # Alles weg, was verhindert, was individuell auf den Kand ist - außer + # kand und Prozentwert + select(-partei_stimmen, -partei_nr, -Platz, -farbe) %>% + # Kandidatennamen in die Spalten zurückverteilen + pivot_wider(names_from = partei, 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, partei,prozent,farbe) %>% + # Nur die ersten (top) Plätze + filter(Platz <= (3)) %>% + #The Big Pivot: Breite die ersten (3) aus. + pivot_wider(names_from = Platz, + values_from = c(partei,prozent,farbe), + names_glue = "{.value}{Platz}") %>% + ungroup() %>% + select(-nr), + by="name") %>% + # Jetzt auswählen und umbenennen + select(nr, # ortsteilnr + k_wahlberechtigt = wahlberechtigt, + k_stimmen = stimmen, + k_stimmen_wahlschein = stimmen_wahlschein, + k_gueltig = gueltig, + ungueltig:partei3) %>% + rename(k_ungueltig = ungueltig) + +ergänzt3_df <- berechne_ergänzt(stadtteildaten_df,3) + +vergleichstabelle_df <- ergänzt3_df %>% + left_join(k_ergänzt_df,by="nr") %>% + select(-zeitstempel, + -ortsteilnr, + -meldungen_anz, + -meldungen_max, + -waehler_regulaer, + -waehler_wahlschein, + -waehler_nv) + # Gesamt-Ergebnisse ergänzen + +write.xlsx(vergleichstabelle_df,"daten/obwahl_da_2023/vergleichstabelle2021.xlsx", overwrite=T) + + +# Briefwahlergebnis +urnenwahl_df <- stimmbezirksdaten_df %>% + # Achtung: Prüfen, ob die Benennung der Briefwahllokale hierzu passt. + filter(nr < 9999) %>% + summarize(gueltig = sum(gueltig), + across(starts_with("D"),~ sum(.))) %>% + 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]+"))) %>% + left_join(kandidaten_df %>% select(kand_nr=Nummer, + Parteikürzel, + kand_name=Name),by="kand_nr") %>% + mutate(`Kandidat/in` = paste0(kand_name," (",Parteikürzel,")")) %>% + mutate(Prozent = kand_stimmen / gueltig *100) %>% + select(`Kandidat/in`, Urnenwahl = Prozent) + +briefwahl_df <- stimmbezirksdaten_df %>% + filter(nr > 9999) %>% + summarize(gueltig = sum(gueltig), + across(starts_with("D"),~ sum(.))) %>% + 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]+"))) %>% + left_join(kandidaten_df %>% select(kand_nr=Nummer, + Parteikürzel, + kand_name=Name),by="kand_nr") %>% + mutate(`Kandidat/in` = paste0(kand_name," (",Parteikürzel,")")) %>% + mutate(Prozent = kand_stimmen / gueltig *100) %>% + select(`Kandidat/in`, Briefwahl = Prozent) %>% + left_join(urnenwahl_df,by = "Kandidat/in") + + + +write.xlsx(briefwahl_df,"daten/briefwahl_ergebnis.xlsx", overwrite = T) + + \ No newline at end of file diff --git a/R/aktualisiere_karten.R b/R/aktualisiere_karten.R index 9fa8491..c3e641f 100644 --- a/R/aktualisiere_karten.R +++ b/R/aktualisiere_karten.R @@ -277,6 +277,11 @@ aktualisiere_top <- function(kand_tabelle_df,top=5) { head(top) # Daten pushen dw_data_to_chart(daten_df,chart_id = top_id) + # Daten aufs Google Bucket (für CORS-Aktualisierung) + if (SERVER) { + write.csv(daten_df,"daten/top.csv") + system('gsutil -h "Cache-Control:no-cache, max_age=0" cp daten/top.csv gs://d.data.gcp.cloud.hr.de/obwahl_top.csv') + } # Intro_Text nicht anpassen. # Balken reinrendern balken_text <- generiere_auszählungsbalken(gezaehlt,stimmbezirke_n,ts) @@ -297,6 +302,10 @@ aktualisiere_tabelle_alle <- function(kand_tabelle_df) { # Daten und Metadaten hochladen, für die Balkengrafik mit allen # Stimmen für alle Kandidaten dw_data_to_chart(kand_tabelle_df, chart_id = tabelle_alle_id) + if (SERVER) { + write.csv(kand_tabelle_df,"daten/kand_tabelle.csv") + system('gsutil -h "Cache-Control:no-cache, max_age=0" cp daten/kand_tabelle.csv gs://d.data.gcp.cloud.hr.de/obwahl_kand_tabelle.csv') + } 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) @@ -320,7 +329,12 @@ aktualisiere_karten <- function(ergänzt_df) { 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) + # Daten pushen dw_data_to_chart(ergänzt_f_df,chart_id = karte_sieger_id) + if (SERVER) { + write.csv(ergänzt_f_df,"daten/ergaenzt.csv") + system('gsutil -h "Cache-Control:no-cache, max_age=0" cp daten/ergaenzt.csv gs://d.data.gcp.cloud.hr.de/obwahl_ergaenzt.csv') + } dw <- dw_publish_chart(karte_sieger_id) # Jetzt die Choropleth-Karten für alle Kandidierenden for (i in 1:nrow(switcher_df)) { @@ -333,7 +347,12 @@ aktualisiere_karten <- function(ergänzt_df) { aktualisiere_hochburgen <- function(hochburgen_df) { # Das ist ziemlich geradeheraus. + # Pushe Daten. dw_data_to_chart(hochburgen_df, chart_id = hochburgen_id) + if (SERVER) { + write.csv(hochburgen_df,"daten/hochburgen.csv") + system('gsutil -h "Cache-Control:no-cache, max_age=0" cp daten/hochburgen.csv gs://d.data.gcp.cloud.hr.de/obwahl_hochburgen.csv') + } balken_text <- generiere_auszählung_nurtext(gezaehlt,stimmbezirke_n,ts) # Metadaten anpassen: Farbcodes für Parteien metadata_chart <- dw_retrieve_chart_metadata(hochburgen_id) @@ -430,7 +449,12 @@ aktualisiere_ergebnistabelle <- function(stadtteildaten_df) { ungroup() %>% arrange(sort) %>% select(-name,-sort) + # Daten pushen dw_data_to_chart(ergebnistabelle_df %>% select(-nr), chart_id = tabelle_stadtteile_id) + if (SERVER) { + write.csv(ergebnistabelle_df,"daten/stadtteile.csv") + system('gsutil -h "Cache-Control:no-cache, max_age=0" cp daten/stadtteile.csv gs://d.data.gcp.cloud.hr.de/obwahl_stadtteile.csv') + } # 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(.) diff --git a/R/lies_aktuellen_stand.R b/R/lies_aktuellen_stand.R index 2ffc5fc..685c7c6 100644 --- a/R/lies_aktuellen_stand.R +++ b/R/lies_aktuellen_stand.R @@ -85,7 +85,7 @@ check_for_timestamp <- function(my_url) { # } 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) + # parse_date_time("%a, %d %m %Y %H:%M:%S",tz = "CET") # } } else { # lokale Datei t = file.info(my_url)$mtime %>% as_datetime @@ -102,14 +102,15 @@ lies_stimmbezirke <- function(stand_url = stimmbezirke_url) { #' 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, + { + stand_df <- read_delim(stand_url, delim = ";", escape_double = FALSE, locale = locale(date_names = "de", decimal_mark = ",", grouping_mark = "."), trim_ws = TRUE) %>% # Spalten umbenennen, Zeitstempel-Spalte einfügen - mutate(zeitstempel=ts) %>% + mutate(zeitstempel=ts) %>% # Sonderregel: wir haben einen Zeitstempel, die "datum"-Spalte macht # Probleme, weil: starts_with("D"). select(-datum) %>% @@ -129,7 +130,9 @@ lies_stimmbezirke <- function(stand_url = stimmbezirke_url) { ungueltig = C, gueltig = D, # neu: alle Zeilen mit Stimmen (D1..Dn) - starts_with("D")) + starts_with("D")) %>% + # Zusatz für Frankfurt, das die Stimmbezirksnummern als character überträgt + mutate(nr = as.integer(nr)) }, warning = function(w) {teams_warning(w,title="OB-Wahl: Datenakquise")}, @@ -166,8 +169,8 @@ aggregiere_stadtteildaten <- function(stimmbezirksdaten_df = stimmbezirksdaten_d relocate(zeitstempel,nr,name,lon,lat) # Sicherheitscheck: Warnen, wenn nicht alle Ortsteile zugeordnet - if (nrow(stadtteildaten_df) != nrow(stadtteile_df)) teams_warnung("Nicht alle Stadtteile zugeordnet") - if (nrow(stimmbezirke_df) != length(unique(stimmbezirke_df$nr))) teams_warnung("Nicht alle Stimmbezirke zugeordnet") + if (nrow(stadtteildaten_df) != nrow(stadtteile_df)) teams_warning("Nicht alle Stadtteile zugeordnet") + if (nrow(stimmbezirke_df) != length(unique(stimmbezirke_df$nr))) teams_warning("Nicht alle Stimmbezirke zugeordnet") cat("Stadtteildaten aggregiert.\n") return(stadtteildaten_df) } @@ -237,7 +240,9 @@ berechne_kand_tabelle <- function(stimmbezirksdaten_df = stimmbezirksdaten_df) { left_join(kandidaten_df %>% select(Nummer, Name, Parteikürzel, Farbwert), by="Nummer") %>% mutate(name = paste0(Name," (",Parteikürzel,")")) %>% - mutate(Prozent = Stimmen / gueltig * 100) %>% + mutate(Prozent = ifelse(gueltig > 0, + Stimmen / gueltig * 100, + 0)) %>% select(Nummer, `Kandidat/in` = name, Parteikürzel, Stimmen, Prozent) cat("Gesamttabelle alle Kandidaten berechnet.\n") return(kand_tabelle_df) @@ -355,8 +360,10 @@ hole_wahldaten <- function() { } # Stadtteil neu ausgezählt? } - meldung_s <- paste0(meldung_s,"<br><br>", + if (!exists("NO_SOCIAL")) { + meldung_s <- paste0(meldung_s,"<br><br>", generiere_socialmedia()) + } teams_meldung(meldung_s,title=wahl_name) check = tryCatch( diff --git a/R/lies_konfiguration.R b/R/lies_konfiguration.R index b36523a..4c4d629 100644 --- a/R/lies_konfiguration.R +++ b/R/lies_konfiguration.R @@ -25,10 +25,20 @@ # - wahlberechtigt - Zahl der Wahlberechtigen (kommt Sonntag) # - briefwahl - Zahl der Briefwahlstimmen (kommt Sonntag) +# Falls der Parameter wahl_name noch nicht definiert ist, +# setze ihn erst mal auf das derzeitige Verzeichnis. +if (exists("wahl_name")) { + index_pfad = paste0("index/",wahl_name,"/") +} else { + index_pfad = paste0("index/") +} + +# Lies die Indexdatei aus dem Verzeichnis wahl_name. +# Falls keines angegeben: aus dem aktuellen Verzeichnis if (TEST) { - config_df <- read_csv("index/config_test.csv") + config_df <- read_csv(paste0(index_pfad,"config_test.csv")) } else { - config_df <- read_csv("index/config.csv") + config_df <- read_csv(paste0(index_pfad,"config.csv")) } for (i in c(1:nrow(config_df))) { # Erzeuge neue Variablen mit den Namen und Werten aus der CSV diff --git a/R/main.R b/R/main.R index 695ed73..a4cb93c 100644 --- a/R/main.R +++ b/R/main.R @@ -16,7 +16,7 @@ p_load(R.utils) rm(list=ls()) TEST = FALSE -DO_PREPARE_MAPS = TRUE +DO_PREPARE_MAPS = FALSE @@ -85,7 +85,7 @@ if (DO_PREPARE_MAPS) { while (gezaehlt < stimmbezirke_n) { check = tryCatch( { # Zeitstempel der Daten holen - ts_daten <- check_for_timestamp(stimmbezirke_url) + ts_daten <- check_for_timestamp(stimmbezirke_url) + hours(1) }, warning = function(w) {teams_warning(w,title=paste0(wahl_name,": CURL-Polling"))}, error = function(e) {teams_warning(e,title=paste0(wahl_name,": CURL-Polling"))} @@ -107,6 +107,7 @@ dw_publish_chart(top_id) # Logging beenden if (!TEST) { + cat("OK: FERTIG - alle Stimmbezirke ausgezählt: ",as.character(ts),"\n") sink() sink(type="message") file.rename("obwahl.log","obwahl_success.log") diff --git a/R/main_oneshot.R b/R/main_oneshot.R index d4914a8..b23dd48 100644 --- a/R/main_oneshot.R +++ b/R/main_oneshot.R @@ -11,19 +11,41 @@ p_load(DatawRappr) p_load(curl) p_load(magick) p_load(openxlsx) +p_load(R.utils) 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("..") +# Lies Kommandozeilen-Parameter: +# (Erweiterte Funktion aus dem R.utils-Paket) +args = R.utils::commandArgs(asValues = TRUE) +if (length(args)!=0) { + if (any(c("h","help","HELP") %in% names(args))) { + cat("Parameter: \n", + "--TEST schaltet Testbetrieb ein\n", + "--DO_PREPARE_MAPS schaltet Generierung der Switcher ein\n", + "wahl_name=<name> holt Index-Dateien aus dem Verzeichnis ./index/<name>\n\n") + } + TEST <- "TEST" %in% names(args) + DO_PREPARE_MAPS <- "DO_PREPARE_MAPS" %in% names(args) + if ("wahl_name" %in% names(args)) { + wahl_name <- args[["wahl_name"]] + if (!dir.exists(paste0("index/",wahl_name))) stop("Kein Index-Verzeichnis für ",wahl_name) + } +} + +# Defaults +if (!exists("wahl_name")) wahl_name = "obwahl_ffm_stichwahl_2023" +if (!exists("TEST")) TEST = FALSE +if (!exists("DO_PREPARE_MAPS")) DO_PREPARE_MAPS = FALSE +NO_SOCIAL = TRUE + + + # Logfile anlegen, wenn kein Test # if (!TEST) { # logfile = file("obwahl.log") @@ -92,4 +114,4 @@ ts <- ts_daten hole_wahldaten() -# EOF \ No newline at end of file +# EOF diff --git a/R/messaging.R b/R/messaging.R index 77807ca..80d8784 100644 --- a/R/messaging.R +++ b/R/messaging.R @@ -22,6 +22,7 @@ if (Sys.getenv("WEBHOOK_OBWAHL") == "") { teams_meldung <- function(...,title="OB-Wahl-Update") { cc <- teamr::connector_card$new(hookurl = t_txt) + if (TEST) {title <- paste0("TEST: ",title) } cc$title(paste0(title," - ",lubridate::with_tz(lubridate::now(), "Europe/Berlin"))) alert_str <- paste0(...) @@ -32,13 +33,13 @@ teams_meldung <- function(...,title="OB-Wahl-Update") { teams_error <- function(...) { alert_str <- paste0(...) - teams_meldung(title="OB-Wahl: FEHLER: ", ...) + teams_meldung("***FEHLER: ",...) stop(alert_str) } teams_warning <- function(...) { alert_str <- paste0(...) - teams_meldung("OB-Wahl: WARNUNG: ",...) + teams_meldung("***WARNUNG: ",...) warning(alert_str) } diff --git a/README.md b/README.md index 32998ac..aae3d03 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,18 @@ Grafiken: * Tabelle nach Kandidaten (3 beste, 3 schlechteste Stadtteile) * Tabelle alle Ergebnisse nach Stadtteil +### Aktualisierung via CORS/GBucket + +Der normale Weg, eine Datawrapper-Grafik anzuzeigen, ist: pushe die Daten auf den Datawrapper-Server - mit dw_data_to_chart() - und aktualisiere. + +Alternativ kann die Grafik aus live bereitgestellten Daten in einem Google Bucket bestückt werden. Die Adressen der Dateien, die an die Grafiken übergeben werden müssen, sind: + +- https://d.data.gcp.cloud.hr.de/obwahl_top.csv +- https://d.data.gcp.cloud.hr.de/obwahl_kand_tabelle.csv +- https://d.data.gcp.cloud.hr.de/obwahl_ergaenzt.csv +- https://d.data.gcp.cloud.hr.de/obwahl_hochburgen.csv +- https://d.data.gcp.cloud.hr.de/obwahl_stadtteile.csv + ### Konfiguration Das Programm holt sich seine Daten aus einer Konfigurationsdatei - entweder für den Live- oder den Testbetrieb, was über die Variable TEST im Progammcode umgestellt wird. Die Indizes für die jeweile Wahl - Kandidatinnen und Kandidaten, Stadtteile und Wahllokal-Zuordnungen - liegen in einem Unterordner mit dem Namen der Wahl, als CSV oder XLSX. @@ -86,7 +98,7 @@ Spalte | Wert ---- | ---- nr | ID des Stimmbezirks ortsteilnr | ID des Stadtteils -ortsteil Name des Stadtteils +ortsteil | Name des Stadtteils Nicht benötigte Spalten können in der Tabelle bleiben, sollten aber möglichst nicht "name" oder so heißen. @@ -107,11 +119,28 @@ Aggregation auf Stadtebene (siehe ["Sitemap"](./sitemap.md) für den Code) +## Vorbereitung einer Wahl + +- Shapefile für die Stadt besorgen; Stimmbezirksebene; Stadtteile +- Stadtteile aggregieren, GEOJSON generieren +- Ordner für die Wahl im Index-Ordner; Datei Kandidaten, Stadtteile (mit Geokoordinaten für die Zentrumspunkte), Stimmbezirke (mit Zuordnung Stadtteil) +- Kopien für die vier Grafiken anlegen: Top, alle Stimmen, Hochburgen, Stadtteile. Link zum Wahlamt nicht vergessen. +- Leerdatei Ergebnisse nach Stadtteil vorbereiten +- Kopie der Sieger-Karte mit GEOJSON anlegen; GEOJSON hochladen, Leerdatei hochladen. Link zum Wahlamt korrigieren. +- Eine erste Kopie der Choropleth-Karten nach Kandidat: Wahlamt-Link ändern und Karte und Leerdatei hochladen, dann kopieren. +- Kopien für alle Kandidierenden anlegen. Jeweils die Werte-Spalte des jeweiligen Kandidaten auswählen; benennen, um sie zuordnen zu können. (Farben und Namen werden automatisch nachgetragen.) +- Indexdatei vorbereiten: Wahlname, Anzahl TOP, Dateinahmen der Index-Dateien, Datawrapper-IDs für die Karten und Diagramme + +## TODO + +- Analyse: Weshalb hängt das Polling manchmal hinterher? +- Aufruf mit Parametern ermöglichen ("main.R obwahl_ffm_2023") +- Oneshot-Variante für Kassel + +- Auswertung Briefwahldaten ## Nice-To-Have -- Vergleich letzte Kommunalwahl - 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 +- Vergleich letzte Kommunalwahl regulär \ No newline at end of file diff --git a/howto_shapefiles.md b/howto_shapefiles.md index c75fe0a..9814772 100644 --- a/howto_shapefiles.md +++ b/howto_shapefiles.md @@ -8,6 +8,8 @@ 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. +Nach den Attributen schauen - die Ortsteilnr. ist ein String, kein Integer! Rechtsklick auf den neuen Layer; Eigenschaften..., dann den "Felder..."-Editor, da oben auf das kleine Abakus-Symbol klicken, Namen für das neue Feld in Ausgabefeldname (z.B. "nr"), dann in Feld Ausdruck eintragen"to_int(Ortsbezirk)" - und OK klicken. Neues Feld wird angelegt. Dann das alte Feld löschen (auswählen, oben Klick auf Löschen-Feld). + - 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 @@ -22,4 +24,12 @@ Dann noch Geokoordinaten der Zentroidpunkte: Rechte Seite die Toolbox, dort "Vek 5. CSV-/XLSX-Dateien putzen - Brauchen eine Stadtteil-Datei mit nr,name,lon,lat (erzeugt aus den Zentroiden) -- Brauchen einen Wahlbezirks-Zuordnung \ No newline at end of file +- Brauchen einen Wahlbezirks-Zuordnung + + +6. Reparatur der Darmstadt-Karte + +- Laden (falsche Geometrie - das erst zum Schluss fixen!) +- Vereinfachen: Fläche +- Auflösen +- Löcher löschen \ No newline at end of file diff --git a/index/obwahl_ffm_2023/ffm_config.csv b/index/obwahl_ffm_2023/ffm_config.csv new file mode 100644 index 0000000..2f97d66 --- /dev/null +++ b/index/obwahl_ffm_2023/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/obwahl_ffm_2023/ffm_config_test.csv b/index/obwahl_ffm_2023/ffm_config_test.csv new file mode 100644 index 0000000..07d5d1a --- /dev/null +++ b/index/obwahl_ffm_2023/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_ks_2023/config.csv b/index/obwahl_ks_2023/config.csv new file mode 100644 index 0000000..cf31ec1 --- /dev/null +++ b/index/obwahl_ks_2023/config.csv @@ -0,0 +1,23 @@ +name,value,comment +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,ks-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,gJNPD, +social1_id,Ts1oS,5 stärkste +social2_id,S9BbQ,Alle Stimmen angepasst diff --git a/index/obwahl_ks_2023/config_test.csv b/index/obwahl_ks_2023/config_test.csv new file mode 100644 index 0000000..8c2e436 --- /dev/null +++ b/index/obwahl_ks_2023/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/obwahl_ks_2023/kandidaten.xlsx b/index/obwahl_ks_2023/kandidaten.xlsx index e10b674797487ee43e2033972e604fe8f5e8c83b..f1c1eac6703306255f182e4ea891e49f72ddc201 100644 GIT binary patch delta 2843 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 delta 2822 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 diff --git a/index/obwahl_ks_2023/parteien-kommunalwahl.xlsx b/index/obwahl_ks_2023/parteien-kommunalwahl.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..5b382a6653d2514848c5bbbe8651c2b62bdb5d04 GIT binary patch literal 6155 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 SBqSX85e%o8XK=X&3F%*>WQLyr literal 0 HcmV?d00001 -- GitLab