Aktuelle Corona-Inzidenz

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

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: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

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: 1366
Registriert: Montag 14. Mai 2018, 14:44
Wohnort: Kreis Unna NRW

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: 1012
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

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: 1366
Registriert: Montag 14. Mai 2018, 14:44
Wohnort: Kreis Unna NRW

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: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

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

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: 1632
Registriert: Samstag 16. April 2011, 12:47

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: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

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: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

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: 878
Registriert: Dienstag 26. November 2019, 23:24

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: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

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: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

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: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@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.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

Genau, wobei '_' im Gegensatz zu '_name' schon wieder eine ganz andere Bedeutung und Anwendung hat.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

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.
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

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()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

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()
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@snafu: Warum zurücksetzen der Locale?
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

__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().
Antworten