home · contact · privacy
f8add4efe2bcfaca4989785621323042b0f22982
[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 # Define and move sum_district from end to start.
99 sum_district = sorted_districts.pop()
100 sorted_districts.insert(0, sum_district)
101
102 # In LaGeSo's data, the last "district" is actually the sum of all districts /
103 # the whole of Berlin.
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 if output_type == 'html':
138     print("""<!DOCTYPE html>
139 <html>
140 <head>
141 <style>
142 th { text-align: left; vertical-align: bottom; }
143 .day_row:nth-child(7n+3) > td { border-top: 1px solid black; }
144 .vertical_header { writing-mode: vertical-rl; transform: rotate(180deg); font-weight: normal; }
145 .fixed_head { position: sticky; top: 0; background-color: white; }
146 .bold { font-weight: bold }
147 </style>
148 <title>Berlin's Corona infection numbers, development by districts</title>
149 </head>
150 <a href="/">home</a> · <a href="/contact.html">contact</a> · <a href="/privacy.html">privacy</a>
151 <h1>Berlin's Corona infection numbers, development by districts</h1>
152 <p>Updated daily at 9pm based on data from the "Senatsverwaltung für Gesundheit, Pflege und Gleichstellung". <a href="https://plomlompom.com/repos/?p=berlin-corona-table">Source code</a>. <a href="berlin_corona.txt">Text view optimized for terminal curl</a>.</p>
153 <table>
154 <tr>
155 <th colspan=2></th>""")
156     sorted_dates.reverse()
157     for district in sorted_districts:
158         # Wrap in div because the vertical orientation otherwise fails
159         # in Chromium.
160         print('<th><div class="vertical_header">%s</div></th>' %
161               translate[district])
162     print('</tr>')
163     print('<tr class="fixed_head">')
164     # In Chromium, the th only stay fixed if also given this class.
165     print('<th class="fixed_head">date</th>')
166     print('<th class="fixed_head"><a href="#symbols">?</a></th>')
167     for district in sorted_districts:
168         print('<th class="fixed_head"><abbr title="%s">%s</abbr></th>' %
169               (translate[district], district))
170     print('</tr>')
171     for date in sorted_dates:
172         print('<tr class="day_row">')
173         print('<td>%s</td>' % date)
174         print('<td><table>')
175         for abbr in ['+', 'Σ', 'Ø', 'i']:
176             print('<tr><th><abbr title="%s">%s</abbr></th></tr>' %
177                   (translate[abbr], abbr))
178         print('</table></td>')
179         for district in sorted_districts:
180             district_data = db[district][date]
181             week_sum = week_avg = week_inc = '?'
182             new_infections = district_data['new_infections']
183             if 'week_sum' in district_data:
184                 week_sum = '%s' % district_data['week_sum']
185             if 'week_average' in district_data:
186                 week_avg = '%.1f' % district_data['week_average']
187             if 'week_incidence' in district_data:
188                 week_inc = '%.1f' % district_data['week_incidence']
189             print('<td>')
190             print('<table>')
191             print('<tr><td class="bold">%s</td></tr>' % new_infections)
192             print('<tr><td>%s</td></tr>' % week_sum)
193             print('<tr><td>%s</td></tr>' % week_avg)
194             print('<tr><td>%s</td></tr>' % week_inc)
195             print('</table>')
196             print('</td>')
197         print('</tr>')
198     print('</table>')
199     print('<h3 id="symbols">Symbols</h3>')
200     print('<dl>')
201     for abbr in ['+', 'Σ', 'Ø', 'i']:
202         print('<dt>%s</dt><dd>%s</dd>' % (abbr, translate[abbr]))
203     print('</dl>')
204     print('</html>')
205
206 # Optimized for in-terminal curl.
207 elif output_type == 'txt':
208
209     # Explain what this is.
210     intro = \
211 """Table of Berlin's Corona infection number development by districts.
212 Updated daily at 9pm based on data from the "Senatsverwaltung für Gesundheit, Pflege und Gleichstellung".
213
214 Abbrevations/explanations:
215 """
216     for k in translate:
217         intro += "%s: %s\n" % (k, translate[k])
218     intro += """
219 Source code: https://plomlompom.com/repos/?p=berlin-corona-table
220 HTML view: https://plomlompom.com/berlin_corona.html"""
221     print(intro)
222
223     # Output table of enhanced daily infection data, newest on top,
224     # separated into 7-day units.
225     sorted_dates.reverse()
226     weekday_count = 0
227     for date in sorted_dates:
228
229         # Week table header.
230         if weekday_count == 0:
231             print()
232             print(' '*13, '   '.join(sorted_districts))
233             print('-'*77)
234
235         # Day table.
236         print(date)
237         new_infections = []
238         weekly_sums = []
239         weekly_avgs = []
240         weekly_incs = []
241         for district in sorted_districts:
242             new_infections += [db[district][date]['new_infections']]
243             weekly_sums += [db[district][date]['week_sum']]
244             weekly_avgs += [db[district][date]['week_average']]
245             weekly_incs += [db[district][date]['week_incidence']]
246         print('+', ' '*11, '  '.join(['%3s' % i for i in new_infections]))
247         print('Σ', ' '*10, ' '.join(['%4s' % wsum for wsum in weekly_sums]))
248         print('Ø', ' '*9, ''.join(['%5.1f' % wavg for wavg in weekly_avgs]))
249         print('i', ' '*9, ''.join(['%5.1f' % winc for winc in weekly_incs]))
250         weekday_count += 1
251         if weekday_count != 7:
252             continue
253         weekday_count = 0