Aktuelle Corona-Inzidenz

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Benutzeravatar
snafu
User
Beiträge: 6331
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Sonntag 6. Juni 2021, 10:09

Folgendes Skript zeigt die tagesaktuelle 7-Tage-Inzidenz für einen Landkreis oder Kreisstadt an (Quelle: RKI):

Code: Alles auswählen

import sys
import requests

API_URL = "https://api.corona-zahlen.org/districts/"

DISTRICT = "berlin"

def get_json(url):
    response = requests.get(url)
    response.raise_for_status()
    return response.json()

def get_incidence(district_name):
    name = district_name.lower()
    data = get_json(API_URL)["data"]
    return {
        district["name"]: district["weekIncidence"]
        for district in data.values()
        if name in district["name"].lower()
    }

def main():
    district_name = sys.argv[1] if len(sys.argv) > 1 else DISTRICT
    result = get_incidence(district_name)
    if not result:
        print(f"Landkreis/Kreisstadt nicht gefunden")
    for name, incidence in result.items():
        print(f"{name}: {incidence:.01f}")

if __name__ == "__main__":
    main()
Einzelne Stadtteile oder kreisangehörige Städte werden (bisher) nicht unterstützt. Es muss dann der Name des Kreises angegeben werden. Mehrere Treffer sind möglich, zB bei Frankfurt. Kleinschreibung ist okay. Der Anfangsteil der Kreisstadt bzw Kreis reicht auch. Die Übergabe des Namens kann von der Kommandozeile erfolgen oder als DISTRICT fest ins Skript gebaut werden.
Benutzeravatar
snafu
User
Beiträge: 6331
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Sonntag 6. Juni 2021, 13:29

Die Abfrage ist nun auch mit dem "Allgemeinen Gemeindeschlüssel" (AGS) möglich. Dieser wird nun gleich mit angezeigt und hat im Übrigen nichts mit der PLZ zu tun. Die Abfrage über Stadtnamen bzw Kreis funktioniert wie zuvor. Es sind als Zusatzinfos die Gesamtfälle und Verstorbenen dazu gekommen.

Code: Alles auswählen

#!/usr/bin/env python3

import sys
from urllib.parse import urljoin

import requests

API_URL = "https://api.corona-zahlen.org/districts/"

DISTRICT = "berlin"

def get_json(url):
    response = requests.get(url)
    response.raise_for_status()
    return response.json()

def get_district_records(district_key):
    key = str(district_key).lower()
    if key.isdigit():
        district_url = urljoin(API_URL, key)
        return get_json(district_url)["data"]
    data = get_json(API_URL)["data"]
    return {
        ags: dist for ags, dist in data.items()
        if key in dist["name"].lower()
    }

def get_info_lines(district_key):
    template = ("({ags}) {name}:\n{weekIncidence:.01f} Inzidenz, "
                "{cases} Gesamtfälle, {deaths} Verstorbene")
    result = get_district_records(district_key)
    return [template.format_map(dist) for dist in result.values()]

def main():
    district_key = sys.argv[1] if len(sys.argv) > 1 else DISTRICT
    lines = get_info_lines(district_key)
    if lines:
        print("\n\n".join(lines))
    else:
        print("Landkreis/Kreisstadt nicht gefunden")

if __name__ == "__main__":
    main()
Zuletzt geändert von snafu am Sonntag 6. Juni 2021, 13:47, insgesamt 1-mal geändert.
Benutzeravatar
ThomasL
User
Beiträge: 1080
Registriert: Montag 14. Mai 2018, 14:44
Wohnort: Kreis Unna NRW

Sonntag 6. Juni 2021, 13:40

Bild
Ich bin Pazifist und greife niemanden an, auch nicht mit Worten.
Für alle meine Code Beispiele gilt: "There is always a better way."
https://projecteuler.net/profile/Brotherluii.png
Benutzeravatar
DeaD_EyE
User
Beiträge: 617
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Montag 7. Juni 2021, 09:18

Ich halte zwar von den Daten nichts, aber eine schöne Aufgabe wäre es die Inzidenz-Werte mal selbst auszurechnen.

Code: Alles auswählen

from collections import deque
from statistics import mean


def inzidenz_iter(daten, bewohner):
    """
    7-Tage inzidenz berechnen

    daten := Positiv getestet pro Tag

    Es wird der Mittelwert für 7 Tage auf 100_000 Einwohner ausgegeben.
    """

    def skalieren(wert):
        return wert * 100_000 / bewohner

    if len(daten) < 7:
        return ValueError("Zu wenig Daten")

    window = deque(daten[:7], maxlen=7)
    yield skalieren(mean(window))

    for value in daten[7:]:
        window.append(value)
        yield skalieren(mean(window))
Alternativ kann man z.B. auch pandas nutzen. Da gibt es die methode DataFrame.rolling(fenstergröße).mean().
Eine weitere alternative wäre z.B. auch more_itertools.windowed.


Wenn in einem Dorf mit 10 Einwohnern 7 Tage lange 5 positiv getestet werden, ergibt das eine Inzidenz von 50_000.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Benutzeravatar
ThomasL
User
Beiträge: 1080
Registriert: Montag 14. Mai 2018, 14:44
Wohnort: Kreis Unna NRW

Montag 7. Juni 2021, 13:49

DeaD_EyE hat geschrieben:
Montag 7. Juni 2021, 09:18
Wenn in einem Dorf mit 10 Einwohnern 7 Tage lange 5 positiv getestet werden, ergibt das eine Inzidenz von 50_000.
Ja, weil das Beispiel ja auch konstruiert und falsch ist.
Um 7 Tage lang 5 positiv zu testen brauchst du ja schon mal 35 Menschen.
Ich bin Pazifist und greife niemanden an, auch nicht mit Worten.
Für alle meine Code Beispiele gilt: "There is always a better way."
https://projecteuler.net/profile/Brotherluii.png
__deets__
User
Beiträge: 9872
Registriert: Mittwoch 14. Oktober 2015, 14:29

Montag 7. Juni 2021, 13:57

Weswegen ja auch Konfidenzintervall zu solchen Zahlen gehören. Was die offiziellen Quellen natürlich auch machen.
Benutzeravatar
snafu
User
Beiträge: 6331
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Montag 7. Juni 2021, 18:45

Meine Intention für den Code war unter anderem eine bequeme Infoquelle für den eigenen Wohnort, da an die Inzidenzwerte ja auch unterschiedliche Maßnahmen / Einschränkungen gekoppelt sind. Ein richtiger Vergleich zwischen Dorf und Großstadt ist nicht unbedingt möglich. Nach einem Dorffest mit Ausbruch kann es eine monströs hohe Inzidenz geben, nachdem sich quasi jeder Zweite angesteckt hat. Die ist aber auch schnell wieder unten. Die nächste Großstadt dagegen könnte eine ganze Weile zwischen 200-250 liegen, da sich hier die Isolation anders auswirkt und die Dunkelziffer wahrscheinlich höher wäre, Corona somit deutlich viraler. Hier geht es aber um Code. Für eine tiefere Diskussion bitte einen eigenen Thread aufmachen. Danke!
nezzcarth
User
Beiträge: 1306
Registriert: Samstag 16. April 2011, 12:47

Montag 7. Juni 2021, 18:53

snafu hat geschrieben:
Montag 7. Juni 2021, 18:45
Meine Intention für den Code war unter anderem eine bequeme Infoquelle für den eigenen Wohnort, da an die Inzidenzwerte ja auch unterschiedliche Maßnahmen / Einschränkungen gekoppelt sind.
Ich habe aus dem gleichen Grund ein ähnliches Skript, das jedoch nur für ein Bundesland funktioniert und die Daten von der dortigen Info-Seite extrahiert. Diese API kannte ich nicht. Deine Lösung gefällt mir besser. Vielen Dank. :)
Benutzeravatar
snafu
User
Beiträge: 6331
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Montag 7. Juni 2021, 19:30

Habe das mit den Gemeindeschlüsseln wieder rausgenommen. Es war verwirrend und komplexer und brachte außerdem keinen echten Geschwindigkeitsvorteil oder Mehrwert. Dafür kann man jetzt mehrere Orte abfragen. Habe das als Beispiel mal für mein Zuhause und die Nachbar-Großstädte eingebaut. Geht nach wie vor auch von der Kommandozeile. Einfach die Orte per Plus-Zeichen aneinanderfügen.

Code: Alles auswählen

#!/usr/bin/env python3
import sys
import requests

API_URL = "https://api.corona-zahlen.org/districts/"

QUERY = "gelsen+essen+bochum"

def get_json(url):
    response = requests.get(url)
    response.raise_for_status()
    return response.json()

def get_district_records(query):
    terms = [term.strip().lower() for term in query.split("+")]
    data = get_json(API_URL)["data"]
    return {
        key: district for key, district in data.items()
        for term in terms if term in district["name"].lower()
    }

def get_info_text(query):
    template = ("{name}:\n{weekIncidence:.01f} Inzidenz, "
                "{cases} Gesamtfälle, {deaths} Verstorbene")
    districts = get_district_records(query).values()
    if not districts:
        raise ValueError("Landkreis/Kreisstadt nicht gefunden")
    return "\n\n".join(
        map(template.format_map, districts)
    )

def main():
    query = sys.argv[1] if len(sys.argv) > 1 else QUERY
    try:
        print(get_info_text(query))
    except Exception as error:
        print(error, file=sys.stderr)

if __name__ == "__main__":
    main()
Benutzeravatar
snafu
User
Beiträge: 6331
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Samstag 12. Juni 2021, 08:10

Nun ist die Ausgabe hübsch als Tabelle formatiert. Dazu muss das Paket rich mittels pip installiert sein. Außerdem werden die Inzidenzwerte je nach Höhe in unterschiedlichen Farben angezeigt (<35 grün, <50 gelb, <100 orange, danach rot).

Code: Alles auswählen

#!/usr/bin/env python3
import sys

import requests
from requests.exceptions import RequestException

from rich.console import Console
from rich.table import Table
from rich.text import Text

API_URL = "https://api.corona-zahlen.org/districts/"

QUERY = "berlin+hamburg+köln"

def _format_incidence(value):
    if value < 35:
        color = "bright_green"
    elif value < 50:
        color = "bright_yellow"
    elif value < 100:
        color = "orange3"
    else:
        color = "bright_red"
    return Text(f"{value:.01f}", color)

COLUMN_SPECS = {
    "Landkreis": ("name", str),
    "Inzidenz": ("weekIncidence", _format_incidence),
    "Gesamtfälle": ("cases", str),
    "Verstorbene": ("deaths", str),
}

def get_json(url):
    response = requests.get(url)
    response.raise_for_status()
    return response.json()

def get_district_records(query):
    terms = [term.strip().lower() for term in query.split("+")]
    data = get_json(API_URL)["data"]
    return {
        key: district for key, district in data.items()
        for term in terms if term in district["name"].lower()
    }

def get_table(query, specs=COLUMN_SPECS):
    districts = get_district_records(query).values()
    if not districts:
        raise ValueError(query)
    table = Table(*specs.keys())
    for district in districts:
        table.add_row(*(
            formatter(district[section])
            for section, formatter in specs.values()
        ))
    return table

_fail = sys.exit

def main():
    query = sys.argv[1] if len(sys.argv) > 1 else QUERY
    try:
        Console().print(get_table(query))
    except RequestException as error:
        _fail(f"Verbindung fehlgeschlagen: {error}")
    except ValueError as error:
        _fail(f"Keine Treffer: {error}")

if __name__ == "__main__":
    main()
Die Farbanzeige hängt leider sehr von den Fähigkeiten des Terminals/Konsole ab. Insbesondere Orange macht wohl Probleme. Da setz ich mich jetzt aber nicht mehr ran...
rogerb
User
Beiträge: 123
Registriert: Dienstag 26. November 2019, 23:24

Samstag 12. Juni 2021, 10:12

Schönes Projekt!
Nur eine Frage: Was hat dich zur Verwendung der _unterstriche bewogen. Ich kenne zwar die gängige Erklärungen "internal use", die tatsächliche Verwendung wie man sie in vielen code Beispielen sieht, scheint aber eher willkürlich zu sein.

[edit] _namen werden beim * import nicht importiert wie ich gerade lese. Gibt es weitere Gründe?
__deets__
User
Beiträge: 9872
Registriert: Mittwoch 14. Oktober 2015, 14:29

Samstag 12. Juni 2021, 10:32

Das ist der Grund. Die sind quasi als Modulintern markiert. Nicht wirklich konsequent in diesem Fall, weil die anderen Funktionen auch nicht öffentlich sinnvoll sind.
Benutzeravatar
snafu
User
Beiträge: 6331
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Samstag 12. Juni 2021, 10:47

Den Unterstrich nehme ich gerne für sehr spezifische Dinge, die zwar notwendig sind, aber nicht so richtig zur Schnittstelle passen (nach meinem Empfinden). Die Grenzen sind da fließend, je nachdem wie man "Teil der Schnittstelle" definiert. Was ist zB mit get_json()? Nicht gerade spezifisch, sondern total allgemein gehalten, aber auch wieder ein technisches Detail, das für die Schnittstelle nicht von Belang, unter der Haube aber sehr wichtig ist. Somit eigentlich auch internal. Man könnte die Unterstriche so gesehen auch komplett weglassen, da es technisch bis auf ein paar Details ohnehin keinen Unterschied macht, aber ich mag halt schon eine gewisse Abgrenzung.
Benutzeravatar
__blackjack__
User
Beiträge: 8716
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Samstag 12. Juni 2021, 13:05

@rogerb: Auf Modulebene hat der Unterstrich eventuell sogar einen Effekt, nämlich dann wenn man ein ``from module import *`` macht, dann importiert das * alles *ausser* Namen die mit einem Unterstrich anfangen.

Und weil Du auch andere Code-Beispiele erwähnt hast: Bei lokalen Namen hat der Unterstrich die Bedeutung, dass der Wert nicht verwendet wird. Sieht man dann beispielsweise bei unbenutzten Argumenten, oder beim „unpacking“, oder in Schleifen. Da dann öfter auch mal mit dem ”extrem” das nur `_` als Name übrig bleibt.
long long ago; /* in a galaxy far far away */
rogerb
User
Beiträge: 123
Registriert: Dienstag 26. November 2019, 23:24

Samstag 12. Juni 2021, 15:51

Genau, wobei '_' im Gegensatz zu '_name' schon wieder eine ganz andere Bedeutung und Anwendung hat.
Antworten