Seite 1 von 2

Aktuelle Corona-Inzidenz

Verfasst: Sonntag 6. Juni 2021, 10:09
von snafu
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.

Re: Aktuelle Corona-Inzidenz

Verfasst: Sonntag 6. Juni 2021, 13:29
von snafu
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()

Re: Aktuelle Corona-Inzidenz

Verfasst: Sonntag 6. Juni 2021, 13:40
von ThomasL
Bild

Re: Aktuelle Corona-Inzidenz

Verfasst: Montag 7. Juni 2021, 09:18
von DeaD_EyE
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.

Re: Aktuelle Corona-Inzidenz

Verfasst: Montag 7. Juni 2021, 13:49
von ThomasL
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.

Re: Aktuelle Corona-Inzidenz

Verfasst: Montag 7. Juni 2021, 13:57
von __deets__
Weswegen ja auch Konfidenzintervall zu solchen Zahlen gehören. Was die offiziellen Quellen natürlich auch machen.

Re: Aktuelle Corona-Inzidenz

Verfasst: Montag 7. Juni 2021, 18:45
von snafu
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!

Re: Aktuelle Corona-Inzidenz

Verfasst: Montag 7. Juni 2021, 18:53
von nezzcarth
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. :)

Re: Aktuelle Corona-Inzidenz

Verfasst: Montag 7. Juni 2021, 19:30
von snafu
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()

Re: Aktuelle Corona-Inzidenz

Verfasst: Samstag 12. Juni 2021, 08:10
von snafu
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...

Re: Aktuelle Corona-Inzidenz

Verfasst: Samstag 12. Juni 2021, 10:12
von rogerb
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?

Re: Aktuelle Corona-Inzidenz

Verfasst: Samstag 12. Juni 2021, 10:32
von __deets__
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.

Re: Aktuelle Corona-Inzidenz

Verfasst: Samstag 12. Juni 2021, 10:47
von snafu
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.

Re: Aktuelle Corona-Inzidenz

Verfasst: Samstag 12. Juni 2021, 13:05
von __blackjack__
@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.

Re: Aktuelle Corona-Inzidenz

Verfasst: Samstag 12. Juni 2021, 15:51
von rogerb
Genau, wobei '_' im Gegensatz zu '_name' schon wieder eine ganz andere Bedeutung und Anwendung hat.

Re: Aktuelle Corona-Inzidenz

Verfasst: Sonntag 13. Juni 2021, 11:12
von snafu
Ein weiterer Effekt: help() zeigt Namen mit vorangestelltem Unterstrich nicht in der Übersicht an. Somit sind sie auch nochmal ein bisschen "private". Ein richtiges Verstecken vor dem User ist das natürlich nicht. Mir geht es da wie gesagt auch mehr um die Abgrenzung als um eine Art von Geheimhaltung.

Re: Aktuelle Corona-Inzidenz

Verfasst: Freitag 25. Juni 2021, 10:50
von __blackjack__
Ich habe das mal um eine Sortierung nach Namen ergänzt, damit das nicht mehr davon abhängig ist in welcher Reihenfolge die API die Ergebnisse liefert.

Code: Alles auswählen

#!/usr/bin/env python3
import sys
from operator import itemgetter
from types import MappingProxyType

import requests
import rich
from requests.exceptions import RequestException
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):
    for limit, color in [
        (35, "bright_green"),
        (50, "bright_yellow"),
        (100, "orange3"),
    ]:
        if value < limit:
            break
    else:
        color = "bright_red"

    return Text(f"{value:.01f}", color)


COLUMN_SPECS = MappingProxyType(
    {
        "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().casefold() 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"].casefold()
    }


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


_fail = sys.exit


def main():
    query = sys.argv[1] if len(sys.argv) > 1 else QUERY
    try:
        rich.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()

Re: Aktuelle Corona-Inzidenz

Verfasst: Freitag 25. Juni 2021, 18:31
von snafu
Nun werden auch Trennzeichen bei Werten >= 1000 angezeigt. Ich habe in die Funktion viel mehr Möglichkeiten eingebaut als für diesen Anwendungsfall nötig gewesen wären. Auch hätte man locale.format() benutzen können. Ein Verändern und sauberes Zurücksetzen der Locale-Settings wäre aber auch nicht sooo viel weniger Code gewesen. Insofern habe ich mich für den Nachbau entschieden.

Code: Alles auswählen

#!/usr/bin/env python3
import sys
from operator import itemgetter
from types import MappingProxyType
from functools import partial

import requests
import rich
from requests.exceptions import RequestException
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):
    for limit, color in [
        (35, "bright_green"),
        (50, "bright_yellow"),
        (100, "orange3"),
    ]:
        if value < limit:
            break
    else:
        color = "bright_red"

    return Text(f"{value:.01f}", color)


def format_integer(value, sep, grouplen=3):
    s = str(int(value))
    if len(s) <= grouplen:
        return s
    offset = s.startswith("-") + (len(s) % grouplen)
    groups = [s[:offset]]
    groups.extend(
        s[i:(i + grouplen)] for i in
        range(offset, len(s), grouplen)
    )
    return sep.join(groups)


COLUMN_SPECS = MappingProxyType(
    {
        "Landkreis": ("name", str),
        "Inzidenz": ("weekIncidence", format_incidence),
        "Gesamtfälle": ("cases", partial(format_integer, sep=".")),
        "Verstorbene": ("deaths", partial(format_integer, sep=".")),
    }
)


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


def get_district_records(query):
    terms = [term.strip().casefold() 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"].casefold()
    }


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


_fail = sys.exit


def main():
    query = sys.argv[1] if len(sys.argv) > 1 else QUERY
    try:
        rich.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()

Re: Aktuelle Corona-Inzidenz

Verfasst: Samstag 26. Juni 2021, 00:09
von __blackjack__
@snafu: Warum zurücksetzen der Locale?

Re: Aktuelle Corona-Inzidenz

Verfasst: Samstag 26. Juni 2021, 05:25
von snafu
__blackjack__ hat geschrieben: Samstag 26. Juni 2021, 00:09 @snafu: Warum zurücksetzen der Locale?
Na, wenn man das Locale nicht ändert, bekommt man Kommas als Tausender-Trenner. Wie würdest du es denn machen, um stattdessen an Punkte zu kommen? Jetzt mal abgesehen von einem text.replace().