Dynamik der Covid-19-Ausbreitung

Hier könnt Ihr gute, von Euch geschriebene Codes posten. Sie müssen auf jeden Fall funktionieren und sollten möglichst effizient, elegant und beispielhaft oder einfach nur cool sein.
Nino
Beiträge: 1300
Registriert: 13.05.2010 09:26
Wohnort: Berlin

Dynamik der Covid-19-Ausbreitung

Beitrag von Nino »

Dieses Programm berechnet für 185 Länder und die EU die Anzahl der akut Erkrankten, die Verdoppelungszeiten und die Anzahl der Neuerkrankten. Außerdem wird die jeweilige Prävalenz (akut Erkrankte pro 100 000 Einwohner) sowie die Inzidenz (Neuerkrankte pro 100 000 Einwohner pro Woche) berechnet. Diese relativen Zahlen sind wichtige epidemiologische Größen, die u.a. den Vergleich verschiedener Länder ermöglichen.

Auf der Basis von weniger als 20 Fällen werden keine Berechnungen vorgenommen, das Programm gibt dann "----" aus. Wo die Fallzahlen nicht mehr exponentiell steigen, gibt das Programm statt der Verdoppelungszeit "n/a" aus.

Im ersten Programmteil kann als 1. Sortierpriorität Prävalenz, Verdoppelungszeit oder Inzidenz gewählt werden.

Wenn man das freie Statistikprogramm R auf seinem PC hat und der Konstanten '#R_Script$' am Beginn des Codes den passenden absoluten oder relativen Pfad zuweist, so wird im zweiten Teil des Programms die Berechnung des Trends für das gewählte Land ggf. verbessert.

Der dritte Programmteil kann eine CSV-Datei mit dem Verlauf der Prävalenzen in ausgewählten Ländern erzeugen (s. Abb. unten, mit LibreOffice Calc erzeugt).

Bei der Interpretation von Daten zu Covid-19 sollte man immer bedenken, dass es bei allen Zahlen eine -- evtl. sehr große -- Dunkelziffer gibt. Z.B. werden längst nicht alle mit SARS-CoV-2 infizierten Personen getestet, auch gibt es im deutschen Infektionsschutzgesetz keine Meldepflicht für genesene Patienten.

Code: Alles auswählen

; Dynamik der Covid-19-Ausbreitung, Version 1.32
; <https://www.purebasic.fr/german/viewtopic.php?f=8&t=31922>
;
; Cross-platform, getestet mit PB 5.72 LTS unter
; - Linux Mint 19.3
; - Windows 10
; - Mac OS

EnableExplicit

CompilerIf #PB_Compiler_OS = #PB_OS_Linux
   #R_Script$ = ""     ; Wenn R installiert ist, hier "Rscript" eintragen.
CompilerElseIf #PB_Compiler_OS = #PB_OS_Windows
   #R_Script$ = ""     ; z.B. "..\R\R-3.6.3\bin\Rscript.exe"
CompilerElseIf #PB_Compiler_OS = #PB_OS_MacOS
   #R_Script$ = ""
CompilerEndIf

CompilerIf #PB_Compiler_Debugger = #False
   CompilerError "Bitte den Debugger einschalten"
CompilerEndIf

;---------------------------------------------------------------------------------------
;- Utility functions

Macro FastMid (_string_, _start_, _length_=-1)
   PeekS(@_string_ + ((_start_)-1)*SizeOf(Character), _length_)
EndMacro


Procedure.i CountStringEx (source$, stringToCount$, quoteChar$=#DQUOTE$)
   ; in : source$       : zu durchsuchender String;
   ;                      Fehlt ein rechtes Anführungszeichen, so wird es am Stringende angenommen.
   ;      stringToCount$: zu zählender String
   ;      quoteChar$    : verwendetes Anführungszeichen (1 Zeichen);
   ;                      "" um Anführungszeichen zu ignorieren
   ; out: Häufigkeit von 'stringToCount$' in 'source$' *außerhalb von Anführungszeichen*,
   ;      -1 bei Fehler
   Protected.i left, right, count=0

   If FindString(stringToCount$, quoteChar$) > 0
      ProcedureReturn -1       ; error
   EndIf

   left = 1
   Repeat
      right = FindString(source$, quoteChar$, left)          ; position of left "
      If right = 0                                           ; z.B. immer wenn quoteChar$ = #Empty$
         count + CountString(FastMid(source$, left), stringToCount$)
         Break
      EndIf

      count + CountString(FastMid(source$, left, right - left), stringToCount$)
      left = FindString(source$, quoteChar$, right+1) + 1    ; position directly behind right "
   Until left = 1                                            ; rechtes " am Stringende angenommen

   ProcedureReturn count
EndProcedure


Procedure.i SplitIntoFields (source$, sep$, Array field$(1), quoteChar$=#DQUOTE$)
   ; -- Aufteilen eines Strings in einzelne Array-Elemente:
   ;    - 'sep$' wird nur außerhalb von Anführungszeichen berücksichtigt
   ;    - ist schneller als der wiederholte Aufruf von StringField()
   ; in : source$   : aufzuteilender String;
   ;                  Fehlt ein rechtes Anführungszeichen, so wird es am Stringende angenommen.
   ;      sep$      : Trennstring (aus 1 oder mehreren Zeichen);
   ;                  Wenn sep$ = "", dann wird ein Array mit 1 Element zurückgegeben
   ;                  das gleich 'source$' ist (analog zu StringField(..., 1, "")).
   ;      quoteChar$: verwendetes Anführungszeichen (1 Zeichen);
   ;                  "" um Anführungszeichen zu ignorieren
   ; out: field$()    : Array mit allen Feldern (von evtl. vorhandene Anführungszeichen eingeschlossen)
   ;      return value: 1 on success
   ;                    0 on warning
   ;                   -1 on error
   Protected.i lastElement, left, right, leftQuote, rightQuote, index=0, ret=1
   Protected.i sourceLen = Len(source$)
   Protected.i sepLen = Len(sep$)

   lastElement = CountStringEx(source$, sep$, quoteChar$)
   If lastElement = -1
      ProcedureReturn -1                        ; error
   EndIf

   Dim field$(lastElement)

   left = 1
   Repeat
      right = FindString(source$, sep$, left)   ; position of sep$
      If right
         leftQuote = FindString(source$, quoteChar$, left)
         If leftQuote > 0 And leftQuote < right
            rightQuote = FindString(source$, quoteChar$, leftQuote+1)
            If rightQuote
               right = FindString(source$, sep$, rightQuote+1)
            Else
               right = sourceLen + 1            ; rechtes " am Stringende angenommen
               ret = 0                          ; warning
            EndIf
         EndIf
      EndIf
      If right = 0
         right = sourceLen + 1
      EndIf

      field$(index) = FastMid(source$, left, right - left)
      index + 1

      left = right + sepLen                     ; position directly behind sep$
   Until left > sourceLen

   ProcedureReturn ret
EndProcedure


Procedure.i Monotone (Array a.i(1), lo.i=0, hi.i=-1)
   ; -- Check whether all values in (a selected range of) an
   ;    integer array are (strictly) decreasing or increasing
   ; in : a(): array to be checked
   ;      lo : least   index to be considered
   ;      hi : largest index to be considered
   ; out: The values in the examined range are
   ;      -2: strictly decreasing
   ;      -1: monotonically decreasing
   ;       0: neither monotonically decreasing nor increasing
   ;       1: monotonically increasing
   ;       2: strictly increasing
   ; see <https://en.wikipedia.org/wiki/Monotonic_function>
   Protected.i i, k, ret, sgn, factor=2

   If hi = -1
      hi = ArraySize(a())
   EndIf

   ; -- Look for the first difference <> 0
   i = lo + 1
   Repeat
      If i > hi
         ProcedureReturn 0       ; neither decreasing nor increasing
      EndIf
      ret = Sign(a(i) - a(i-1))
      i + 1
   Until ret <> 0
   If i > lo + 2
      factor = 1                 ; not *strictly* monotone
   EndIf

   ; -- Check whether all other differences <> 0 have
   ;    the same sign as the first difference <> 0
   For k = i To hi
      sgn = Sign(a(k) - a(k-1))
      If sgn = 0
         factor = 1              ; not *strictly* monotone
      ElseIf sgn <> ret
         ProcedureReturn 0       ; not monotone
      EndIf
   Next

   ProcedureReturn ret * factor
EndProcedure


Procedure.d R_cor (x$, y$, *cor.Double, method$)
   ; -- Korrelation mit R berechnen (getestet mit R 3.6.3)
   ; in : x$, y$ : "parallel" lists of numeric values, separated by ',' (e.g. "1,2,3")
   ;      method$: 'pearson', 'kendall', or 'spearman' (can be abbreviated)
   ; out: cor\d       : Korrelationskoeffizient (aus dem Intervall [-1, 1])
   ;      return value: p-Wert (aus dem Intervall [0, 1])
   ;                    bzw. -1, -2, -3, -4 oder -5 bei Fehler
   Protected script$, result$, line$
   Protected fn.i, p.i, ret.d=-1

   If FindString(" pearson kendall spearman", " "+method$) = 0
      ProcedureReturn -1      ; error
   EndIf

   If CountString(x$, ",") <> CountString(y$, ",")
      ProcedureReturn -2      ; error
   EndIf

   script$ = GetTemporaryDirectory() + "script.r"
   result$ = GetTemporaryDirectory() + "r_out.txt"

   fn = CreateFile(#PB_Any, script$)
   If fn = 0
      ProcedureReturn -3      ; error
   EndIf

   WriteStringN(fn, "x <- c(" + x$ + ")")
   WriteStringN(fn, "y <- c(" + y$ + ")")
   WriteStringN(fn, "cor.test(x, y, method='" + method$ + "')")
   CloseFile(fn)

   CompilerIf #PB_Compiler_OS = #PB_OS_Linux
      If RunProgram("/bin/bash", ~"-c \"" + #R_Script$ + " " + script$ + " > " + result$ + ~"\"", "", #PB_Program_Wait|#PB_Program_Hide) = #False
         ProcedureReturn -4      ; error
      EndIf
   CompilerElseIf #PB_Compiler_OS = #PB_OS_Windows
      If RunProgram(#R_Script$, script$ + " > " + result$, "", #PB_Program_Wait|#PB_Program_Hide) = #False
         ProcedureReturn -4      ; error
      EndIf
   CompilerElseIf #PB_Compiler_OS = #PB_OS_MacOS
      ; not yet supported
   CompilerEndIf

   fn = ReadFile(#PB_Any, result$)
   If fn = 0
      ProcedureReturn -5      ; error
   EndIf

   ReadStringFormat(fn)
   While Eof(fn) = #False
      line$ = ReadString(fn)
      p = FindString(line$, "p-value")
      If p
         ret = ValD(Mid(line$, p+10))
      ElseIf FindString(line$, "sample estimates:") = 1
         ReadString(fn)
         line$ = ReadString(fn)
         *cor\d = ValD(line$)
      EndIf
   Wend
   CloseFile(fn)

   ProcedureReturn ret
EndProcedure

Procedure.i R_cor_int (Array y.i(1), lo.i=0, hi.i=-1)
   ; -- Wrapper für die Prozedur R_cor() für Integer-Arrays
   ; in : y(): array to be checked
   ;      lo : least   index to be considered
   ;      hi : largest index to be considered
   ; out: return value: -1: decreasing trend
   ;                     0: steady trend
   ;                     1: increasing trend
   ;                    -7: error
   Protected x$, y$, i.i, p.d, cor.d

   If hi = -1
      hi = ArraySize(y())
   EndIf

   x$ = Str(lo)
   y$ = Str(y(lo))
   For i = lo+1 To hi
      x$ + "," + Str(i)
      y$ + "," + Str(y(i))
   Next

   p = R_cor(x$, y$, @cor, "spearman")

   If p < 0
      ProcedureReturn -7    ; error
   ElseIf p > 0.05 Or cor = 0.0
      ProcedureReturn 0
   ElseIf cor < 0.0
      ProcedureReturn -1
   Else
      ProcedureReturn 1
   EndIf
EndProcedure

Procedure.i CalcTrend (Array a.i(1), iTarget.i, numElements.i, useR.i)
   ; -- Trend in den 'numElements' Elementen bis a(iTarget) berechnen
   ; in : a()        : zu prüfendes Array
   ;      iTarget    : Array-Index des interessierenden Elements
   ;      numElements: Anzahl zu berücksichtigender Elemente
   ;      useR       : 'Rscript' benutzen (#True/#False)
   ; out: return value: 0: bleibt ungefähr gleich
   ;                    1: bleibt gleich
   ;                   -2: nimmt ab
   ;                   -4: nimmt zunehmend langsamer ab
   ;                   -6: nimmt zunehmend schneller ab
   ;                    2: nimmt zu
   ;                    4: nimmt zunehmend langsamer zu
   ;                    6: nimmt zunehmend schneller zu
   ;                   -7: Fehler
   Protected.i i, k, rise, ret
   Protected Dim diff.i(numElements-2)

   ret = Monotone(a(), iTarget-numElements+1, iTarget)   ; -2, -1, 0, 1 oder 2

   If ret                               ; (streng) monoton steigend oder fallend
      If Abs(ret) = 1
         ret * 2
      EndIf

      ; prüfen, ob eine weitergehende Aussage gemacht werden kann
      k = 0
      For i = iTarget-numElements+2 To iTarget
         diff(k) = a(i) - a(i-1)
         k + 1
      Next

      rise = Monotone(diff())           ; -2, -1, 0, 1 oder 2

      If Sign(rise) = Sign(ret)
         ret * 3
      ElseIf Sign(rise) = -Sign(ret)
         ret * 2

      ElseIf useR                       ; präzisere Trendberechnung
         rise = R_cor_int(diff())       ; -1, 0, 1 oder -7
         If rise = -7
            ret = -7                    ; error
         ElseIf Sign(rise) = Sign(ret)
            ret * 3
         ElseIf Sign(rise) = -Sign(ret)
            ret * 2
         EndIf
      EndIf

   ElseIf useR                                               ; präzisere Trendberechnung
      ret = R_cor_int(a(), iTarget-numElements+1, iTarget)   ; -1, 0, 1 oder -7
      If ret = 0
         ret = 1
      Else
         ret * 2
      EndIf
   EndIf

   ProcedureReturn ret
EndProcedure


Procedure.s RAlign (s$, width.i, char$=" ")
   ; -- String rechts ausrichten
   ; in: s$   : auszurichtender String
   ;     width: auszufüllender Platz (Anzahl Zeichen)
   ;     char$: verwendetes Füllzeichen (nur 1 Zeichen!)

   If Len(s$) >= width
      ProcedureReturn s$
   Else
      ProcedureReturn RSet(s$, width, char$)
   EndIf
EndProcedure


Macro RepeatChar (_count_, _char_=" ")
   ; *sehr* schnell, geht aber nur mit 1 Zeichen
   ; (evtl. folgende Zeichen werden ignoriert)
   LSet("", _count_, _char_)
EndMacro


Macro RoundEx (_number_, _factor_, _mode_=#PB_Round_Nearest)
   ; in: _number_: zu rundende Zahl
   ;     _factor_:  10 um auf 1 Nachkommastelle  zu runden,
   ;               100 um auf 2 Nachkommastellen zu runden usw.
   ;     _mode_  : Art der Rundung: #PB_Round_Nearest, #PB_Round_Up oder #PB_Round_Down
   (Round((_number_) * (_factor_), _mode_) / (_factor_))
EndMacro

;---------------------------------------------------------------------------------------

#Auto = -1
#SecondsPerDay = 24 * 60 * 60
#DateMaskIn$  = "%mm/%dd/%yy"
#DateMaskOut$ = "%dd.%mm.%yyyy"
#None$ = "    ----"
#NA$ = "n/a"
#MinCases = 20             ; Mindestanzahl von Fällen pro Land
#TrendDays = 7             ; Anzahl der Tage zur Berechnung des aktuellen Trends

#CSV_IndexCountry   = 1
#CSV_IndexFirstDate = 4

; Zahlen für ein Land
Structure Record
   Population.i            ; Einwohnerzahl
   Array Confirmed.i(0)    ; bestätigte Fälle
   Array Sick.i(0)         ; akut Erkrankte (= bestätigte Fälle - Genesene - Verstorbene)
EndStructure

Structure CSV
   Array Date.i(0)         ; Datumsangaben
   Map Country.Record()    ; alle Länder
EndStructure

; "Arbeitsdaten" für ein Land für Procedure CurrentData() zum Sortieren
Structure Region
   Name$                   ; Name des Staates
   ErrDate.i               ; erster Tag im betrachteten Zeitraum, dessen Daten nicht konsistent sind
   Sick.i                  ; akut Erkrankte
   Td.d                    ; Verdoppelungszeit der Anzahl akut Erkrankter
   New.i                   ; Neuerkrankte in der Woche bis zum jeweiligen Tag
   Prevalence.d            ; Prävalenz (Erkrankte pro 100 000 Einwohner)
   Incidence.d             ; Inzidenz  (Neuerkrankte pro 100 000 Einwohner und Woche)
EndStructure

; Sortieroptionen für Procedure CurrentData()
Enumeration 1
   #SortByPrevalence
   #SortByDoublingTime
   #SortByIncidence
EndEnumeration


Procedure.d DoublingTime (Array dates.i(1), Array cases.i(1), iFirst.i, iTarget.i)
   ; -- Verdoppelungszeit
   ; in : dates(): Array mit Datumsangaben (aufeinanderfolgende Tage)
   ;      cases(): Array mit Fallzahlen;
   ;         Die beiden Arrays sind "parallel".
   ;      iFirst : Index des ersten zu berücksichtigenden Array-Elements
   ;      iTarget: Index des interessierenden Datums
   ; out: return value: Zeit in Tagen, in der sich die Anzahl der Fälle verdoppelt hat
   ;                    (in Bezug auf dates(iTarget));
   ;                    10 000 wenn keine Verdoppelung erkennbar ist
   Protected.i iDate0, halfCases, halfDate, diff

   halfCases = cases(iTarget) / 2
   iDate0 = iTarget - 1
   While iDate0 >= iFirst
      diff = cases(iDate0+1) - cases(iDate0)
      If diff < 0            ; Für die Berechnung der Verdoppelungszeit
         Break               ; müssen die Fallzahlen monoton steigen.
      EndIf

      If cases(iDate0) <= halfCases
         If diff <> 0
            ; halfDate: durch lineare Interpolation zwischen zwei aufeinanderfolgenden Tagen
            ;           geschätzter Zeitpunkt, an dem die Anzahl der Fälle = halfCases ist
            halfDate = dates(iDate0) + (dates(iDate0+1)-dates(iDate0))/diff * (halfCases-cases(iDate0))
            ProcedureReturn (dates(iTarget) - halfDate) / #SecondsPerDay
         Else
            Break
         EndIf
      EndIf

      iDate0 - 1
   Wend

   ProcedureReturn 10000.0
EndProcedure


Procedure.i LastCSVdate (file$, sep$=",")
   ; -- return most recent date in given CSV file
   ; in : file$: local CSV file
   ;      sep$ : separator between data fields
   ; out: return value: last date in header of the given CSV file,
   ;                    or 0 on error
   Protected header$, ifn.i, lastField.i

   ifn = ReadFile(#PB_Any, file$)
   If ifn = 0
      ProcedureReturn 0   ; error
   EndIf
   ReadStringFormat(ifn)
   header$ = ReadString(ifn)
   CloseFile(ifn)
   lastField = CountString(header$, sep$) + 1

   ProcedureReturn ParseDate(#DateMaskIn$, StringField(header$, lastField, sep$))
EndProcedure

;---------------------------------------------------------------------------------------

Define s_FileConfirmed$, s_FileRecovered$, s_FileDeaths$

Procedure.i GetCSVfiles (download.i)
   ; in : download: #True : download 3 CSV files to the same directory where the program is
   ;                #False: use local CSV files
   ;                #Auto : download CSV files only if local files are not up-to-date
   ; out: shared names of downloaded files
   ;      return value: 0 on success / 1 or 2 on error
   Shared s_FileConfirmed$, s_FileRecovered$, s_FileDeaths$
   Protected urlConfirmed$, urlRecovered$, urlDeaths$
   Protected.i lastDate, yesterday

   ; Übersicht: <https://github.com/CSSEGISandData/COVID-19>
   urlConfirmed$ = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_confirmed_global.csv"
   urlRecovered$ = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_recovered_global.csv"
   urlDeaths$    = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_deaths_global.csv"

   s_FileConfirmed$ = GetFilePart(GetURLPart(urlConfirmed$, #PB_URL_Path))
   s_FileRecovered$ = GetFilePart(GetURLPart(urlRecovered$, #PB_URL_Path))
   s_FileDeaths$    = GetFilePart(GetURLPart(urlDeaths$,    #PB_URL_Path))

   If download = #Auto
      If FileSize(s_FileConfirmed$) = -1
         download = #True
      Else
         lastDate = LastCSVdate(s_FileConfirmed$)
         If lastDate = 0
            ProcedureReturn 1        ; error
         EndIf
         yesterday = Int(AddDate(Date(), #PB_Date_Day, -1) / #SecondsPerDay) * #SecondsPerDay
         If lastDate = yesterday
            download = #False
         Else
            download = #True
         EndIf
      EndIf
   EndIf

   If download = #True
      InitNetwork()
      If ReceiveHTTPFile(urlConfirmed$, s_FileConfirmed$) = 0 Or
         ReceiveHTTPFile(urlRecovered$, s_FileRecovered$) = 0 Or
         ReceiveHTTPFile(urlDeaths$,    s_FileDeaths$)    = 0
         ProcedureReturn 2           ; error
      EndIf
   EndIf

   ProcedureReturn 0                 ; success
EndProcedure


Procedure.i AddFirstCSV (file$, *covid.CSV, sep$=",")
   ; -- erste CSV-Datei lesen und die Fallzahlen zu
   ;    *covid\Country()\Confirmed() sowie *covid\Country()\Sick() addieren
   ; in : file$: local CSV file
   ;      sep$ : separator between data fields
   ; out: *covid      : data from the CSV file
   ;      return value: 0 on success / 1, 2, 3, 4 or 5 on error
   Protected header$, record$, country$
   Protected.i ifn, lastField, i
   Protected Dim field$(0)

   ifn = ReadFile(#PB_Any, file$)
   If ifn = 0
      ProcedureReturn 1            ; error
   EndIf

   ReadStringFormat(ifn)
   header$ = ReadString(ifn)
   If SplitIntoFields(header$, sep$, field$()) <= 0
      CloseFile(ifn)
      ProcedureReturn 2            ; error
   EndIf
   lastField = ArraySize(field$())
   If lastField = 0
      CloseFile(ifn)
      ProcedureReturn 3            ; error
   EndIf

   Dim *covid\Date(lastField)
   *covid\Date(#CSV_IndexFirstDate) = ParseDate(#DateMaskIn$, field$(#CSV_IndexFirstDate))
   For i = #CSV_IndexFirstDate+1 To lastField
      *covid\Date(i) = ParseDate(#DateMaskIn$, field$(i))
      ; Datumsangaben auf Plausibilität prüfen:
      If *covid\Date(i-1) >= *covid\Date(i)
         CloseFile(ifn)
         ProcedureReturn 4         ; error
      EndIf
   Next

   While Eof(ifn) = #False
      record$ = ReadString(ifn)
      If SplitIntoFields(record$, sep$, field$()) <= 0
         CloseFile(ifn)
         ProcedureReturn 5         ; error
      EndIf

      country$ = Trim(field$(#CSV_IndexCountry), #DQUOTE$)

      If FindMapElement(*covid\Country(), country$) = #Null
         AddMapElement(*covid\Country(), country$, #PB_Map_NoElementCheck)
         Dim *covid\Country()\Confirmed(lastField)
         Dim *covid\Country()\Sick(lastField)
      EndIf

      For i = #CSV_IndexFirstDate To lastField
         *covid\Country()\Confirmed(i) + Val(field$(i))
         *covid\Country()\Sick(i) = *covid\Country()\Confirmed(i)
      Next
   Wend

   CloseFile(ifn)
   ProcedureReturn 0               ; success
EndProcedure


Procedure.i SubNextCSV (file$, *covid.CSV, sep$=",")
   ; -- nächste CSV-Datei lesen und die Fallzahlen von *covid\Country()\Sick() subtrahieren
   ; in : file$ : local CSV file
   ;      *covid: data from the CSV files
   ;      sep$  : separator between data fields
   ; out: *covid      : adapted data from the CSV files
   ;      return value: 0 on success / 1, 2, 3, 4, 5 or 6 on error
   Protected header$, record$, country$
   Protected.i ifn, lastField, i
   Protected Dim field$(0)

   ifn = ReadFile(#PB_Any, file$)
   If ifn = 0
      ProcedureReturn 1            ; error
   EndIf

   ReadStringFormat(ifn)
   header$ = ReadString(ifn)
   If SplitIntoFields(header$, sep$, field$()) <= 0
      CloseFile(ifn)
      ProcedureReturn 2            ; error
   EndIf
   lastField = ArraySize(field$())

   ; prüfen, ob der Kopfdatensatz dieser CSV-Datei mit demjenigen der 1. CSV-Datei übereinstimmt
   If lastField <> ArraySize(*covid\Date())
      CloseFile(ifn)
      ProcedureReturn 3            ; error
   EndIf
   For i = #CSV_IndexFirstDate To lastField
      If *covid\Date(i) <> ParseDate(#DateMaskIn$, field$(i))
         CloseFile(ifn)
         ProcedureReturn 4         ; error
      EndIf
   Next

   While Eof(ifn) = #False
      record$ = ReadString(ifn)
      If SplitIntoFields(record$, sep$, field$()) <= 0
         CloseFile(ifn)
         ProcedureReturn 5         ; error
      EndIf

      country$ = Trim(field$(#CSV_IndexCountry), #DQUOTE$)

      If FindMapElement(*covid\Country(), country$) = #Null
         CloseFile(ifn)
         AddMapElement(*covid\Country(), country$, #PB_Map_NoElementCheck)  ; nur für die Fehlermeldung
         ProcedureReturn 6                                                  ; error
      EndIf

      For i = #CSV_IndexFirstDate To lastField
         *covid\Country()\Sick(i) - Val(field$(i))
      Next
   Wend

   CloseFile(ifn)
   ProcedureReturn 0               ; success
EndProcedure


Macro AssignPopulation (_country_, _size_)
   ; -- Assign population figure only if the country already exists in the map.
   ; in : _country_: name of country
   ;      _size_   : size of its population
   ; out: *covid\Country()\Population aktualisiert
   ;      knownCountries aktualisiert

   knownCountries + 1
   If FindMapElement(*covid\Country(), _country_)
      *covid\Country()\Population = _size_
   Else
      AddMapElement(*covid\Country(), _country_, #PB_Map_NoElementCheck)  ; nur für die Fehlermeldung
      ProcedureReturn 1                                                   ; error
   EndIf
EndMacro

Procedure.i InitCountries (*covid.CSV)
   ; -- Einwohnerzahlen von 185 Staaten erfassen, auf 1 000 gerundet
   ;    [aus der engl. Wikipedia (April/Mai 2020)
   ;     <https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)>]
   ;    sowie die Zahlen aller 27 EU-Mitgliedsstaaten addieren, so dass
   ;    dann die EU wie ein Staat behandelt werden kann
   ; in : *covid: data from the CSV files
   ; out: *covid      : data from the CSV files, supplemented by
   ;                    population figures of all contained countries,
   ;                    and data for the EU as a whole
   ;      return value: 0 on success / 1 on error
   Protected *ptrEU.Record, msg$
   Protected.i i, newCountries, knownCountries=0, lastField=ArraySize(*covid\Date())
   Protected NewMap europe.i()

   ; -- Namen und Einwohnerzahlen aller 27 EU-Mitgliedsstaaten
   ;    zunächst in einer Map speichern
   europe("Austria")     =  8955000
   europe("Belgium")     = 11539000
   europe("Bulgaria")    =  7000000
   europe("Croatia")     =  4130000
   europe("Cyprus")      =  1180000
   europe("Czechia")     = 10689000
   europe("Denmark")     =  5772000
   europe("Estonia")     =  1326000
   europe("Finland")     =  5532000
   europe("France")      = 65692000
   europe("Germany")     = 83517000
   europe("Greece")      = 10473000
   europe("Hungary")     =  9685000
   europe("Ireland")     =  4882000
   europe("Italy")       = 60550000
   europe("Latvia")      =  1907000
   europe("Lithuania")   =  2760000
   europe("Luxembourg")  =   616000
   europe("Malta")       =   440000
   europe("Netherlands") = 17097000
   europe("Poland")      = 37888000
   europe("Portugal")    = 10226000
   europe("Romania")     = 19365000
   europe("Slovakia")    =  5457000
   europe("Slovenia")    =  2079000
   europe("Spain")       = 46737000
   europe("Sweden")      = 10036000

   ; -- Einwohnerzahlen der EU-Staaten zuweisen
   ForEach europe()
      AssignPopulation(MapKey(europe()), europe())
   Next

   ; -- Zahlen für die gesamte EU berechnen
   ;    (falls in der Map kein Land namens "EU" vorkommt)
   If FindMapElement(*covid\Country(), "EU") = #Null
      *ptrEU = AddMapElement(*covid\Country(), "EU", #PB_Map_NoElementCheck)
      knownCountries + 1
      Dim *ptrEU\Confirmed(lastField)
      Dim *ptrEU\Sick(lastField)

      ForEach europe()
         *ptrEU\Population + europe()
         FindMapElement(*covid\Country(), MapKey(europe()))
         For i = #CSV_IndexFirstDate To lastField
            *ptrEU\Confirmed(i) + *covid\Country()\Confirmed(i)
            *ptrEU\Sick(i)      + *covid\Country()\Sick(i)
         Next
      Next
   EndIf

   ; -- Einwohnerzahlen anderer Staaten zuweisen
   AssignPopulation("Afghanistan",                        38042000)
   AssignPopulation("Albania",                             2881000)
   AssignPopulation("Algeria",                            43053000)
   AssignPopulation("Andorra",                               77000)
   AssignPopulation("Angola",                             31825000)
   AssignPopulation("Antigua and Barbuda",                   97000)
   AssignPopulation("Argentina",                          44781000)
   AssignPopulation("Armenia",                             2958000)
   AssignPopulation("Australia",                          25203000)
   AssignPopulation("Azerbaijan",                         10048000)
   AssignPopulation("Bahamas",                              389000)
   AssignPopulation("Bahrain",                             1641000)
   AssignPopulation("Bangladesh",                        163046000)
   AssignPopulation("Barbados",                             287000)
   AssignPopulation("Belarus",                             9452000)
   AssignPopulation("Belize",                               390000)
   AssignPopulation("Benin",                              11801000)
   AssignPopulation("Bhutan",                               763000)
   AssignPopulation("Bolivia",                            11513000)
   AssignPopulation("Bosnia and Herzegovina",              3301000)
   AssignPopulation("Botswana",                            2304000)
   AssignPopulation("Brazil",                            211050000)
   AssignPopulation("Brunei",                               433000)
   AssignPopulation("Burkina Faso",                       20321000)
   AssignPopulation("Burma",                              54045000)
   AssignPopulation("Burundi",                            10864000)
   AssignPopulation("Cabo Verde",                           550000)
   AssignPopulation("Cambodia",                           16487000)
   AssignPopulation("Cameroon",                           25876000)
   AssignPopulation("Canada",                             37411000)
   AssignPopulation("Central African Republic",            4745000)
   AssignPopulation("Chad",                               15947000)
   AssignPopulation("Chile",                              18952000)
   AssignPopulation("China",                            1433784000)
   AssignPopulation("Colombia",                           50339000)
   AssignPopulation("Comoros",                              851000)
   AssignPopulation("Congo (Brazzaville)",                 5381000)
   AssignPopulation("Congo (Kinshasa)",                   86791000)
   AssignPopulation("Costa Rica",                          5048000)
   AssignPopulation("Cote d'Ivoire",                      25717000)
   AssignPopulation("Cuba",                               11333000)
   AssignPopulation("Djibouti",                             974000)
   AssignPopulation("Dominica",                              72000)
   AssignPopulation("Dominican Republic",                 10739000)
   AssignPopulation("Ecuador",                            17374000)
   AssignPopulation("Egypt",                             100388000)
   AssignPopulation("El Salvador",                         6454000)
   AssignPopulation("Equatorial Guinea",                   1356000)
   AssignPopulation("Eritrea",                             3497000)
   AssignPopulation("Eswatini",                            1148000)
   AssignPopulation("Ethiopia",                          112079000)
   AssignPopulation("Fiji",                                 890000)
   AssignPopulation("Gabon",                               2173000)
   AssignPopulation("Gambia",                              2348000)
   AssignPopulation("Georgia",                             3997000)
   AssignPopulation("Ghana",                              28834000)
   AssignPopulation("Grenada",                              112000)
   AssignPopulation("Guatemala",                          17581000)
   AssignPopulation("Guinea",                             12771000)
   AssignPopulation("Guinea-Bissau",                       1921000)
   AssignPopulation("Guyana",                               783000)
   AssignPopulation("Haiti",                              11264000)
   AssignPopulation("Honduras",                            9746000)
   AssignPopulation("Iceland",                              339000)
   AssignPopulation("India",                            1366418000)
   AssignPopulation("Indonesia",                         270626000)
   AssignPopulation("Iran",                               82914000)
   AssignPopulation("Iraq",                               39310000)
   AssignPopulation("Israel",                              8519000)
   AssignPopulation("Jamaica",                             2948000)
   AssignPopulation("Japan",                             126860000)
   AssignPopulation("Jordan",                             10102000)
   AssignPopulation("Kazakhstan",                         18551000)
   AssignPopulation("Kenya",                              52574000)
   AssignPopulation("Korea, South",                       51225000)
   AssignPopulation("Kosovo",                              1810000)
   AssignPopulation("Kuwait",                              4207000)
   AssignPopulation("Kyrgyzstan",                          6416000)
   AssignPopulation("Laos",                                7169000)
   AssignPopulation("Lebanon",                             6856000)
   AssignPopulation("Lesotho",                             2125000)
   AssignPopulation("Liberia",                             4937000)
   AssignPopulation("Libya",                               6777000)
   AssignPopulation("Liechtenstein",                         38000)
   AssignPopulation("Madagascar",                         26969000)
   AssignPopulation("Malawi",                             18629000)
   AssignPopulation("Malaysia",                           31950000)
   AssignPopulation("Maldives",                             531000)
   AssignPopulation("Mali",                               19658000)
   AssignPopulation("Mauritania",                          4526000)
   AssignPopulation("Mauritius",                           1199000)
   AssignPopulation("Mexico",                            127576000)
   AssignPopulation("Moldova",                             4043000)
   AssignPopulation("Monaco",                                39000)
   AssignPopulation("Mongolia",                            3225000)
   AssignPopulation("Montenegro",                           628000)
   AssignPopulation("Morocco",                            36472000)
   AssignPopulation("Mozambique",                         30366000)
   AssignPopulation("Namibia",                             2495000)
   AssignPopulation("Nepal",                              28609000)
   AssignPopulation("New Zealand",                         4783000)
   AssignPopulation("Nicaragua",                           6546000)
   AssignPopulation("Niger",                              23311000)
   AssignPopulation("Nigeria",                           200964000)
   AssignPopulation("North Macedonia",                     2083000)
   AssignPopulation("Norway",                              5379000)
   AssignPopulation("Oman",                                4975000)
   AssignPopulation("Pakistan",                          216565000)
   AssignPopulation("Panama",                              4246000)
   AssignPopulation("Papua New Guinea",                    8776000)
   AssignPopulation("Paraguay",                            7045000)
   AssignPopulation("Peru",                               32510000)
   AssignPopulation("Philippines",                       108117000)
   AssignPopulation("Qatar",                               2832000)
   AssignPopulation("Russia",                            145872000)
   AssignPopulation("Rwanda",                             12627000)
   AssignPopulation("Saint Kitts and Nevis",                 53000)
   AssignPopulation("Saint Lucia",                          183000)
   AssignPopulation("Saint Vincent and the Grenadines",     111000)
   AssignPopulation("San Marino",                            34000)  ; kleinster Staat in der Liste
   AssignPopulation("Sao Tome and Principe",                215000)
   AssignPopulation("Saudi Arabia",                       34269000)
   AssignPopulation("Senegal",                            16296000)
   AssignPopulation("Serbia",                              6964000)  ; ohne Kosovo (s.o.)
   AssignPopulation("Seychelles",                            98000)
   AssignPopulation("Sierra Leone",                        7813000)
   AssignPopulation("Singapore",                           5804000)
   AssignPopulation("Somalia",                            15443000)
   AssignPopulation("South Africa",                       58558000)
   AssignPopulation("South Sudan",                        11062000)
   AssignPopulation("Sri Lanka",                          21324000)
   AssignPopulation("Sudan",                              42813000)
   AssignPopulation("Suriname",                             581000)
   AssignPopulation("Switzerland",                         8591000)
   AssignPopulation("Syria",                              17070000)
   AssignPopulation("Taiwan*",                            23774000)
   AssignPopulation("Tajikistan",                          9321000)
   AssignPopulation("Tanzania",                           58005000)
   AssignPopulation("Thailand",                           69038000)
   AssignPopulation("Timor-Leste",                         1293000)
   AssignPopulation("Togo",                                8082000)
   AssignPopulation("Trinidad and Tobago",                 1395000)
   AssignPopulation("Tunisia",                            11695000)
   AssignPopulation("Turkey",                             83430000)
   AssignPopulation("Uganda",                             44270000)
   AssignPopulation("Ukraine",                            43994000)
   AssignPopulation("United Arab Emirates",                9771000)
   AssignPopulation("United Kingdom",                     67530000)
   AssignPopulation("Uruguay",                             3462000)
   AssignPopulation("US",                                329065000)
   AssignPopulation("Uzbekistan",                         32982000)
   AssignPopulation("Venezuela",                          28516000)
   AssignPopulation("Vietnam",                            96462000)
   AssignPopulation("West Bank and Gaza",                  4981000)
   AssignPopulation("Western Sahara",                       582000)
   AssignPopulation("Yemen",                              29162000)
   AssignPopulation("Zambia",                             17861000)
   AssignPopulation("Zimbabwe",                           14645000)

   ; -- Kreuzfahrtschiffe und "Heiligen Stuhl" entfernen
   DeleteMapElement(*covid\Country(), "Diamond Princess")
   DeleteMapElement(*covid\Country(), "MS Zaandam")
   DeleteMapElement(*covid\Country(), "Holy See")

   ; -- prüfen, ob die aktuellen Daten der Johns Hopkins Universität neue
   ;    Staaten enthalten
   newCountries = MapSize(*covid\Country()) - knownCountries
   If newCountries > 0
      If newCountries = 1
         msg$ = "Ein neuer Staat ist hinzugekommen," + #LF$ +
                "die Einwohnerzahl sollte erfasst werden:"
      Else
         msg$ = Str(newCountries) + " neue Staaten sind hinzugekommen," + #LF$ +
                "die Einwohnerzahlen sollten erfasst werden:"
      EndIf
      ForEach *covid\Country()
         If *covid\Country()\Population = 0
            msg$ + #LF$ + Space(5) + MapKey(*covid\Country())
         EndIf
      Next
      MessageRequester("Hinweis", msg$, #PB_MessageRequester_Warning)
   EndIf

   ProcedureReturn 0       ; success
EndProcedure


Procedure.i CurrentData (*covid.CSV, sort.i, countries$="")
   ; -- aktuelle Fallzahlen, Prävalenzen, Verdoppelungszeiten, Neuerkrankungen
   ;    und Inzidenzen für ausgewählte oder alle Länder
   ; in : *covid    : Länderdaten
   ;      sort      : #SortByPrevalence, #SortByDoublingTime oder #SortByIncidence
   ;      countries$: Liste der interessierenden Länder (durch ';' getrennt),
   ;                  "" für alle Länder
   ; out: return value: 0 on success / 1 on error
   Protected i.i, iCurDate.i, head2$, msg$
   Protected NewList nation.Region()

   If Asc(countries$) <> ''
      countries$ = ";" + countries$ + ";"
   EndIf
   iCurDate = ArraySize(*covid\Date())

   ForEach *covid\Country()
      If Asc(countries$) = '' Or FindString(countries$, ";"+MapKey(*covid\Country())+";")
         With *covid\Country()
            AddElement(nation())
            nation()\Name$ = MapKey(*covid\Country())
            nation()\Sick  = \Sick(iCurDate)

            If \Sick(iCurDate) >= #MinCases
               If \Population > 0
                  nation()\Prevalence = RoundEx(\Sick(iCurDate) * 100000 / \Population, 10)         ; Prävalenz mit 1 Nachkommastelle
               EndIf
               nation()\Td = RoundEx(DoublingTime(*covid\Date(), \Sick(), #CSV_IndexFirstDate, iCurDate), 10)  ; Tage mit 1 Nachkommastelle
            Else
               nation()\Td = -1.0
               nation()\Prevalence = 10000.0
            EndIf

            ; bestätigte Fälle auf Plausibilität prüfen:
            For i = iCurDate-6 To iCurDate
               If \Confirmed(i) < \Confirmed(i-1)
                  nation()\ErrDate = *covid\Date(i)
                  Break
               EndIf
            Next

            nation()\New = Round(\Confirmed(iCurDate) - \Confirmed(iCurDate-7), #PB_Round_Nearest)  ; durchschnittl. Neuerkrankte pro Woche
            If nation()\New >= #MinCases And \Population > 0
               nation()\Incidence = RoundEx(nation()\New * 100000 / \Population, 10)                ; Inzidenz  mit 1 Nachkommastelle
            Else
               nation()\Incidence = 10000.0
            EndIf
         EndWith
      EndIf
   Next

   head2$ = "Land                      absolut   pro 100 000 Einwohner     Verdoppelungszeit     |   absolut   pro 100 000 Einwohner  "

   ; Der folgende Code funktioniert wie gewünscht, weil SortStructuredList() einen *stabilen* Sortieralgorithmus verwendet.
   SortStructuredList(nation(), #PB_Sort_Ascending|#PB_Sort_NoCase, OffsetOf(Region\Name$), TypeOf(Region\Name$))
   If sort = #SortByPrevalence
      ; Sortierprioritäten: 1. Prävalenz, 2. Verdoppelungszeit, 3. Inzidenz, 4. Ländernamen
      SortStructuredList(nation(), #PB_Sort_Descending, OffsetOf(Region\Incidence),  TypeOf(Region\Incidence))
      SortStructuredList(nation(), #PB_Sort_Ascending,  OffsetOf(Region\Td),         TypeOf(Region\Td))
      SortStructuredList(nation(), #PB_Sort_Descending, OffsetOf(Region\Prevalence), TypeOf(Region\Prevalence))
      PokeC(@head2$ + 2*58, '▼')
   ElseIf sort = #SortByDoublingTime
      ; Sortierprioritäten: 1. Verdoppelungszeit, 2. Inzidenz, 3. Prävalenz, 4. Ländernamen
      SortStructuredList(nation(), #PB_Sort_Descending, OffsetOf(Region\Prevalence), TypeOf(Region\Prevalence))
      SortStructuredList(nation(), #PB_Sort_Descending, OffsetOf(Region\Incidence),  TypeOf(Region\Incidence))
      SortStructuredList(nation(), #PB_Sort_Ascending,  OffsetOf(Region\Td),         TypeOf(Region\Td))
      PokeC(@head2$ + 2*80, '▲')
   ElseIf sort = #SortByIncidence
      ; Sortierprioritäten: 1. Inzidenz, 2. Verdoppelungszeit, 3. Prävalenz, 4. Ländernamen
      SortStructuredList(nation(), #PB_Sort_Descending, OffsetOf(Region\Prevalence), TypeOf(Region\Prevalence))
      SortStructuredList(nation(), #PB_Sort_Ascending,  OffsetOf(Region\Td),         TypeOf(Region\Td))
      SortStructuredList(nation(), #PB_Sort_Descending, OffsetOf(Region\Incidence),  TypeOf(Region\Incidence))
      PokeC(@head2$ + 2*120, '▼')
   Else
      ProcedureReturn 1          ; error
   EndIf

   Debug "Lage in " + ListSize(nation()) + " Ländern am " + FormatDate(#DateMaskOut$, *covid\Date(iCurDate))
   Debug ""
   Debug "                                        akut Erkrankte                              |      Neuerkrankte/Woche"
   Debug head2$
   Debug "----                      -------   -----------------------   -------------------   |   -------   -----------------------"

   ForEach nation()
      msg$ = LSet(nation()\Name$, 24) + RAlign(Str(nation()\Sick), 9) + Space(9)

      If nation()\Sick >= #MinCases
         msg$ + RAlign(StrD(nation()\Prevalence, 1), 8) + Space(13)
         If nation()\Td >= 10000.0
            msg$ + Space(7) + #NA$ + Space(11)
         Else
            msg$ + RAlign(StrD(nation()\Td,1), 8) + " Tage" + Space(8)
         EndIf
      Else
         msg$ + #None$ + Space(15) + #None$ + Space(11)
      EndIf

      msg$ + "|" + Space(2)

      If nation()\ErrDate
         msg$ + "inkonsistenter Wert von \Confirmed() am " +
                FormatDate(#DateMaskOut$, nation()\ErrDate)
      Else
         msg$ + RAlign(Str(nation()\New), 8) + Space(9)
         If nation()\Incidence < 10000.0
            msg$ + RAlign(StrD(nation()\Incidence,1), 8)
         Else
            msg$ + #None$
         EndIf
      EndIf

      Debug msg$
   Next

   ProcedureReturn 0        ; success
EndProcedure


Procedure.i CourseOfTime (*covid.CSV, country$)
   ; -- zeitliche Entwicklung der Fallzahlen, Prävalenzen, Verdoppelungszeiten,
   ;    Neuerkrankungen und Inzidenzen in einem ausgewählten Land
   ; in : *covid  : Länderdaten
   ;      country$: interessierendes Land,
   ;                "" um diesen Programmteil zu überspringen
   ; out: return value: 0 on success / 1 or 2 on error
   Protected td.d, incidence.d, msg$
   Protected.i i, error, new, iCurDate, iTarget, iTrend, checkTrend=#True
   Protected *pOut, *pMax, max.i=0
   Protected NewList out$()

   If Asc(country$) = ''
      ProcedureReturn 0         ; success
   EndIf

   If FindMapElement(*covid\Country(), country$) = #Null
      ProcedureReturn 1         ; error
   EndIf

   Debug "Zeitliche Entwicklung in: " + country$
   Debug ""
   Debug "                             akut Erkrankte                          |      Neuerkrankte/Woche"
   Debug "Datum ▲        absolut   pro 100 000 Einwohner   Verdoppelungszeit   |   absolut   pro 100 000 Einwohner"
   Debug "-------        -------   ---------------------   -----------------   |   -------   ---------------------"

   iCurDate = ArraySize(*covid\Date())

   For iTarget = 43 To iCurDate                 ; erst ab 1. März auswerten
      With *covid\Country()
         *pOut = AddElement(out$())
         out$() = FormatDate(#DateMaskOut$, *covid\Date(iTarget)) + RAlign(Str(\Sick(iTarget)), 12) + Space(8)

         If \Sick(iTarget) >= #MinCases
            If \Population > 0
               out$() + RAlign(StrD(\Sick(iTarget) * 100000 / \Population, 1), 8) + Space(10)
            Else
               out$() + #None$ + Space(12)
            EndIf

            td = DoublingTime(*covid\Date(), \Sick(), #CSV_IndexFirstDate, iTarget)
            If td >= 10000.0
               out$() + Space(7) + #NA$ + Space(11)
            Else
               out$() + RAlign(StrD(td,1), 8) + " Tage" + Space(8)
            EndIf
         Else
            out$() + #None$ + Space(13) + #None$ + Space(10)
         EndIf

         out$() + "|" + Space(2)

         If \Confirmed(iTarget) < \Confirmed(iTarget-1)
            out$() + "inkonsistenter Wert von \Confirmed()"
         Else
            error = #False
            For i = iTarget-6 To iTarget-1
               If \Confirmed(i) < \Confirmed(i-1)
                  error = #True
                  Break
               EndIf
            Next

            If error
               out$() + #None$ + Space(8) + #None$
            Else
               new = Round(\Confirmed(iTarget) - \Confirmed(iTarget-7), #PB_Round_Nearest)  ; Neuerkrankte pro Woche
               If max < new
                  max = new
                  *pMax = *pOut
               EndIf

               out$() + RAlign(Str (new), 8) + Space( 8)
               If new >= #MinCases And \Population > 0
                  incidence = new * 100000 / \Population
                  out$() + RAlign(StrD(incidence,1), 8)
               Else
                  out$() + #None$
               EndIf
            EndIf
         EndIf

         ; Die Sick()-Werte der letzten #TrendDays Tage prüfen
         iTrend = iTarget - iCurDate + #TrendDays - 1
         If iTrend >= 0
            If \Sick(iTarget) < #MinCases
               checkTrend = #False          ; Es kann kein Trend berechnet werden.
            EndIf
         EndIf
      EndWith
   Next

   ForEach out$()
      If @out$() = *pMax
         Debug out$() + "  max."
      Else
         Debug out$()
      EndIf
   Next

   ; -- Trend berechnen
   Debug ""
   msg$ = "Trend der letzten " + #TrendDays + " Tage: "

   If checkTrend = #False
      Debug msg$ + Space(4) + #None$
      ProcedureReturn 0                    ; success
   EndIf

   msg$ + "Die Anzahl akut Erkrankter "
   Select CalcTrend(*covid\Country()\Sick(), iCurDate, #TrendDays, Bool(Asc(#R_Script$) <> ''))
      Case 0
         Debug msg$ + "bleibt ungefähr gleich."
      Case 1
         Debug msg$ + "bleibt gleich."
      Case -2
         Debug msg$ + "nimmt ab."
      Case -4
         Debug msg$ + "nimmt zunehmend langsamer ab."
      Case -6
         Debug msg$ + "nimmt zunehmend schneller ab."
      Case 2
         Debug msg$ + "nimmt zu."
      Case 4
         Debug msg$ + "nimmt zunehmend langsamer zu."
      Case 6
         Debug msg$ + "nimmt zunehmend schneller zu."
      Default
         ProcedureReturn 2   ; error
   EndSelect

   ProcedureReturn 0         ; success
EndProcedure


Procedure.i ExportPrevalences (*covid.CSV, countries$, csvFile$, fieldSep$=";", decimalSep$=",")
   ; -- CSV-Datei mit dem Verlauf der Prävalenzen in den angegebenen Ländern erzeugen
   ;    (Erkrankte pro 100 000 Einwohner)
   ; in : *covid     : Länderdaten
   ;      countries$ : Liste der interessierenden Länder (durch ';' getrennt),
   ;                   "" um keine CSV-Datei zu schreiben
   ;      csvFile$   : Name der zu erzeugenden CSV-Datei,
   ;                   "" um keine CSV-Datei zu schreiben
   ;      fieldSep$  : Feldtrennzeichen
   ;      decimalSep$: Dezimaltrennzeichen (Voreinstellung für LibreOffice Calc)
   ; out: return value: 0 on success / 1 or 2 on error
   Protected line$, country$, warning$=""
   Protected.i ofn, i, d, numCountries, ret=0
   Protected.i iCurDate = ArraySize(*covid\Date())

   If Asc(countries$) = '' Or Asc(csvFile$) = ''
      ProcedureReturn 0                  ; success
   EndIf

   ofn = CreateFile(#PB_Any, csvFile$)
   If ofn = 0
      ProcedureReturn 1                  ; error
   EndIf

   line$ = "Country"
   For d = 43 To iCurDate                ; erst ab 1. März auswerten
      line$ + fieldSep$ + FormatDate(#DateMaskOut$, *covid\Date(d))
   Next
   WriteStringN(ofn, line$)

   numCountries = CountString(countries$, ";") + 1

   For i = 1 To numCountries
      country$ = StringField(countries$, i, ";")
      With *covid\Country()
         If FindMapElement(*covid\Country(), country$) > 0 And \Population > 0
            line$ = country$
            For d = 43 To iCurDate       ; erst ab 1. März auswerten
               If \Sick(d) >= #MinCases
                  line$ + fieldSep$ + FormatNumber(\Sick(d) * 100000 / \Population, 1, decimalSep$)
               Else
                  line$ + fieldSep$ + "0"
               EndIf
               If \Confirmed(d) < \Confirmed(d-1)
                  warning$ + ";" + country$
               EndIf
            Next
            WriteStringN(ofn, line$)
         Else
            ret = 2      ; error
         EndIf
      EndWith
   Next
   CloseFile(ofn)

   Debug "Die zeitliche Entwicklung der Prävalenzen in den Ländern" + #LF$ +
         Space(3) + countries$ + #LF$ +
         "wurde in die Datei '" + csvFile$ + "' geschrieben."

   If Asc(warning$) <> ''
      Debug ""
      Debug "Achtung: Die 'confirmed'-Daten der Johns Hopkins Universität von" + #LF$ +
            Space(3) + Mid(warning$, 2) + #LF$ +
            "enthalten inkonsistente Werte."
   EndIf

   ProcedureReturn ret
EndProcedure


; =======  P R O G R A M M S T A R T  =======

#Title$ = "Dynamics of Covid-19 propagation"
Define download.i, sortCurrentCountries.i, currentCountries$, ctCountry$, exportCountries$, exportFile$, covid.CSV

;-------------------------------------
;- * EINSTELLUNGEN *

download = #Auto

; currentCountries$ = ""  ; means *all* countries
currentCountries$ = "Singapore;US;United Kingdom;Sweden;Italy;Norway;EU;Russia;Switzerland;Germany;Austria"

; sortCurrentCountries = #SortByPrevalence
; sortCurrentCountries = #SortByDoublingTime
sortCurrentCountries = #SortByIncidence

; -- Zeitliche Entwicklung zeigen für:
; ctCountry$ = "Austria"
; ctCountry$ = "EU"
; ctCountry$ = "France"
ctCountry$ = "Germany"
; ctCountry$ = "Italy"
; ctCountry$ = "Japan"
; ctCountry$ = "Korea, South"
; ctCountry$ = "New Zealand"
; ctCountry$ = "Norway"
; ctCountry$ = "Russia"
; ctCountry$ = "Singapore"
; ctCountry$ = "Spain"
; ctCountry$ = "Sweden"
; ctCountry$ = "Switzerland"
; ctCountry$ = "United Kingdom"
; ctCountry$ = "US"
; ctCountry$ = ""

exportCountries$ = "Singapore;US;United Kingdom;Sweden;Italy;Norway;EU;Russia;Switzerland;Germany;Austria"
exportFile$ = "Covid-19 prevalence.csv"
;-------------------------------------

; -- Vorbereitungen
Select GetCSVfiles(download)
   Case 1
      MessageRequester(#Title$, "Error reading most recent date in file '" +
                                s_FileConfirmed$ + "'.", #PB_MessageRequester_Error)
      End
   Case 2
      MessageRequester(#Title$, "Error downloading CSV files.", #PB_MessageRequester_Error)
      End
EndSelect

Select AddFirstCSV(s_FileConfirmed$, @covid)
   Case 1
      MessageRequester(#Title$, "Error reading CSV file '" + s_FileConfirmed$ + "'.", #PB_MessageRequester_Error)
      End
   Case 2, 3, 5
      MessageRequester(#Title$, "Invalid CSV format in file '" + s_FileConfirmed$ + "'.", #PB_MessageRequester_Error)
      End
   Case 4
      MessageRequester(#Title$, "No continuous dates in CSV file '" + s_FileConfirmed$ + "'.", #PB_MessageRequester_Error)
      End
EndSelect

Select SubNextCSV(s_FileRecovered$, @covid)
   Case 1
      MessageRequester(#Title$, "Error reading CSV file '" + s_FileRecovered$ + "'.", #PB_MessageRequester_Error)
      End
   Case 2, 5
      MessageRequester(#Title$, "Invalid CSV format in file '" + s_FileRecovered$ + "'.", #PB_MessageRequester_Error)
      End
   Case 3, 4
      MessageRequester(#Title$, "Headers of files '" + s_FileConfirmed$ + "' and '" +
                                s_FileRecovered$ + "' are different.", #PB_MessageRequester_Error)
      End
   Case 6
      MessageRequester(#Title$, "Country '"+ MapKey(covid\Country()) + "' is contained in file '" + s_FileRecovered$ +
                                "' but not in file '" + s_FileConfirmed$ + "'.", #PB_MessageRequester_Error)
      End
EndSelect

Select SubNextCSV(s_FileDeaths$, @covid)
   Case 1
      MessageRequester(#Title$, "Error reading CSV file '" + s_FileDeaths$ + "'.", #PB_MessageRequester_Error)
      End
   Case 2, 5
      MessageRequester(#Title$, "Invalid CSV format in file '" + s_FileDeaths$ + "'.", #PB_MessageRequester_Error)
      End
   Case 3, 4
      MessageRequester(#Title$, "Headers of files '" + s_FileConfirmed$ + "' and '" +
                                s_FileDeaths$ + "' are different.", #PB_MessageRequester_Error)
      End
   Case 6
      MessageRequester(#Title$, "Country '"+ MapKey(covid\Country()) + "' is contained in file '" + s_FileDeaths$ +
                                "' but not in file '" + s_FileConfirmed$ + "'.", #PB_MessageRequester_Error)
      End
EndSelect

If InitCountries(@covid) = 1
   MessageRequester(#Title$, "Unknown country '" + MapKey(covid\Country()) +
                             "' in procedure InitCountries().", #PB_MessageRequester_Error)
   End
EndIf


; -- 1) aktuelle Lage in ausgewählten oder allen Ländern zeigen
If CurrentData(@covid, sortCurrentCountries, currentCountries$) = 1
   MessageRequester(#Title$, "Invalid sort mode " + sortCurrentCountries + ".", #PB_MessageRequester_Error)
   End
EndIf


Debug ""
Debug RepeatChar(121, "=")
Debug ""


; -- 2) zeitliche Entwicklung in einem Land zeigen
Select CourseOfTime(@covid, ctCountry$)
   Case 1
      MessageRequester(#Title$, "Country '" + ctCountry$ + "' not found.", #PB_MessageRequester_Error)
      End
   Case 2
      MessageRequester(#Title$, "Error when calculating trend for country '" + ctCountry$ + "'.", #PB_MessageRequester_Error)
      End
EndSelect


Debug ""
Debug RepeatChar(121, "=")
Debug ""


; -- 3) Prävalenzen ausgewählter Länder in CSV-Datei schreiben
Select ExportPrevalences(@covid, exportCountries$, exportFile$)
   Case 1
      MessageRequester(#Title$, "Can't create CSV file '" + exportFile$ + "'.", #PB_MessageRequester_Error)
      End
   Case 2
      MessageRequester(#Title$, "The following list contains one or more unknown countries:" + #LF$ +
                                Space(3) + exportCountries$, #PB_MessageRequester_Error)
      End
EndSelect
Bild
Zuletzt geändert von Nino am 17.05.2020 12:41, insgesamt 15-mal geändert.
Benutzeravatar
dige
Beiträge: 1183
Registriert: 08.09.2004 08:53

Re: Dynamik der Covid-19-Ausbreitung

Beitrag von dige »

Danke :allright:
"Papa, mein Wecker funktioniert nicht! Der weckert immer zu früh."
Benutzeravatar
juergenkulow
Beiträge: 188
Registriert: 22.12.2016 12:49
Wohnort: :D_üsseldorf-Wersten

Re: Dynamik der Covid-19-Ausbreitung

Beitrag von juergenkulow »

Hallo Nino,

wenn ich eine Anfrage mit country$="US" mache, bekomme ich nur die Daten der Province/State Washington,US,47.4009,-121.4905,0,0,... ,0,267,366,442,568,572,643,904,1076,1014,1376,1524,1793,1996 .
Ähnliches passiert bei den Ländern China mit Hubei, Canada mit British Columbia, Australia mit New South Wales, United Kingdom, Australia, Demark, Netherlands.

Weiter habe ich den Eindruck das bei vielen US-Daten, wie New York County, NY 13 führende Nullen fehlen und so die Daten um 13 Tage verschoben sind.
Nino
Beiträge: 1300
Registriert: 13.05.2010 09:26
Wohnort: Berlin

Re: Dynamik der Covid-19-Ausbreitung

Beitrag von Nino »

Hallo!
ReadCSV() hat geschrieben:; Anm.: Diese Funktion ist einfach gehalten und funktioniert nur richtig
; für Datensätze, die keine in Anführungszeichen eingeschlossenen
; Feld-Trennzeichen enthalten.
:-)
Daher und weil bei manchen Ländern (z.B. China, US) die Anzahl der Fälle auf mehrere Datensätze ("Provinzen") verteilt ist, funktionierte der Code nicht mit allen Ländern.
Es kam mir zunächst v.a. auf die Verdoppelungszeit bei uns in Deutschland an.

Beide Probleme sind jetzt in der neuen Version behoben.
Nino
Beiträge: 1300
Registriert: 13.05.2010 09:26
Wohnort: Berlin

Re: Dynamik der Covid-19-Ausbreitung

Beitrag von Nino »

Neue Version:
  • Darstellung der aktuellen Verdoppelungszeiten für alle Länder (sofern berechenbar) hinzugefügt
  • einige interne Verbesserungen
Nino
Beiträge: 1300
Registriert: 13.05.2010 09:26
Wohnort: Berlin

Re: Dynamik der Covid-19-Ausbreitung

Beitrag von Nino »

Neue Version:
  • Da die bisherige Datenquelle nicht mehr aktualisiert wird, verwendet das Programm jetzt eine andere (ebenfalls von der Johns-Hopkins-Universität).
  • Das Programm läuft jetzt deutlich schneller als zuvor.
Nino
Beiträge: 1300
Registriert: 13.05.2010 09:26
Wohnort: Berlin

Re: Dynamik der Covid-19-Ausbreitung

Beitrag von Nino »

Neue Version:
  • Wenn auf dem PC das freie Statistikprogramm R vorhanden ist, kann im zweiten Teil des Programms für die Verdoppelungszeit im gewählten Land jetzt auch ein aktueller Trend berechnet werden.
  • Wenn die Anzahl der bestätigten Krankheitsfälle in einem Land kleiner als 20 ist, werden keine Berechnungen durchgeführt, da mir diese zu ungenau wären.
  • Diverse kleinere Änderungen.
Benutzeravatar
juergenkulow
Beiträge: 188
Registriert: 22.12.2016 12:49
Wohnort: :D_üsseldorf-Wersten

Re: Dynamik der Covid-19-Ausbreitung

Beitrag von juergenkulow »

Hallo Nino,

vielen Dank für Dein Programm. Ich habe die url$ auf
"https://github.com/CSSEGISandData/COVID ... global.csv"
gesetzt und eine weltweite Prognose für 7 und 14 Tage laufen lassen.
Nino
Beiträge: 1300
Registriert: 13.05.2010 09:26
Wohnort: Berlin

Re: Dynamik der Covid-19-Ausbreitung

Beitrag von Nino »

juergenkulow hat geschrieben:... und eine weltweite Prognose für 7 und 14 Tage laufen lassen.
Prognosen sind mit meinem Code nicht möglich.

Allgemein halte ich es, was Prognosen betrifft, mit Niels Bohr:
Vorhersagen sind schwierig, besonders über die Zukunft.
Benutzeravatar
juergenkulow
Beiträge: 188
Registriert: 22.12.2016 12:49
Wohnort: :D_üsseldorf-Wersten

Re: Dynamik der Covid-19-Ausbreitung

Beitrag von juergenkulow »

Nino,
Du hast vollkommen recht. Vorhersagen der Zukunft sind unsicher, schon bei der 14 tägigen Prognose können kleine Änderungen Auswirkungen in den Hunderttausenden haben.
Mir ist es wichtig die Gründe der Entscheidungsfindung zu verstehen und dabei hat mir Dein Programm weiter geholfen.
Sollte der Wunsch nach Veröffentlichung der von mir vorgenommenen kleinen Programmänderungen bestehen, bitte ich um kurze Nachricht.

@alle: Bitte stecke keinen an. Das Virus ist eigenartig bösartig.
Antworten