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 'wsum': 'sum of new infections for last 7 days',
36 'wavg': 'per-day average of new infections for last 7 days',
37 'winc': 'incidence (x per 100k inhabitants) of new infections for last 7 days',
40 # Read infections table path and output type.
42 if len(sys.argv) != 3:
43 print('Expecting infections table file path and output type as only arguments.')
45 infections_table = sys.argv[1]
46 output_type = sys.argv[2]
48 # Read infections table file lines.
49 f = open(infections_table, 'r')
53 # Basic input validation.
55 header_elements = lines[0].split()
56 if set(header_elements) != district_pops.keys() or \
57 len(header_elements) != len(district_pops.keys()):
58 raise Exception('infections table: invalid header')
60 for line in lines[1:]:
63 if len(header_elements) != len(fields) - 1:
64 raise Exception('infections table: too many elements on line %s',
67 datetime.date.fromisoformat(fields[0])
69 raise Exception('infections table: bad ISO date on line %s',
71 for field in fields[1:]:
75 raise Exception('infections table: bad value on line %s',
78 # Parse first table file line for the names and order of districts.
81 for header in lines[0].split():
82 sorted_districts += [header]
85 # Seed DB with daily new infections data per district, per date.
87 for line in lines[1:]:
90 sorted_dates += [date]
91 for i in range(len(sorted_districts)):
92 district = sorted_districts[i]
93 district_data = fields[i + 1]
94 db[district][date] = {'new_infections': int(district_data)}
97 # In LaGeSo's data, the last "district" is actually the sum of all districts /
98 # the whole of Berlin.
100 # Fail on any day where the "sum" district's new infections are not the proper
101 # sum of the individual districts new infections. Yes, sometimes Lageso sends
102 # data that is troubled in this way. It will then have to be fixed manually in
103 # the table file, since we should have a human look at what mistake was
105 for date in sorted_dates:
106 sum_district = sorted_districts[-1]
108 for district in sorted_districts[:-1]:
109 day_sum += db[district][date]['new_infections']
110 if day_sum != db[sum_district][date]['new_infections']:
111 raise Exception('Questionable district infection sum in %s' % date)
113 # Enhance DB with data about weekly sums, averages, incidences per day. Ignore
114 # days that have less than 6 predecessors (we can only know a weekly average if
115 # we have a whole week of data).
116 for i in range(len(sorted_dates)):
119 date = sorted_dates[i]
122 week_dates += [sorted_dates[i - j]]
123 for district in sorted_districts:
124 district_pop = district_pops[district]
126 for week_date in week_dates:
127 week_sum += db[district][week_date]['new_infections']
128 db[district][date]['week_sum'] = week_sum
129 db[district][date]['week_average'] = week_sum / 7
130 db[district][date]['week_incidence'] = (week_sum / district_pop) * 100000
132 # Optimized for web browser viewing.
133 if output_type == 'html':
134 print("""<!DOCTYPE html>
138 th { text-align: left; vertical-align: bottom; }
139 .day_row:nth-child(7n+3) > td { border-top: 1px solid black; }
140 .vertical_header { writing-mode: vertical-rl; transform: rotate(180deg); font-weight: normal; }
141 .fixed_head { position: sticky; top: 0; background-color: white; }
142 .bold { font-weight: bold }
144 <title>Berlin's Corona infection numbers, development by districts</title>
146 <a href="/">home</a> · <a href="/contact.html">contact</a> · <a href="/privacy.html">privacy</a>
147 <h1>Berlin's Corona infection numbers, development by districts</h1>
148 <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>
151 <th colspan=2></th>""")
152 sorted_dates.reverse()
153 sum_district = sorted_districts[-1]
154 for district in sorted_districts:
155 long_form = translate[district]
156 # Wrap in div because the vertical orientation otherwise fails
158 print('<th><div class="vertical_header">%s</div></th>' % long_form)
160 print('<tr class="fixed_head">')
161 # In Chromium, the th only stay fixed if also given this class.
162 print('<th class="fixed_head">date</th>')
163 print('<th class="fixed_head"></th>')
164 for district in sorted_districts:
165 print('<th class="fixed_head">%s</th>' % district)
167 for date in sorted_dates:
168 print('<tr class="day_row">')
169 print('<td>%s</td>' % date)
171 print('<tr><th><abbr title="new infections counted">new</abbr></th></tr>')
172 for abbr in ['wsum', 'wavg', 'winc']:
173 print('<tr><th><abbr title="%s">%s</abbr></th></tr>' %
174 (translate[abbr], abbr))
175 print('</table></td>')
176 for district in sorted_districts:
177 district_data = db[district][date]
178 week_sum = week_avg = week_inc = '?'
179 new_infections = district_data['new_infections']
180 if 'week_sum' in district_data:
181 week_sum = '%s' % district_data['week_sum']
182 if 'week_average' in district_data:
183 week_avg = '%.1f' % district_data['week_average']
184 if 'week_incidence' in district_data:
185 week_inc = '%.1f' % district_data['week_incidence']
188 print('<tr><td class="bold">%s</td></tr>' % new_infections)
189 print('<tr><td>%s</td></tr>' % week_sum)
190 print('<tr><td>%s</td></tr>' % week_avg)
191 print('<tr><td>%s</td></tr>' % week_inc)
198 # Optimized for in-terminal curl.
199 elif output_type == 'txt':
201 # Explain what this is.
203 """Table of Berlin's Corona infection number development by districts.
204 Updated daily at 9pm based on data from the "Senatsverwaltung für Gesundheit, Pflege und Gleichstellung".
206 Abbrevations/explanations:
209 intro += "%s: %s\n" % (k, translate[k])
211 Source code: https://plomlompom.com/repos/?p=berlin-corona-table
212 HTML view: https://plomlompom.com/berlin_corona.html
216 # Output table of enhanced daily infection data, newest on top,
217 # separated into 7-day units.
218 sorted_dates.reverse()
220 sum_district = sorted_districts[-1]
221 for date in sorted_dates:
224 if weekday_count == 0:
225 print(' '*11, ' '.join(sorted_districts[:-1]),
226 sorted_districts[-1], 'wsum', ' wavg', 'winc')
227 week_start_date = date
231 for district in sorted_districts:
232 new_infections += [db[district][date]['new_infections']]
233 week_sum = week_avg = week_inc = ''
234 sum_district_data = db[sum_district][date]
235 if 'week_sum' in sum_district_data:
236 week_sum = '%4s' % sum_district_data['week_sum']
237 if 'week_average' in sum_district_data:
238 week_avg = '%5.1f' % sum_district_data['week_average']
239 if 'week_incidence' in sum_district_data:
240 week_inc = '%4.1f' % sum_district_data['week_incidence']
241 print(date, ' '.join(['%3s' % infections
242 for infections in new_infections]),
243 week_sum, week_avg, week_inc)
245 # Maintain 7-day cycle.
247 if weekday_count != 7:
251 # After each 7 days, print summary for individual districts.
255 for district in sorted_districts[:-1]:
256 weekly_sums += [db[district][week_start_date]['week_sum']]
257 weekly_avgs += [db[district][week_start_date]['week_average']]
258 weekly_incs += [db[district][week_start_date]['week_incidence']]
260 print('district stats for week from %s to %s:' % (date, week_start_date))
261 print(' '*7, ' '.join(sorted_districts[:-1]))
262 print('wsum', ' '.join(['%5.1f' % wsum for wsum in weekly_sums]))
263 print('wavg', ' '.join(['%5.1f' % wavg for wavg in weekly_avgs]))
264 print('winc', ' '.join(['%5.1f' % winc for winc in weekly_incs]))