Skip to main content
Sign in
Snippets Groups Projects
Commit 6503c58f authored by untergeekDE's avatar untergeekDE
Browse files

getestete Version 1.9

parent 0e875a3e
No related branches found
No related tags found
No related merge requests found
Showing
with 4327 additions and 10 deletions
#' aktualisiere_karten.R
aktualisiere_karten <- function(wl_url = wahllokale_url) {
# Lies Ortsteil-Daten ein und vergleiche
neue_orts_df <- lies_gebiet(wl_url) %>%
aggregiere_stadtteile() %>%
mutate(quorum = ifelse(wahlberechtigt == 0,
NA,
ja / wahlberechtigt * 100)) %>%
mutate(status = ifelse(meldungen_anz == 0,
NA,
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)
} 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)
}
}
\ No newline at end of file
library(readr)
library(lubridate)
library(tidyr)
library(stringr)
library(dplyr)
# Hilfsroutinen, die die Index-Dateien generieren
source("lies_aktuellen_stand.R")
tmp_df <- lies_stand(stand_url)
#---- Generiere Zentroide aus JSON ----
library(jsonlite)
# Hole die Wahlergebnisse der OB-Wahl 2018 - mit den Wählerzahlen.
#
# Nach Auskunft von Michael Wolfsteiner, Leiter des Wahlamts,
# ist die Zahl der Wahlberechtigen derzeit bei ca. 510.000
# (endgültig steht das erst am 6.11. um 18 Uhr fest).
# Daran gemessen sind die hier verwendeten Zahlen um ca 1% zu niedrig -
# auch wenn es nach Stadtteil stärker schwanken wird:
# ich glaube, das kann man verschmerzen.
ob2018stadtteile <- read_delim("index/ob2018stadtteile.csv",
delim = ";",
escape_double = FALSE,
locale = locale(date_names = "de",
decimal_mark = ",",
grouping_mark = ".",
encoding = "ISO-8859-1"),
trim_ws = TRUE)
tmp <- fromJSON("shapefile/zentroide.geojson")
# unnest_wider dauert eeeewig lang, aber funktioniert
stadtteile_df <- tibble(nr=tmp$features$properties$STT,
name = tmp$features$properties$NAME,
latlon = tmp$features$geometry$coordinates) %>%
unnest_wider(latlon) %>%
rename(lat =4, lon = 3) %>%
left_join(ob2018stadtteile %>%
select(nr = Stadtteilnummer,
wahlberechtigt_2018 = 5,
waehler_2018 = 6,
gueltig_2018 = 11,
feldmann_2018 = 14),
by = "nr")
# df enthält jetzt:
# - nr (des Stadtteils)
# - name
# - lat
# - lon
write_csv(stadtteile_df,"index/stadtteile.csv")
#---- Die Index-Daten alle in ein handliches .rda verpacken----
stadtteile_df <- read_csv("index/stadtteile.csv")
zuordnung_wahllokale_df <- read_csv("index/zuordnung_wahllokale.csv")
opendata_wahllokale_df <- read_csv2("index/opendata-wahllokale.csv")
save(stadtteile_df,zuordnung_wahllokale_df,opendata_wahllokale_df,file ="index/index.rda")
library(readr)
library(lubridate)
library(tidyr)
library(stringr)
library(dplyr)
rm(list=ls())
source("R/messaging.R")
source("R/lies_aktuellen_stand.R")
source("R/aktualisiere_karten.R")
#----aktualisiere_fom() ----
aktualisiere_fom <- function() {
# 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("2022-11-02 18:00:00 CET"),
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,
wahler_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)
wahllokale_df <- lies_gebiet(wahllokale_url)
neue_fom_df <- wahllokale_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(wahllokale_df,"archiv/wahllokale/")
# 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
# Aktualisiere auch die Stadtteilkarten
aktualisiere_karten()
# Sende die Daten an Datawrapper und aktualisiere
fom_dw_df <- fom_df %>%
mutate(ausgezählt = wahlberechtigt / ffm_waehler *100) %>%
mutate(prozent30 = wahlberechtigt * 0.3) %>%
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)
dw_publish_chart(fom_id)
# Teams-Meldung
teams_meldung(title="Feldmann-o-meter","Update: ",
floor(neue_fom_df$wahlberechtigt/ ffm_waehler * 100),
"% ausgezählt")
}
}
#---- MAIN ----
# Ruft aktualisiere_fom() auf
# (die dann wieder aktualisiere_karten() aufruft)
#' generiere_balken.R
#'
#' Hilfsfunktionen für die Grafikdarstellung in Datawrapper
#' Produzieren den Code für die Fake-Balken
#'
# Hilfsfunktion: Die Prozent-Balken ja/nein generieren
#
# Konstante: Breite der Pufferzellen in px
puffer = 100
generiere_balken <- function (wb, ja, nein, auszählung_beendet) {
if (wb == 0) {
quorum = 0
} else {
quorum = (ja / wb * 100)
}
ja_breite_prozent <- ifelse(quorum >= 30,
100,
floor(quorum / 0.3))
# Nein-Balken:
nein_breite_prozent <- floor(ifelse(nein==0,0, nein / ja) *
# Berechnen als Anteil des Ja-Balkens
ifelse(quorum >= 30,
# Wenn Quorum erreicht, nimmt Ja-Balken volle Breite ein -
100,
# sonst nur diese Breite -
ja_breite_prozent))
# FALLS mehr Nein-Stimmen als Ja-Stimmen abgegeben werden,
# könnte der Algorithmus inkorrekte Daten darstellen, deshlab der Cap bei 100.
if (nein_breite_prozent > 100) { nein_breite_prozent <- 100}
# Funktion in der Funktion: Die Markierungs-Balken brauchen wir 2x.
# Wenn das Quorum erreicht ist, brauchen wir zwei Zellen, um den Strich zu
# produzieren:
markierungsbalken <- function(quorum) {
mbalkencode <- ifelse(quorum >= 30,
# Wenn Quorum erreicht (ja-Balken 100%),
# brauchen wir zwei Zellen, um die Markierung darzustellen
paste0(
"<span style='width:",
# Welchen Anteil haben die 30% am tatsächlich erreichten Quorum?
floor(30 / quorum * 100), # Integer!
"%; height:8px;border-right: 4px solid #000;
;text-align:center;'></span>",
"<span style ='width:",
100 - floor(30 / quorum * 100),
"%; height:8px;'></span>"),
# Nur eine Zelle (100%), wenn das Quorum nicht erreicht ist
"<span style='width:100%;height:8px;border-right:4px solid #000;'></span>")
return(paste0(
# Container
"<span style='height:8px;display: flex;justify-content: space-around;align-items: flex-end; width: 100%;'>",
# Puffer-Zelle linke Spalte
"<span style='width:",
puffer,
"px; '>&nbsp;</span>",
mbalkencode,
"</span>"))
}
# Den Fake-Balken-Code generieren
balkencode <- paste0(
"Ja-Stimmen: (",
# Läuft die Auszählung noch?
ifelse(auszählung_beendet,"","derzeit "),
format(quorum,decimal.mark=",",nsmall=1, digits=3),
"% der Wahlberechtigten; Quorum ",
ifelse(quorum >= 30,
"erreicht)",
"nicht erreicht)"),
# Container Fakebalken 1: Ja-Stimmen
"<span style='height:32px;display:flex;flex-direction:column;width:100%;'>",
markierungsbalken(quorum),
# Container Ja-Stimmen-Balken
"<span style='height:16px;display: flex;justify-content: space-around;align-items: flex-end; width: 100%;'>",
# Pufferzelle mit Stimmenzahl
"<span style='width:",puffer,"px; text-align:left;font-size:90%;'>",
format(ja,decimal.mark = ",",big.mark = "."),
"</span>",
# Blauer Balken
"<span style='width:",
ja_breite_prozent, #integer!
"%; background:#005293; height:16px;'></span>",
# Grauer Balken
"<span style='width:",
100-ja_breite_prozent, #integer!
"%; background:#a6abb0; height:16px;",
# Wenn Quorum nicht erreicht, Zielmarken-Strich am rechten Rand des Balkens
ifelse(quorum < 30, "border-right: 4px solid #000;",""),
"'></span>",
# Ende Container Ja-Stimmen-Balken
"</span>",
markierungsbalken(quorum),
# Ende Container Fakebalken 1
"</span>",
"Nein-Stimmen:",
# Container Fakebalken 2
"<span style='height:32px;display:flex;flex-direction:column;width:100%;'>",
# Container Nein-Stimmen-Balken
"<span style='height:16px;display: flex;justify-content: space-around;align-items: flex-end; width: 100%;'>",
# Pufferzelle mit Stimmenzahl
"<span style='width:",puffer,"px; text-align:left;font-size:90%;'>",
format(nein,decimal.mark = ",",big.mark = "."),
"</span>",
# Roter Balken
"<span style='width:",
nein_breite_prozent, #integer!
"%; background:#d34600; height:16px;'></span>",
# Grauer Balken
"<span style='width:",
100-nein_breite_prozent, #integer!
"%; background:#a6abb0; height:16px;'></span>",
# Ende Container Ja-Stimmen-Balken
"</span>",
# Ende Container Fakebalken 2
"</span>",
"Verhältnis Ja:Nein: ",
ifelse(ja+nein == 0, "", format(ja/(ja+nein)*100,decimal.mark=",",nsmall=1, digits=3)),"%:",
ifelse(ja+nein == 0, "", format(nein/(ja+nein)*100,decimal.mark=",",nsmall=1, digits=3)),"%"
)
}
generiere_auszählungsbalken <- function(ausgezählt,anz,max,ts) {
annotate_str <- paste0("Anteil der Wahlberechtigten, die die Auszählung umfasst",
# 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;'>",
ausgezählt,
"%</span>",
# dunkelblauer Balken
"<span style='width:",
ausgezählt,
"%; background:#002747; height:16px;'></span>",
# grauer Balken
"<span style='width:",
100-ausgezählt,
"%; background:#CCC; height:16px;'></span>",
# Hintere Pufferzelle 5px
"<span style='width:5px;'></span>",
# Ende Fake-Balken
"</span>",
"<br><br><strong>Stand: ",
format.Date(ts, "%d.%m.%y, %H:%M Uhr"),
"</strong> - ",
anz," von ",max,
" Wahllokalen ausgezählt<br>"
)
}
......@@ -12,6 +12,27 @@ 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,
......@@ -23,7 +44,6 @@ vorlage_wahllokale_df <- read_delim("testdaten/Open-Data-06412000-Buergerentsche
wahllokale_max <- sum(vorlage_wahllokale_df$`max-schnellmeldungen`)
# Konstanten für die Simulation - werden jeweils um bis zu +/-25% variiert
rand <- 0.5 # Wahrscheinlichkeit für eine neue "Meldung" bei 1/2
c_wahlberechtigt = 510000 / wahllokale_max # Gleich große Wahlbezirke
c_wahlbeteiligung = 0.31 # Wahlbeteiligung um 31%
c_wahlschein = 0.25 # 25% Briefwähler
......@@ -47,6 +67,8 @@ i = 1
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
......@@ -91,8 +113,24 @@ while(sum(vorlage_wahllokale_df$`anz-schnellmeldungen`) < wahllokale_max) {
arrange(`gebiet-nr`)
write_csv2(vorlage_wahllokale_df,
paste0("testdaten/wahllokale",i,".csv"),
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
}
......
......
library(readr)
library(lubridate)
library(tidyr)
library(stringr)
library(dplyr)
# 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)
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/wahllokale") {
if (!dir.exists(a_directory)) {
dir.create(a_directory)
teams_warning(a_directory," neu angelegt")
}
write_csv(df,
paste0(a_directory,"/",
# Zeitstempel isolieren und alle Doppelpunkte
# durch Bindestriche ersetzen
str_replace_all(df %>% pull(zeitstempel) %>% last(),
"\\:","_"),
".csv"))
}
hole_letztes_df <- function(a_directory = "daten/wahllokale") {
if (!dir.exists(a_directory)) return(tibble())
neuester_file <- list.files(a_directory, full.names=TRUE) %>%
file.info() %>%
# Legt eine Spalte namens path an
tibble::rownames_to_column(var = "path") %>%
arrange(desc(ctime)) %>%
head(1) %>%
# Pfad wieder rausziehen
pull(path)
if (length(neuester_file)==0) {
# Falls keine Daten archiviert, gibt leeres df zurück
return(tibble())
} else {
return(read_csv(neuester_file))
}
}
#---- Lese-Funktionen ----
lies_gebiet <- function(stand_url = wahllokale_url) {
ts <- now()
# Versuch Daten zu lesen - und gib ggf. Warnung oder Fehler zurück
check = tryCatch(
{ stand_df <- read_delim(stand_url,
delim = ";", escape_double = FALSE,
locale = locale(date_names = "de",
decimal_mark = ",",
grouping_mark = "."),
trim_ws = TRUE) %>%
# Spalten umbenennen, Zeitstempel-Spalte einfügen
mutate(zeitstempel=ts) %>%
select(zeitstempel,
nr = `gebiet-nr`,
name = `gebiet-name`,
meldungen_anz = `anz-schnellmeldungen`,
meldungen_max = `max-schnellmeldungen`,
# Ergebniszellen
wahlberechtigt = A,
# Mehr zum Wahlschein hier: https://www.bundeswahlleiter.de/service/glossar/w/wahlscheinvermerk.html
waehler_regulaer = A1,
waehler_wahlschein = A2,
waehler_nv = A3,
stimmen = B,
stimmen_wahlschein = B1,
ungueltig = C,
gueltig = D,
ja = D1,
nein = D2)
},
warning = function(w) {teams_warning(w,title="Feldmann: Datenakquise")},
error = function(e) {teams_warning(e,title="Feldmann: Datenakquise")})
# Spalten umbenennen,
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 Wahllokale, gibt nach Ortsteil aggregierte Daten zurück
#' (hier: kein Sicherheitscheck)
aggregiere_stadtteile <- function(wahllokale_df) {
ortsteile_df <- wahllokale_df %>%
left_join(zuordnung_wahllokale_df,by=c("nr","name")) %>%
group_by(ortsteilnr) %>%
summarize(zeitstempel = last(zeitstempel),
across(meldungen_anz:nein, ~ sum(.,na.rm = T))) %>%
rename(nr = ortsteilnr) %>%
# Stadtteilnamen, 2018er Ergebnisse, 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_wahllokale_df) != length(unique(wahllokale_df$nr))) teams_warnung("Nicht alle Wahllokale zugeordnet")
return(ortsteile_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)
}
library(readr)
library(lubridate)
library(tidyr)
library(stringr)
library(dplyr)
library(teamr)
#' messaging.R
#'
#' Kommunikation mit Teams
#'
#' Webhook wird als
#'
#'
teams_meldung <- function(...,title="Feldmann-Update") {
cc <- teamr::connector_card$new(hookurl = Sys.getenv("WEBHOOK_REFERENDUM"))
cc$title(paste0(title," - ",lubridate::with_tz(lubridate::now(),
"Europe/Berlin")))
alert_str <- paste0(...)
cc$text(alert_str)
cc$print()
cc$send()
}
teams_error <- function(...) {
alert_str <- paste0(...)
teams_meldung(title="Feldmann: FEHLER: ", ...)
stop(alert_str)
}
teams_warning <- function(...) {
alert_str <- paste0(...)
teams_meldung("Feldmann: WARNUNG: ",...)
warning(alert_str)
}
library(readr)
library(lubridate)
library(tidyr)
library(stringr)
library(dplyr)
library(DatawRappr)
rm(list=ls())
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 = wahllokale_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 = 999,
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)
wahllokale_df <- lies_gebiet(wl_url)
neue_fom_df <- wahllokale_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(wahllokale_df,"dateh/wahllokale/")
# 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 = wahlberechtigt / ffm_waehler *100) %>%
mutate(prozent30 = wahlberechtigt * 0.3) %>%
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 wäre Peter Feldmann als OB abgewählt."
}
}
}
# 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 &quot;Ja&quot; 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
)
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(wahllokale_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(wahllokale_url)
},
warning = function(w) {teams_warning(w,title="Feldmann: Karten")},
error = function(e) {teams_warning(e,title="Feldmann: Karten")})
if (neue_daten) {
teams_meldung("Daten aktualisiert - OK",title="Feldmann-Referendum")
} else {
teams_warning("Neue Wahllokal-Daten, aber keine neuen Ortsdaten?")
}
}
# Auch hier TRUE zurückbekommen;; alles OK?
......@@ -2,11 +2,31 @@
R-Routinen, um die Abstimmungsergebnisse des Referendums in Frankfurt am 6. November 2022 auf die hessenschau.de-Website zu bringen.
## Todo
- Fehlerbehandlung für aktualisiere_fom() und aktualisiere_karten()
- Barchart-Generierung in fom()
- Ja-Stimmen in fom()
- Daten in der Tooltipps-Box in den Karten
V1.0 - Fragen und Anmerkungen jan.eggers (klammeraffe) hr.de
## Aufbau
Das Skript update_feldmann() ist dazu gedacht, 1x pro Minute aufgerufen zu werden.
Es lädt die Wahllokal-Daten und vergleicht sie mit dem letzten abgelegten Stand -
wenn sich nichts verändert hat, wird das Skript beendet.
Mit den Daten aus den Wahllokalen wird zuerst das "Feldmann-o-meter" aktualisiert -
die Grafik, die anzeigt, welcher Anteil der Wahlberechtigten schon ausgezählt ist,
wieviele Ja- und Nein-Stimmen es gab, und welchen Anteil die Ja-Stimmen an der
Gesamtheit der Wahlberechtigten hätten (geschätzt auf den Anteil der ausgezählten
Wahlberechtigten).
Dann wird aus den Wahllokal-Daten der Auszählung für den Stadtteil generiert -
das kann man in dieser Form auch direkt vom Server der Stadt ziehen; da ich aber
die Zuordnung der Wahllokale zu den Stadtteilen habe und selbst aggregieren kann,
rechnet eine Routine es schnell selbst.
Aus der Stadtteil-Auszählung werden die drei Datawrapper-Grafiken auf den aktuellen
Stand gebracht:
- eine Choropleth-Karte mit dem Anteil der Ja-Stimmen an der Wahlbevölkerung,
- eine Symbol-Karte mit den absoluten Ja-Stimmen nach Wahlbezirk,
- eine Tabelle mit den Ergebnissen in barrierefreier Form.
## Datenquelle und Datenformat
......@@ -31,7 +51,10 @@ Nutzt die Livedaten von https://wahlen.frankfurt.de - die aktuellen Daten nach W
## Wann gibt es wo Daten?
Soweit ich es sehen kann:
- Sobald ein Wahllokal ausgezählt ist, wird eine "Schnellmeldung" erzeugt und werden die CSVs aktualisiert.
- Eine Schnellmeldung umfasst ein Wahllokal.
- Nicht ausgezählte Wahllokale enthalten NA bei Wahlberechtigten/Wählern
- Ortsteile haben, solang sie noch nicht ganz ausgezählt sind, fiktive Wahlberechtigten-Zahlen - die dann nur die Wahllokale abbilden, die bereits ausgezählt sind. (Beispiel: Ein Ortsteil hat 3000 Wahlberechtigte in 3 Wahllokal-Bezirken mit jeweils 1000 Wahlberechtigten - solange nur 2 ausgezählt sind, wird für den Ortsteil eine Wahlberechtigten-Anzahl von 2000 angezeigt.)
- Briefwahl"lokale" - die Wahllokale mit den Nummern 9xx-xx - haben 0 Wahlberechtigte.
**An dieser Stelle ein Dankeschön an das Wahlamt der Stadt Frankfurt, das trotz Zeitdrucks geduldig und kompetent Unterstützung geleistet hat.**
name,value,comment
wahllokale_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=1667586196549,
ffm_waehler,510000,Schätzung Wählerinnen und Wähler – wird Sonntagabend aktualisiert
fom_id,raDG0,Datawrapper-ID Feldmann-o-meter
choropleth_id,w6caA,Datawrapper-ID Stadtteile Choropleth-Karte
symbol_id,ANKmx,Datawrapper-ID Stadtteile Symbole (absolute Stimmen)
tabelle_id,Un8Zz,Datawrapper-ID Tabelle Stadtteile
startdatum,2022-11-06 18:00:00 CET,Beginn der Auszählung
File added
Source diff could not be displayed: it is too large. Options to address this: view the blob.
STT,Shape_Leng,Shape_Area,NAME
"1",3246.50336244,506567.302747,Altstadt
"2",7682.0601294,1490200.89629,Innenstadt
"4",8382.11214938,2494895.4421,Westend-Süd
"5",5882.09282338,1630791.61391,Westend-Nord
"6",8492.9822688,3097758.64287,Nordend-West
"7",6360.44539376,1530531.93183,Nordend-Ost
"8",10941.4475374,5558007.54887,Ostend
"9",8307.7531642,2783942.45168,Bornheim
"10",10636.3052803,2336113.79188,Gutleut- und Bahnhofsviertel
"11",9966.86172965,4512465.83969,Gallus
"12",15551.0783172,8025712.89522,Bockenheim
"13",11334.2297164,4228538.08192,Sachsenhausen-Nord
"14",52725.6395956,54668620.6195,Sachsenhausen-Süd und Flughafen
"16",8408.63049619,2705423.33135,Oberrad
"17",11657.6724783,6119869.99388,Niederrad
"18",17370.1552298,14761540.2972,Schwanheim
"19",11666.167878,5096278.85391,Griesheim
"20",10601.4590033,4655909.71952,Rödelheim
"21",5971.99975786,1245429.93522,Hausen
"22",12819.2927644,5150078.39841,Praunheim
"24",7062.20915036,2512527.99004,Heddernheim
"25",17218.9119362,7400010.08944,Niederursel
"26",7927.28438588,2692768.59787,Ginnheim
"27",10408.8956447,2381980.04842,Dornbusch
"28",9996.16428731,3231404.25279,Eschersheim
"29",7947.72737103,2084449.0419,Eckenheim
"30",9713.20382227,3679083.90825,Preungesheim
"31",5429.38078236,1370475.09218,Bonames
"32",8758.27520312,3182489.49906,Berkersheim
"33",4041.52241905,976773.458365,Riederwald
"34",13052.9992861,7990475.22287,Seckbach
"35",13617.1503107,7074421.05332,Fechenheim
"36",13143.3336268,4593964.97889,Höchst
"37",9916.39176308,3704544.95018,Nied
"38",12879.9864686,3965052.07991,Sindlingen
"39",12399.4010702,5462797.97552,Zeilsheim
"40",12800.8758111,6015783.22976,Unterliederbach
"41",10792.909728,5914916.98748,Sossenheim
"42",13474.7203596,8360361.97443,Nieder-Erlenbach
"43",12576.9514872,6593988.59303,Kalbach-Riedberg
"44",10638.3166746,4832897.98209,Harheim
"45",11174.8124013,6343315.34634,Nieder-Eschbach
"46",18021.0142135,12591174.787,Bergen-Enkheim
"47",7901.77597555,2564397.7516,Frankfurter Berg
"nr","name","ortsteilnr"
"01001","010-01",1
"01002","010-02",1
"04001","040-01",2
"07001","070-01",2
"08001","080-01",2
"09001","090-01",10
"10001","100-01",4
"10002","100-02",4
"11001","110-01",4
"11002","110-02",4
"12001","120-01",6
"12002","120-02",6
"12003","120-03",6
"12004","120-04",6
"13001","130-01",7
"13002","130-02",7
"13003","130-03",7
"14001","140-01",8
"14002","140-02",8
"14003","140-03",8
"14004","140-04",8
"14005","140-05",8
"15101","151-01",10
"15102","151-02",10
"15103","151-03",10
"15104","151-04",10
"15301","153-01",11
"15302","153-02",11
"15401","154-01",11
"15402","154-02",11
"16101","161-01",11
"16102","161-02",11
"16103","161-03",11
"16104","161-04",11
"16201","162-01",11
"16202","162-02",11
"16203","162-03",11
"16204","162-04",11
"16205","162-05",11
"16301","163-01",12
"16302","163-02",12
"16303","163-03",12
"16401","164-01",11
"16501","165-01",11
"16502","165-02",11
"16503","165-03",11
"16504","165-04",11
"17001","170-01",4
"17002","170-02",4
"17003","170-03",4
"18001","180-01",4
"18002","180-02",4
"18003","180-03",4
"19101","191-01",5
"19102","191-02",5
"19103","191-03",5
"19201","192-01",5
"19202","192-02",5
"20101","201-01",6
"20102","201-02",6
"20201","202-01",6
"20202","202-02",6
"20301","203-01",6
"20302","203-02",6
"20303","203-03",6
"20304","203-04",6
"21101","211-01",6
"21102","211-02",6
"21103","211-03",6
"21201","212-01",6
"21202","212-02",6
"21203","212-03",6
"21301","213-01",6
"22101","221-01",7
"22102","221-02",7
"22103","221-03",7
"22201","222-01",7
"22202","222-02",7
"22203","222-03",7
"22204","222-04",7
"23001","230-01",7
"23002","230-02",7
"23003","230-03",7
"24001","240-01",9
"24002","240-02",9
"25101","251-01",8
"25102","251-02",8
"25103","251-03",8
"25104","251-04",8
"25201","252-01",8
"25202","252-02",8
"25203","252-03",8
"25204","252-04",8
"25205","252-05",8
"26101","261-01",8
"26102","261-02",8
"26103","261-03",8
"26201","262-01",33
"26202","262-02",33
"27101","271-01",9
"27102","271-02",9
"27201","272-01",9
"27202","272-02",9
"27203","272-03",9
"27204","272-04",9
"27205","272-05",9
"28101","281-01",9
"28102","281-02",9
"28103","281-03",9
"28201","282-01",9
"28202","282-02",9
"28203","282-03",9
"29001","290-01",9
"29002","290-02",9
"30001","300-01",13
"30002","300-02",13
"32101","321-01",13
"32102","321-02",13
"32103","321-03",13
"32201","322-01",13
"32202","322-02",13
"32203","322-03",13
"32204","322-04",13
"32205","322-05",13
"32301","323-01",14
"32302","323-02",14
"32303","323-03",14
"32304","323-04",14
"32305","323-05",14
"32306","323-06",14
"32401","324-01",13
"32402","324-02",13
"32403","324-03",13
"32404","324-04",13
"32601","326-01",14
"32602","326-02",14
"32603","326-03",14
"33101","331-01",13
"33102","331-02",13
"33103","331-03",13
"33201","332-01",14
"33202","332-02",14
"33203","332-03",14
"33204","332-04",14
"33205","332-05",14
"33206","332-06",14
"34101","341-01",12
"34102","341-02",12
"34103","341-03",12
"34201","342-01",12
"34202","342-02",12
"34301","343-01",12
"34401","344-01",12
"35001","350-01",12
"35002","350-02",12
"35003","350-03",12
"35004","350-04",12
"36101","361-01",12
"36102","361-02",12
"36103","361-03",12
"36201","362-01",12
"36202","362-02",12
"36203","362-03",12
"36204","362-04",12
"37101","371-01",17
"37102","371-02",17
"37103","371-03",17
"37104","371-04",17
"37105","371-05",17
"37106","371-06",17
"37201","372-01",17
"37202","372-02",17
"37203","372-03",17
"37204","372-04",17
"37205","372-05",17
"37206","372-06",17
"38001","380-01",16
"38002","380-02",16
"38003","380-03",16
"38004","380-04",16
"38005","380-05",16
"38006","380-06",16
"39001","390-01",34
"39002","390-02",34
"39003","390-03",34
"39004","390-04",34
"39005","390-05",34
"39006","390-06",34
"39007","390-07",34
"40101","401-01",20
"40102","401-02",20
"40103","401-03",20
"40104","401-04",20
"40105","401-05",20
"40201","402-01",20
"40202","402-02",20
"40203","402-03",20
"40204","402-04",20
"41001","410-01",21
"41002","410-02",21
"41003","410-03",21
"42201","422-01",22
"42202","422-02",22
"42203","422-03",22
"42301","423-01",22
"42302","423-02",22
"42401","424-01",22
"42501","425-01",22
"42601","426-01",22
"42602","426-02",22
"43101","431-01",24
"43102","431-02",24
"43103","431-03",24
"43104","431-04",24
"43105","431-05",24
"43201","432-01",24
"43202","432-02",24
"43203","432-03",24
"43204","432-04",24
"44101","441-01",26
"44102","441-02",26
"44103","441-03",26
"44104","441-04",26
"44105","441-05",26
"44106","441-06",26
"44107","441-07",26
"44108","441-08",26
"44201","442-01",27
"44202","442-02",27
"44203","442-03",27
"44204","442-04",27
"45101","451-01",28
"45102","451-02",28
"45103","451-03",28
"45104","451-04",28
"45201","452-01",28
"45202","452-02",28
"45203","452-03",28
"45204","452-04",28
"46101","461-01",29
"46102","461-02",29
"46103","461-03",29
"46104","461-04",29
"46105","461-05",29
"46106","461-06",29
"46107","461-07",29
"46201","462-01",27
"46202","462-02",27
"46203","462-03",27
"46204","462-04",27
"46205","462-05",27
"46206","462-06",27
"46301","463-01",27
"46302","463-02",27
"47001","470-01",30
"47002","470-02",30
"47003","470-03",30
"47004","470-04",30
"47005","470-05",30
"47006","470-06",30
"47007","470-07",30
"48101","481-01",25
"48102","481-02",25
"48103","481-03",25
"48201","482-01",25
"48202","482-02",25
"48203","482-03",25
"48301","483-01",25
"49101","491-01",31
"49102","491-02",31
"49103","491-03",31
"49201","492-01",47
"49202","492-02",47
"49203","492-03",47
"49204","492-04",47
"50001","500-01",32
"50002","500-02",32
"51001","510-01",35
"51002","510-02",35
"51003","510-03",35
"51004","510-04",35
"52001","520-01",35
"52002","520-02",35
"52003","520-03",35
"53101","531-01",18
"53102","531-02",18
"53103","531-03",18
"53104","531-04",18
"53105","531-05",18
"53201","532-01",18
"53202","532-02",18
"53203","532-03",18
"53204","532-04",18
"53205","532-05",18
"53206","532-06",18
"54101","541-01",19
"54102","541-02",19
"54201","542-01",19
"54202","542-02",19
"54203","542-03",19
"55101","551-01",19
"55102","551-02",19
"55201","552-01",19
"55202","552-02",19
"55203","552-03",19
"56101","561-01",37
"56102","561-02",37
"56103","561-03",37
"56104","561-04",37
"56105","561-05",37
"56201","562-01",37
"56202","562-02",37
"56203","562-03",37
"56204","562-04",37
"57001","570-01",36
"57002","570-02",36
"57003","570-03",36
"58001","580-01",36
"58002","580-02",36
"59101","591-01",36
"59102","591-02",36
"60101","601-01",38
"60102","601-02",38
"60201","602-01",38
"60202","602-02",38
"60401","604-01",39
"61101","611-01",39
"61102","611-02",39
"61201","612-01",39
"61202","612-02",39
"62101","621-01",40
"62102","621-02",40
"62103","621-03",40
"62104","621-04",40
"62201","622-01",40
"62202","622-02",40
"62203","622-03",40
"63101","631-01",41
"63102","631-02",41
"63103","631-03",41
"63104","631-04",41
"63201","632-01",41
"63202","632-02",41
"63203","632-03",41
"64001","640-01",42
"64002","640-02",42
"64003","640-03",42
"65001","650-01",43
"65002","650-02",43
"65003","650-03",43
"65101","651-01",43
"65102","651-02",43
"65103","651-03",43
"65104","651-04",43
"65105","651-05",43
"65106","651-06",43
"65107","651-07",43
"66001","660-01",44
"66002","660-02",44
"66003","660-03",44
"67001","670-01",45
"67002","670-02",45
"67003","670-03",45
"67004","670-04",45
"67005","670-05",45
"67006","670-06",45
"68001","680-01",46
"68002","680-02",46
"68003","680-03",46
"68004","680-04",46
"68005","680-05",46
"68006","680-06",46
"68007","680-07",46
"68008","680-08",46
"68009","680-09",46
"68010","680-10",46
90101,"901-01",1
90201,"902-01",2
90202,"902-02",2
90401,"904-01",4
90402,"904-02",4
90403,"904-03",4
90404,"904-04",4
90405,"904-05",4
90501,"905-01",5
90502,"905-02",5
90503,"905-03",5
90601,"906-01",6
90602,"906-02",6
90603,"906-03",6
90604,"906-04",6
90605,"906-05",6
90606,"906-06",6
90607,"906-07",6
90608,"906-08",6
90609,"906-09",6
90701,"907-01",7
90702,"907-02",7
90703,"907-03",7
90704,"907-04",7
90705,"907-05",7
90706,"907-06",7
90707,"907-07",7
90801,"908-01",8
90802,"908-02",8
90803,"908-03",8
90804,"908-04",8
90805,"908-05",8
90806,"908-06",8
90807,"908-07",8
90808,"908-08",8
90809,"908-09",8
90901,"909-01",9
90902,"909-02",9
90903,"909-03",9
90904,"909-04",9
90905,"909-05",9
90906,"909-06",9
90907,"909-07",9
90908,"909-08",9
90909,"909-09",9
91001,"910-01",10
91002,"910-02",10
91003,"910-03",10
91101,"911-01",11
91102,"911-02",11
91103,"911-03",11
91104,"911-04",11
91105,"911-05",11
91106,"911-06",11
91107,"911-07",11
91108,"911-08",11
91201,"912-01",12
91202,"912-02",12
91203,"912-03",12
91204,"912-04",12
91205,"912-05",12
91206,"912-06",12
91207,"912-07",12
91208,"912-08",12
91209,"912-09",12
91210,"912-10",12
91211,"912-11",12
91301,"913-01",13
91302,"913-02",13
91303,"913-03",13
91304,"913-04",13
91305,"913-05",13
91306,"913-06",13
91307,"913-07",13
91308,"913-08",13
91309,"913-09",13
91310,"913-10",13
91401,"914-01",14
91402,"914-02",14
91403,"914-03",14
91404,"914-04",14
91405,"914-05",14
91406,"914-06",14
91407,"914-07",14
91408,"914-08",14
91409,"914-09",14
91601,"916-01",16
91602,"916-02",16
91603,"916-03",16
91701,"917-01",17
91702,"917-02",17
91703,"917-03",17
91704,"917-04",17
91705,"917-05",17
91706,"917-06",17
91801,"918-01",18
91802,"918-02",18
91803,"918-03",18
91804,"918-04",18
91805,"918-05",18
91806,"918-06",18
91901,"919-01",19
91902,"919-02",19
91903,"919-03",19
91904,"919-04",19
91905,"919-05",19
92001,"920-01",20
92002,"920-02",20
92003,"920-03",20
92004,"920-04",20
92005,"920-05",20
92101,"921-01",21
92102,"921-02",21
92201,"922-01",22
92202,"922-02",22
92203,"922-03",22
92204,"922-04",22
92205,"922-05",22
92401,"924-01",24
92402,"924-02",24
92403,"924-03",24
92404,"924-04",24
92405,"924-05",24
92501,"925-01",25
92502,"925-02",25
92503,"925-03",25
92504,"925-04",25
92601,"926-01",26
92602,"926-02",26
92603,"926-03",26
92604,"926-04",26
92701,"927-01",27
92702,"927-02",27
92703,"927-03",27
92704,"927-04",27
92705,"927-05",27
92706,"927-06",27
92801,"928-01",28
92802,"928-02",28
92803,"928-03",28
92804,"928-04",28
92901,"929-01",29
92902,"929-02",29
92903,"929-03",29
92904,"929-04",29
93001,"930-01",30
93002,"930-02",30
93003,"930-03",30
93004,"930-04",30
93101,"931-01",31
93102,"931-02",31
93201,"932-01",32
93301,"933-01",33
93401,"934-01",34
93402,"934-02",34
93403,"934-03",34
93501,"935-01",35
93502,"935-02",35
93503,"935-03",35
93504,"935-04",35
93601,"936-01",36
93602,"936-02",36
93603,"936-03",36
93701,"937-01",37
93702,"937-02",37
93703,"937-03",37
93704,"937-04",37
93801,"938-01",38
93802,"938-02",38
93901,"939-01",39
93902,"939-02",39
93903,"939-03",39
94001,"940-01",40
94002,"940-02",40
94003,"940-03",40
94004,"940-04",40
94101,"941-01",41
94102,"941-02",41
94103,"941-03",41
94104,"941-04",41
94201,"942-01",42
94202,"942-02",42
94301,"943-01",43
94302,"943-02",43
94303,"943-03",43
94304,"943-04",43
94305,"943-05",43
94401,"944-01",44
94402,"944-02",44
94501,"945-01",45
94502,"945-02",45
94503,"945-03",45
94601,"946-01",46
94602,"946-02",46
94603,"946-03",46
94604,"946-04",46
94605,"946-05",46
94701,"947-01",47
94702,"947-02",47
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment