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',
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 # Define and move sum_district from end to start.
99 sum_district = sorted_districts.pop()
100 sorted_districts.insert(0, sum_district)
102 # In LaGeSo's data, the last "district" is actually the sum of all districts /
103 # the whole of Berlin.
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.
137 if output_type == 'html':
138 print("""<!DOCTYPE html>
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 }
148 <title>Berlin's Corona infection numbers, development by districts</title>
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>
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
160 print('<th><div class="vertical_header">%s</div></th>' %
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))
171 for date in sorted_dates:
172 print('<tr class="day_row">')
173 print('<td>%s</td>' % date)
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']
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)
199 print('<h3 id="symbols">Symbols</h3>')
201 for abbr in ['+', 'Σ', 'Ø', 'i']:
202 print('<dt>%s</dt><dd>%s</dd>' % (abbr, translate[abbr]))
206 # Optimized for in-terminal curl.
207 elif output_type == 'txt':
209 # Explain what this is.
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".
214 Abbrevations/explanations:
217 if k == '+': # not used in terminal version
219 intro += "%s: %s\n" % (k, translate[k])
221 Source code: https://plomlompom.com/repos/?p=berlin-corona-table
222 HTML view: https://plomlompom.com/berlin_corona.html
226 # Output table of enhanced daily infection data, newest on top,
227 # separated into 7-day units.
228 sorted_dates.reverse()
230 for date in sorted_dates:
233 if weekday_count == 0:
234 print(' '*10, ' '.join(sorted_districts),
236 week_start_date = date
240 for district in sorted_districts:
241 new_infections += [db[district][date]['new_infections']]
242 week_sum = week_avg = week_inc = ''
243 sum_district_data = db[sum_district][date]
244 if 'week_sum' in sum_district_data:
245 week_sum = '%4s' % sum_district_data['week_sum']
246 if 'week_average' in sum_district_data:
247 week_avg = '%5.1f' % sum_district_data['week_average']
248 if 'week_incidence' in sum_district_data:
249 week_inc = '%4.1f' % sum_district_data['week_incidence']
250 print(date, ' '.join(['%3s' % infections
251 for infections in new_infections]),
252 week_sum, week_avg, week_inc)
254 # Maintain 7-day cycle.
256 if weekday_count != 7:
260 # After each 7 days, print summary for individual districts.
264 for district in sorted_districts[1:]:
265 weekly_sums += [db[district][week_start_date]['week_sum']]
266 weekly_avgs += [db[district][week_start_date]['week_average']]
267 weekly_incs += [db[district][week_start_date]['week_incidence']]
269 print('district stats for week from %s to %s:' % (date, week_start_date))
270 print(' '*4, ' '.join(sorted_districts[1:]))
271 print('Σ', ' '.join(['%5.1f' % wsum for wsum in weekly_sums]))
272 print('Ø', ' '.join(['%5.1f' % wavg for wavg in weekly_avgs]))
273 print('i', ' '.join(['%5.1f' % winc for winc in weekly_incs]))