home · contact · privacy
Declare inactive, link to new Senatsverwaltung dashboard.
[berlin-corona-table] / enhance_table.py
1 #!//usr/bin/env python3
2
3 # District population numbers as per Wikipedia.
4 district_pops = {
5   'CW': 342332,
6   'FK': 289762,
7   'Li': 291452,
8   'MH': 268548,
9   'Mi': 384172,
10   'Ne': 329691,
11   'Pa': 407765,
12   'Re': 265225,
13   'Sp': 243977,
14   'SZ': 308697,
15   'TS': 351644,
16   'TK': 271153,
17   'sum': 3754418,
18 }
19
20 # Map abbreviations to full names.
21 translate = {
22   'CW': 'Charlottenburg-Wilmersdorf',
23   'FK': 'Friedrichshain-Kreuzberg',
24   'Li': 'Lichtenberg',
25   'MH': 'Marzahn-Hellersdorf',
26   'Mi': 'Mitte',
27   'Ne': 'Neukölln',
28   'Pa': 'Pankow',
29   'Re': 'Reinickendorf',
30   'Sp': 'Spandau',
31   'SZ': 'Steglitz-Zehlendorf',
32   'TS': 'Tempelhof-Schöneberg',
33   'TK': 'Treptow-Köpenick',
34   'sum': 'all of Berlin',
35   '+': 'new infections counted that day',
36   'Σ': 'sum of new infections for last 7 days',
37   'Ø': 'per-day average of new infections for last 7 days',
38   'i': 'incidence (x per 100k inhabitants) of new infections for last 7 days',
39 }
40
41 # Read infections table path and output type.
42 import sys
43 if len(sys.argv) != 3:
44     print('Expecting infections table file path and output type as only arguments.')
45     exit(1)
46 infections_table = sys.argv[1]
47 output_type = sys.argv[2]
48
49 # Read infections table file lines.
50 f = open(infections_table, 'r')
51 lines = f.readlines()
52 f.close()
53
54 # Basic input validation.
55 import datetime
56 header_elements = lines[0].split()
57 if set(header_elements) != district_pops.keys() or \
58        len(header_elements) != len(district_pops.keys()):
59     raise Exception('infections table: invalid header')
60 line_count = 0
61 for line in lines[1:]:
62     line_count += 1
63     fields = line.split()
64     if len(header_elements) != len(fields) - 1:
65         raise Exception('infections table: too many elements on line %s',
66                         line_count)
67     try:
68         datetime.date.fromisoformat(fields[0])
69     except ValueError:
70         raise Exception('infections table: bad ISO date on line %s',
71                         line_count)
72     for field in fields[1:]:
73         try:
74             int(field)
75         except ValueError:
76             raise Exception('infections table: bad value on line %s',
77                             line_count)
78
79 # Parse first table file line for the names and order of districts.
80 db = {}
81 sorted_districts = []
82 for header in lines[0].split():
83     sorted_districts += [header]
84     db[header] = {}
85
86 # Seed DB with daily new infections data per district, per date.
87 sorted_dates = []
88 for line in lines[1:]:
89     fields = line.split()
90     date = fields[0]
91     sorted_dates += [date]
92     for i in range(len(sorted_districts)):
93         district = sorted_districts[i]
94         district_data = fields[i + 1]
95         db[district][date] = {'new_infections': int(district_data)}
96 sorted_dates.sort()
97
98 # In LaGeSo's data, the last "district" is actually the sum of all districts /
99 # the whole of Berlin.  For our district order, move it in front of the other
100 # districts, as its numbers are the most interesting, so in the table views
101 # we want to see it first.
102 sum_district = sorted_districts.pop()
103 sorted_districts.insert(0, sum_district)
104
105 # Fail on any day where the "sum" district's new infections are not the proper
106 # sum of the individual districts new infections.  Yes, sometimes Lageso sends
107 # data that is troubled in this way.  It will then have to be fixed manually in
108 # the table file, since we should have a human look at what mistake was
109 # probably made.
110 for date in sorted_dates:
111     day_sum = 0
112     for district in [d for d in sorted_districts if not d==sum_district]:
113         day_sum += db[district][date]['new_infections']
114     if day_sum != db[sum_district][date]['new_infections']:
115         raise Exception('Questionable district infection sum in %s' % date)
116
117 # Enhance DB with data about weekly sums, averages, incidences per day.  Ignore
118 # days that have less than 6 predecessors (we can only know a weekly average if
119 # we have a whole week of data).
120 for i in range(len(sorted_dates)):
121     if i < 6:
122         continue
123     date = sorted_dates[i]
124     week_dates = []
125     for j in range(7):
126         week_dates += [sorted_dates[i - j]]
127     for district in sorted_districts:
128         district_pop = district_pops[district]
129         week_sum = 0
130         for week_date in week_dates:
131             week_sum += db[district][week_date]['new_infections']
132         db[district][date]['week_sum'] = week_sum
133         db[district][date]['week_average'] = week_sum / 7
134         db[district][date]['week_incidence'] = (week_sum / district_pop) * 100000
135
136 # Optimized for web browser viewing.
137 import calendar
138 if output_type == 'html':
139     print("""<!DOCTYPE html>
140 <html>
141 <head>
142 <style>
143 th { text-align: left; vertical-align: bottom; }
144 .vertical_header { writing-mode: vertical-rl; transform: rotate(180deg); font-weight: normal; }
145 .repeated_head th { padding-top: 0.5em; border-bottom: 1px solid black; }
146 .bold { font-weight: bold }
147 .date { vertical-align: top; padding-top: 0.5em; }
148 </style>
149 <title>Berlin's Corona infection numbers, development by districts</title>
150 </head>
151 <a href="/">home</a> · <a href="/contact.html">contact</a> · <a href="/privacy.html">privacy</a>
152 <h1>Berlin's Corona infection numbers, development by districts</h1>
153 <p><del>Updated daily at 7pm based on data from the "Senatsverwaltung für Gesundheit, Pflege und Gleichstellung".</del> Updates currently inactive – check out <a href="https://www.berlin.de/corona/lagebericht/desktop/corona.html#bezirke">the new dashboard</a> instead that they offer by themselves.</p>
154 <p><a href="https://plomlompom.com/repos/?p=berlin-corona-table">Source code</a>. <a href="berlin_corona.txt">Plain text view (optimized for terminal curl)</a>.</p>
155 <table>
156 <tr>
157 <th colspan=2></th>""")
158     sorted_dates.reverse()
159     for district in sorted_districts:
160         # Wrap in div because the vertical orientation otherwise fails
161         # in Chromium.
162         print('<th><div class="vertical_header">%s</div></th>' %
163               translate[district])
164     print('</tr>')
165     weekday_count = 0
166     for date in sorted_dates:
167         if weekday_count == 0:
168             print('<tr class="repeated_head">')
169             print('<th>date</th>')
170             print('<th><a href="#symbols">?</a></th>')
171             for district in sorted_districts:
172                 print('<th><abbr title="%s">%s</abbr></th>' %
173                       (translate[district], district))
174             print('</tr>')
175         print('<tr class="day_row">')
176         weekday = calendar.day_name[datetime.date.fromisoformat(date).weekday()]
177         print('<td class="date">%s<br />%s</td>' % (date, weekday))
178         print('<td><table>')
179         for abbr in ['+', 'Σ', 'Ø', 'i']:
180             print('<tr><th><abbr title="%s">%s</abbr></th></tr>' %
181                   (translate[abbr], abbr))
182         print('</table></td>')
183         for district in sorted_districts:
184             district_data = db[district][date]
185             week_sum = week_avg = week_inc = '?'
186             new_infections = district_data['new_infections']
187             if 'week_sum' in district_data:
188                 week_sum = '%s' % district_data['week_sum']
189             if 'week_average' in district_data:
190                 week_avg = '%.1f' % district_data['week_average']
191             if 'week_incidence' in district_data:
192                 week_inc = '%.1f' % district_data['week_incidence']
193             print('<td>')
194             print('<table>')
195             print('<tr><td class="bold">%s</td></tr>' % new_infections)
196             print('<tr><td>%s</td></tr>' % week_sum)
197             print('<tr><td>%s</td></tr>' % week_avg)
198             print('<tr><td>%s</td></tr>' % week_inc)
199             print('</table>')
200             print('</td>')
201         print('</tr>')
202         weekday_count += 1
203         if weekday_count != 7:
204             continue
205         weekday_count = 0
206     print('</table>')
207     print('<h3 id="symbols">Symbols</h3>')
208     print('<dl>')
209     for abbr in ['+', 'Σ', 'Ø', 'i']:
210         print('<dt>%s</dt><dd>%s</dd>' % (abbr, translate[abbr]))
211     print('</dl>')
212     print('</html>')
213
214 # Optimized for in-terminal curl.
215 elif output_type == 'txt':
216
217     # Explain what this is.
218     intro = \
219 """Table of Berlin's Corona infection number development by districts.
220 NO LONGER Updated daily at 7pm based on data from the "Senatsverwaltung für Gesundheit, Pflege und Gleichstellung".
221 Currently inactive. Instead check out the new dashboard they offer: https://www.berlin.de/corona/lagebericht/desktop/corona.html#bezirke
222
223 Abbrevations/explanations:
224 """
225     for k in translate:
226         intro += "%s: %s\n" % (k, translate[k])
227     intro += """
228 Source code: https://plomlompom.com/repos/?p=berlin-corona-table
229 HTML view: https://plomlompom.com/berlin_corona.html"""
230     print(intro)
231
232     # Output table of enhanced daily infection data, newest on top,
233     # separated into 7-day units.
234     sorted_dates.reverse()
235     weekday_count = 0
236     for date in sorted_dates:
237
238         # Week table header.
239         if weekday_count == 0:
240             print()
241             print(' '*13, '   '.join(sorted_districts))
242             print('-'*77)
243
244         # Day table.
245         weekday = calendar.day_name[datetime.date.fromisoformat(date).weekday()]
246         print('%s (%s)' % (date, weekday))
247         new_infections = []
248         weekly_sum_strings = []
249         weekly_avg_strings = []
250         weekly_inc_strings = []
251         for district in sorted_districts:
252             district_day_data = db[district][date]
253             new_infections += [district_day_data['new_infections']]
254             wsum_string = ' '*3 + '?'
255             wavg_string = winc_string = ' '*4 + '?'
256             if 'week_sum' in district_day_data:
257                 wsum_string = '%4s' % district_day_data['week_sum']
258             weekly_sum_strings += [wsum_string]
259             if 'week_average' in district_day_data:
260                 wavg_string = '%5.1f' % district_day_data['week_average']
261             weekly_avg_strings += [wavg_string]
262             if 'week_incidence' in district_day_data:
263                 winc_string = '%5.1f' % district_day_data['week_incidence']
264             weekly_inc_strings += [winc_string]
265         print('+', ' '*11, '  '.join(['%3s' % i for i in new_infections]))
266         print('Σ', ' '*10, ' '.join(weekly_sum_strings))
267         print('Ø', ' '*9, ''.join(weekly_avg_strings))
268         print('i', ' '*9, ''.join(weekly_inc_strings))
269         weekday_count += 1
270         if weekday_count != 7:
271             continue
272         weekday_count = 0