Die Zahl der Menschen, welche sich mit dem neuartigen Coronavirus SARS-CoV-2 infiziert haben, ist ein wichtiger Indikator für die Ausbreitung von COVID-19. Krankenhäuser und Ärzte sind dazu verpflichtet Corona-Fälle an die örtlichen Gesundheitsämter zu melden. Aus der Summe dieser Meldungen ergeben sich die offiziellen Fallzahlen. Im Freistaat Bayern werden die jeweils aktuellen Fallzahlen durch das Bayerische Landesamt für Gesundheit und Lebensmittelsicherheit (LGL) veröffentlicht.
Die hier veröffentlichten Skripte dienen dazu die veröffentlichen Zahlen des LGL täglich in einer Datenbank zu sichern und eine Schnittstelle (API) zum Abfragen der Daten bereitzustellen.
Eine Übersicht der aktuellen Statistiken zu Coronavirusinfektionen in Bayern findet sich auf der Webseite des Bayerisches Landesamt für Gesundheit und Lebensmittelsicherheit (LGL). Die absoluten Fallzahlen werden aus der „Tabelle 03: Coronavirusinfektionen“ bezogen. Das LGL aktualisiert die Fallzahlen jeden Tag, meistens zwischen 12 und 15 Uhr.
Die Daten werden zur Zeit mehrfach täglich in einer Datenbank (Google Cloud Firestore) gesichert. Dabei sind jedoch folgende Einschränkungen zu beachten:
- Die Fallzahlen für die einzelnen Landkreise sind erst ab dem 12.3.2020 verfügbar.
- Für die Landkreise werden keine Nachmeldungen durch das LGL (Stichwort: Meldeverzögerung) veröffentlicht. Daher können die historischen Zahlen von den Zahlen des RKIs abweichen.
- Unklare Datenlage vor dem 20.3.2020, da die Fallzahlen in einzelnen Landkreise tageweise wieder zurück ging.
- Die Zahl der Todesfälle ist erst seit dem 28.3.2020 verfügbar und wird seit dem 6.4.2020 auch in der Datenbank erfasst.
Für die Verwendung der Daten in Apps und interaktiven Grafiken (Datawrapper) stellen wir einen API bereit, die das Abfragen der LGL-Daten nach bestimmten Parametern (Zeitpunkt, Landkreis, Dateiformat) ermöglicht.
URL: https://europe-west3-brdata-corona.cloudfunctions.net/lglApi/
/: alle verfügbaren Daten abrufen/date: aktuellste Daten abrufen/date/[date]: Daten für ein spezifische Datum abrufen, z.B.2020-03-18/county: Daten für alle Landkreise abrufen/county/[id]: Daten für einen Landkreis abrufen, z.B.amberg-sulzbach
?filetype=csv: Daten als CSV-Tabelle zurückgeben
id: einzigartige ID, z.B. "neumarkt-idopf"name-lgl: offizielle Bezeichnung des LGLs, z.B. "Neumarkt i.d.Opf."name: ausgeschriebener Name, z.B. "Neumarkt in der Oberpfalz"type: "Stadt" oder "Landkreis"district: Regierungsbezirk, z.B. "Oberpfalz",ags: Amtlicher Gemeindeschlüssel, z.B. "09373"lat: Längengrad, z.B. 49.2265324long: Breitengrat, z.B. 11.5580180pop: Einwohnerzahl, z.B. 1471508date: Datum für/date-Anfragen, z.B."2020-03-24"cases: Fallzahlen für/date-Anfragen, z.B. 35cases: Alle bisher erfassten Fallzahlen pro Datum für Anfragen ohne/date-Parameter. Beispiel: { "2020-03-25": 43, "2020-03-24": 35, "2020-03-23": 18, ... }last-cases: letzte erfasste Fallzahlen, z.B. 43previous-cases: vorletzte erfasste Fallzahlen, z.B. 37deaths: Zahl der Todesfälle für/date-Anfragen, z.B. 2deaths: Alle bisher erfassten Todesfälle pro Datum für Anfragen ohne/date-Parameter. Beispiel: { "2020-03-25": 1, "2020-03-24": 1, "2020-03-23": 2, ... }last-deaths: letzte erfasste Todefälle, z.B. 2previous-deaths: vorletzte erfasste Todefälle, z.B. 1last-updated: Datum der letzten Aktualisierung, z.B. "2020-03-25T19:09:05.188Z"cases-per-tsd: letzte berechnete Fallzahlen pro 1.000 Einwohner, z.B. 0.1cases-per-100tsd: Fallzahlen pro 100.000 Einwohner nach Berechnung des LGLcases-per-100tsd-7days: Neuinfektionen pro 100.000 Einwohner der letzten Woche nach Berechnung des LGLdoubling-time: letzte berechnete Verdopplungszeit, z.B. 2.7
Jeder Stadt und jeder Landkreis haben einen eigene ID. Die IDs werden aus Namen des LGL name-lgl und der Methode toDashcase(string) aus ./lib/to-dashcase erzeugt. Dieses Vorgehen hilft dabei die IDs stabil zu halten, auch wenn es kleiner Änderungen (Leerzeichen, Punkte) in der Benennung seitens des LGLs gibt.
Beispiel:
toDashcase('Neumarkt i.d.Opf.') // => neumarkt-idopfEin vollständige Liste der IDs findet sich in der Datei ./import/data/counties.json.
Bei Anfragen für ein bestimmtes Datum wird kein case-Objekt mit Fallzahlen für andere Daten zurückgegeben. Die Rückgabe-Objekt für den Endpunkt /date sind identisch, nur das hierbei immer die jeweils aktuellsten Daten von heute oder gestern zurückgegeben werden.
Beispiel für /date/2020-03-25:
[
{
"id": "ansbach-stadt",
"name-lgl": "Ansbach Stadt",
"name": "Ansbach",
"type": "Stadt",
"district": "Mittelfranken",
"ags": "09561",
"lat": 49.2917917440462,
"long": 10.5691214101633,
"pop": 41847,
"last-updated": "2020-03-31T18:00:05.119Z",
"date": "2020-03-25",
"cases": 6,
"last-cases": 24,
"previous-cases": 18,
"deaths": 0,
"last-deaths": 00,
"previous-deaths": 0,
"cases-per-tsd": 0.14,
"cases-per-100tsd": 140,
"cases-per-100tsd-7days": 20,
"doubling-time": 2.9
},
{
// ... andere Landkreise
}
]Bei Anfragen für einen bestimmten Landkreis, werden alle Fallzahlen für jeden verfügbaren Tag im case-Objekt zurückgegeben. Die Rückgabe-Objekte für die Endpunkte /county und / sind identisch. Hier werden jeweils alle verfügbaren Daten für alle Landkreise zurückgegeben.
Beispiel für /county/ansbach-stadt:
[
{
"id": "ansbach-stadt",
"name-lgl": "Ansbach Stadt",
"name": "Ansbach",
"type": "Stadt",
"district": "Mittelfranken",
"ags": "09561",
"lat": 49.2917917440462,
"long": 10.5691214101633,
"pop": 41847,
"last-updated": "2020-03-31T18:00:05.119Z",
"cases": {
"2020-03-31": 24,
"2020-03-30": 19,
"2020-03-29": 24,
"2020-03-28": 18,
"2020-03-27": 15,
"2020-03-26": 10
// ... mehr Fallzahlen
},
"last-cases": 24,
"deaths": {
"2020-03-31": 0,
"2020-03-30": 0,
"2020-03-29": 0,
"2020-03-28": 0,
"2020-03-27": 0,
"2020-03-26": 0
// ... mehr Fallzahlen
},
"cases-per-100tsd": 140,
"cases-per-100tsd-7days": 20,
"last-deaths": 43
}
]- Repository klonen
git clone https://... - Erforderliche Module installieren
npm install - Entwicklungsserver starten
npm run watch
Um die Module installieren und die Entwicklerwerkzeuge nutzen zu können, muss vorher die JavaScript-Runtime Node.js installiert werden.
Diese Anleitung geht davon aus, dass bereits ein Google Cloud-Konto vorhanden und ein Rechnungskonto eingerichtet ist. Außerdem sollte das Google Cloud-Kommandzeilenwerkzeug installiert und mit einem Benutzerkonto verknüpft sein.
Neues Projekt mit der ID brdata-corona erstellen. Der Parameter --name ist optional.
$ gcloud projects create brdata-corona --name=30-BRData-coronaDas Projekt als aktuelles Arbeitsprojekt festlegen:
$ gcloud config set project brdata-coronaVerwende die Google Cloud-Weboberfläche, um eine neue Firebase-Datenbank zu erstellen:
Für die Datenbank sollte dabei der „native Modus“ ausgewählt werden. Als Region, also den Speicheort wählen wir europe-west3 (Frankfurt) aus. Jede Datenbank kann mehrere Sammlungen (collections) enthalten, in der die einzelnen Daten als sogenannten Dokumente gespeichert werden können. Jetzt musst du nur noch eine neue Sammlung anlegen und benennen, zum Beispiel bayern-lgl.
In Zukunft wird das Erstellen einer Firebase-Datenbank auch über die Kommandozeile möglich sein:
Firestore-API aktivieren:
$ gcloud services enable firestore.googleapis.comNeue Datenbank im Rechenzentrum europe-west3 (Frankfurt) anlegen:
$ gcloud alpha firestore databases create --region=europe-west3Der externe Zugriff auf die Datenbank, zum Beispiel vom eigenen Computer, erfolgt über einen sogenanntes Dienstkonto (service account). Damit du dich mit dem Dienstkonto anzumelden kannst, musst du zuerst noch einen Dienstkontoschlüssel erstellen:
$ gcloud iam service-accounts keys create ~/key.json \
--iam-account corona.iam.gserviceaccount.comDabei ist corona die Projekt-ID. Das Ausführen des Befehls erzeugt eine Datei key.json, welche einen privaten Schlüssel erhält, der den Zugang zur Datenbank von extern ermöglicht. Diese Datei sollte geheim gehalten werden und darf niemals in Git eingecheckt werden.
Nachdem du die Datenbank, Sammlung und das Dienstkontoschlüssel erstellt hast, musst du diese Information noch in die Konfigurationsdatei config.json übertragen:
{
"url": "https://www.lgl.bayern.de/gesundheit/infektionsschutz/infektionskrankheiten_a_z/coronavirus/karte_coronavirus/index.htm",
"firestore": {
"projectId": "corona",
"collectionId": "bayern-lgl",
"timestampsInSnapshots": true,
"keyFilename": "./key.json"
}
}Achtung: Überschreibt alle bisherigen Daten in der Datenbank! Basisdatensatz aller Landkreise mit Geodaten (Lat/Long) importieren:
$ node import-counties.js ./data/counties.jsonFehlende Daten für einzelne Tage importieren:
$ node import/import-daily.js ./data/daily/2020-03-15.jsonGoogle Cloud Function für das aktuelle Projekt aktivieren:
$ gcloud services enable cloudfunctions.googleapis.comRechenzentrum europe-west3 (Frankfurt) als Ziel für das Funktions-Deployment festlegen. Das gewählte Rechenzentrum muss identisch sein, mit dem Rechenzentrum für die Firestore-Datenbank:
$ gcloud config set functions/region europe-west3Neues Pub/Sub-Thema lgl-scraper-start erstellen, welches die Scraper-Funktion auslöst:
$ gcloud pubsub topics create lgl-scraper-startScraper-Funktion deployen und den Pub/Sub-Auslöser lgl-scraper-start festlegen
$ gcloud functions deploy lglScraper --runtime=nodejs10 --trigger-topic=lgl-scraper-startDie Abfrage, ob auch eine authentifizierte Ausführung erlaubt werden soll, kann in dem meisten Fällen mit „Nein“ beantwortet werden, da die Funktion vom Google Cloud Scheduler zeitgesteuert ausgelöst werden kann.
Allow unauthenticated invocations of new function [lglScraper]? (y/N)?Falls man später doch eine nicht authentifizierte Ausführung erlauben möchte, muss man die entsprechende IAM-Richtline ändern:
$ gcloud alpha functions add-iam-policy-binding lglScraper --member=allUsers --role=roles/cloudfunctions.invokerDer Google Cloud Scheduler erlaubt es den Scraper zeitgesteuert, zu bestimmten Uhrzeiten, auszuführen. Dazu muss der Cloud Scheduler jedoch erstmal für das Projekt aktiviert werden:
$ gcloud services enable cloudscheduler.googleapis.comWie häufig die Scraper-Funktion ausgeführt werden soll, kann mit dem Parameter --schedule festgelegt werden, welche die Crontab-Syntax unterstützt. Dabei hilft zum Beispiel der crontab.guru. Außerdem muss die gültige Zeitzone --time-zone und der Pub/Sub-Auslöser --topic festgelegt werden. In diesem Beispiel wird der Scraper alle zwei Stunden von 8 bis 20 Uhr ausgeführt:
$ gcloud scheduler jobs create pubsub brdata-corona --topic=lgl-scraper-start --schedule="0 8-20/2 * * *" --time-zone="Europe/Berlin" --message-body="undefined"API-Funktion deployen. In diesem Beispiel wird der nicht authentifizierte Zugriff von außerhalb erlaubt, um den Datenaustausch zwischen API und beispielsweise einer Web-App zu ermöglichen:
$ gcloud functions deploy lglApi --runtime=nodejs10 --trigger-http --allow-unauthenticatedUm das Skript index.js lokal zu testen, verwendet man am besten das Google Functions Framework. Das Functions Framework kann mit dem Befehl npm run watch gestartet werden. Das hat den Vorteil, dass das Skript jedes Mal neu geladen wird, sobald man Änderungen am Code vornimmt.
Man kann das Functions Framework aber auch manuell installieren und ausführen:
$ npm i -g @google-cloud/functions-frameworkFunktion lglApi starten:
$ functions-framework --target=lglApiAPI-Anfrage an die aktivierte Funktion stellen (Beispiel):
$ curl -X GET 'localhost:8080?date=2020-03-18'- Datenvalidierung für Scraper hinzufügen: Richtige Tabelle, richtige Spalte?
- Aggregation pro Regierungsbezirk oder für ganz Bayern ermöglichen
- Festlegen, wie viele Tage
getSpecificDate()maximal zurückgehen darf - Bessere Fehlerbehandlung und Reporting für die API