1 #!//usr/bin/env python3
3 # District population numbers as per Wikipedia.
20 # Map abbreviations to full names.
22 'CW': 'Charlottenburg-Wilmersdorf',
23 'FK': 'Friedrichshain-Kreuzberg',
25 'MH': 'Marzahn-Hellersdorf',
29 'Re': 'Reinickendorf',
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',
41 # Read infections table path and output type.
43 if len(sys.argv) != 3:
44 print('Expecting infections table file path and output type as only arguments.')
46 infections_table = sys.argv[1]
47 output_type = sys.argv[2]
49 # Read infections table file lines.
50 f = open(infections_table, 'r')
54 # Basic input validation.
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')
61 for line in lines[1:]:
64 if len(header_elements) != len(fields) - 1:
65 raise Exception('infections table: too many elements on line %s',
68 datetime.date.fromisoformat(fields[0])
70 raise Exception('infections table: bad ISO date on line %s',
72 for field in fields[1:]:
76 raise Exception('infections table: bad value on line %s',
79 # Parse first table file line for the names and order of districts.
82 for header in lines[0].split():
83 sorted_districts += [header]
86 # Seed DB with daily new infections data per district, per date.
88 for line in lines[1:]:
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)}
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)
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
110 for date in sorted_dates:
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)
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)):
123 date = sorted_dates[i]
126 week_dates += [sorted_dates[i - j]]
127 for district in sorted_districts:
128 district_pop = district_pops[district]
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
136 # Optimized for web browser viewing.
138 if output_type == 'html':
139 print("""<!DOCTYPE html>
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; }
149 <title>Berlin's Corona infection numbers, development by districts</title>
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>
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
162 print('<th><div class="vertical_header">%s</div></th>' %
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))
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))
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']
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)
203 if weekday_count != 7:
207 print('<h3 id="symbols">Symbols</h3>')
209 for abbr in ['+', 'Σ', 'Ø', 'i']:
210 print('<dt>%s</dt><dd>%s</dd>' % (abbr, translate[abbr]))
214 # Optimized for in-terminal curl.
215 elif output_type == 'txt':
217 # Explain what this is.
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
223 Abbrevations/explanations:
226 intro += "%s: %s\n" % (k, translate[k])
228 Source code: https://plomlompom.com/repos/?p=berlin-corona-table
229 HTML view: https://plomlompom.com/berlin_corona.html"""
232 # Output table of enhanced daily infection data, newest on top,
233 # separated into 7-day units.
234 sorted_dates.reverse()
236 for date in sorted_dates:
239 if weekday_count == 0:
241 print(' '*13, ' '.join(sorted_districts))
245 weekday = calendar.day_name[datetime.date.fromisoformat(date).weekday()]
246 print('%s (%s)' % (date, weekday))
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))
270 if weekday_count != 7: