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 # Define and move sum_district from end to start.
98 sum_district = sorted_districts.pop()
99 sorted_districts.insert(0, sum_district)
101 # In LaGeSo's data, the last "district" is actually the sum of all districts /
102 # the whole of Berlin.
104 # Fail on any day where the "sum" district's new infections are not the proper
105 # sum of the individual districts new infections. Yes, sometimes Lageso sends
106 # data that is troubled in this way. It will then have to be fixed manually in
107 # the table file, since we should have a human look at what mistake was
109 for date in sorted_dates:
111 for district in [d for d in sorted_districts if not d==sum_district]:
112 day_sum += db[district][date]['new_infections']
113 if day_sum != db[sum_district][date]['new_infections']:
114 raise Exception('Questionable district infection sum in %s' % date)
116 # Enhance DB with data about weekly sums, averages, incidences per day. Ignore
117 # days that have less than 6 predecessors (we can only know a weekly average if
118 # we have a whole week of data).
119 for i in range(len(sorted_dates)):
122 date = sorted_dates[i]
125 week_dates += [sorted_dates[i - j]]
126 for district in sorted_districts:
127 district_pop = district_pops[district]
129 for week_date in week_dates:
130 week_sum += db[district][week_date]['new_infections']
131 db[district][date]['week_sum'] = week_sum
132 db[district][date]['week_average'] = week_sum / 7
133 db[district][date]['week_incidence'] = (week_sum / district_pop) * 100000
135 # Optimized for web browser viewing.
136 if output_type == 'html':
137 print("""<!DOCTYPE html>
141 th { text-align: left; vertical-align: bottom; }
142 .day_row:nth-child(7n+3) > td { border-top: 1px solid black; }
143 .vertical_header { writing-mode: vertical-rl; transform: rotate(180deg); font-weight: normal; }
144 .fixed_head { position: sticky; top: 0; background-color: white; }
145 .bold { font-weight: bold }
147 <title>Berlin's Corona infection numbers, development by districts</title>
149 <a href="/">home</a> · <a href="/contact.html">contact</a> · <a href="/privacy.html">privacy</a>
150 <h1>Berlin's Corona infection numbers, development by districts</h1>
151 <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>
154 <th colspan=2></th>""")
155 sorted_dates.reverse()
156 for district in sorted_districts:
157 long_form = translate[district]
158 # Wrap in div because the vertical orientation otherwise fails
160 print('<th><div class="vertical_header">%s</div></th>' % long_form)
162 print('<tr class="fixed_head">')
163 # In Chromium, the th only stay fixed if also given this class.
164 print('<th class="fixed_head">date</th>')
165 print('<th class="fixed_head"></th>')
166 for district in sorted_districts:
167 print('<th class="fixed_head">%s</th>' % district)
169 for date in sorted_dates:
170 print('<tr class="day_row">')
171 print('<td>%s</td>' % date)
173 print('<tr><th><abbr title="new infections counted">new</abbr></th></tr>')
174 for abbr in ['wsum', 'wavg', 'winc']:
175 print('<tr><th><abbr title="%s">%s</abbr></th></tr>' %
176 (translate[abbr], abbr))
177 print('</table></td>')
178 for district in sorted_districts:
179 district_data = db[district][date]
180 week_sum = week_avg = week_inc = '?'
181 new_infections = district_data['new_infections']
182 if 'week_sum' in district_data:
183 week_sum = '%s' % district_data['week_sum']
184 if 'week_average' in district_data:
185 week_avg = '%.1f' % district_data['week_average']
186 if 'week_incidence' in district_data:
187 week_inc = '%.1f' % district_data['week_incidence']
190 print('<tr><td class="bold">%s</td></tr>' % new_infections)
191 print('<tr><td>%s</td></tr>' % week_sum)
192 print('<tr><td>%s</td></tr>' % week_avg)
193 print('<tr><td>%s</td></tr>' % week_inc)
200 # Optimized for in-terminal curl.
201 elif output_type == 'txt':
203 # Explain what this is.
205 """Table of Berlin's Corona infection number development by districts.
206 Updated daily at 9pm based on data from the "Senatsverwaltung für Gesundheit, Pflege und Gleichstellung".
208 Abbrevations/explanations:
211 intro += "%s: %s\n" % (k, translate[k])
213 Source code: https://plomlompom.com/repos/?p=berlin-corona-table
214 HTML view: https://plomlompom.com/berlin_corona.html
218 # Output table of enhanced daily infection data, newest on top,
219 # separated into 7-day units.
220 sorted_dates.reverse()
222 for date in sorted_dates:
225 if weekday_count == 0:
226 print(' '*11, ' '.join(sorted_districts[:-1]),
227 sorted_districts[-1], 'wsum', ' wavg', 'winc')
228 week_start_date = date
232 for district in sorted_districts:
233 new_infections += [db[district][date]['new_infections']]
234 week_sum = week_avg = week_inc = ''
235 sum_district_data = db[sum_district][date]
236 if 'week_sum' in sum_district_data:
237 week_sum = '%4s' % sum_district_data['week_sum']
238 if 'week_average' in sum_district_data:
239 week_avg = '%5.1f' % sum_district_data['week_average']
240 if 'week_incidence' in sum_district_data:
241 week_inc = '%4.1f' % sum_district_data['week_incidence']
242 print(date, ' '.join(['%3s' % infections
243 for infections in new_infections]),
244 week_sum, week_avg, week_inc)
246 # Maintain 7-day cycle.
248 if weekday_count != 7:
252 # After each 7 days, print summary for individual districts.
256 for district in sorted_districts[1:]:
257 weekly_sums += [db[district][week_start_date]['week_sum']]
258 weekly_avgs += [db[district][week_start_date]['week_average']]
259 weekly_incs += [db[district][week_start_date]['week_incidence']]
261 print('district stats for week from %s to %s:' % (date, week_start_date))
262 print(' '*7, ' '.join(sorted_districts[1:]))
263 print('wsum', ' '.join(['%5.1f' % wsum for wsum in weekly_sums]))
264 print('wavg', ' '.join(['%5.1f' % wavg for wavg in weekly_avgs]))
265 print('winc', ' '.join(['%5.1f' % winc for winc in weekly_incs]))