Правильные графики Covid-19

—

от автора

Во времена повсеместной одержимости библиотеками и веб-фреймворками мы стали забывать радость от решения задач минимальными средствами. В этой статье, мы запилим веб-сервис на актуальную тему, используя ванильные Python и JavaScript, а также, задеплоим его в GitLab Pages. Быстро, минималистично, без лишних зависимостей, и максимально элегантно.
Вдохновившись видосом How To Tell If We’re Beating COVID-19 от minutephysics, я набросал в свободное (от удаленной работы и домашних дел) время сервис, который на основе данных с Карты распространения коронавируса в России и мире от Яндекса строит графики, аналогичные тем, что на странице Covid Trends. Вот, что из этого вышло:

Интересно? Погнали!

Где взять данные?

Примерно в то время, когда у меня родилась идея воспроизвести графики от minutephysics для регионов России, Яндекс добавил в карту гистограммы по каждому региону.

Данные хранятся прямо в теле страницы, что оказалось крайне удобно. Я испытываю какое-то особое удовольствие, когда получается элегантно решить задачу без внешних зависимостей, так что, для такого простого случая как наш, монструозные парсеры и библиотека requests идут лесом. Только встроенные средства языка, только хардкор (не такой уж и хардкор, в стандартной библиотеке есть всё, что надо, и всё крайне удобно):

from urllib.request import urlopen from html.parser import HTMLParser import json  class Covid19DataLoader(HTMLParser):     page_url = "https://yandex.ru/web-maps/covid19"      def __init__(self):         super().__init__()         self.config_found = False         self.config = None      def load(self):         with urlopen(self.page_url) as response:             page = response.read().decode("utf8")         self.feed(page)         return self.config['covidData']      def handle_starttag(self, tag, attrs):         if tag == 'script':             for k, v in attrs:                 if k == 'class' and v == 'config-view':                     self.config_found = True      def handle_data(self, data):         if self.config_found and not self.config:             self.config = json.loads(data)

Как поместить данные на страницу?

Данные получены, следующая задача — поместить их в нужное место HTML-странички, чтобы показать через Chart.js. В веб-фреймворках для этого используются шаблонизаторы, но нам не нужны никакие шаблонизаторы кроме string.Template:

def get_html(covid_data):     template_str = open(page_path, 'r', encoding='utf-8').read()     template = Template(template_str)     page = template.substitute(         covid_data=json.dumps(covid_data),         data_info=get_info(covid_data)     )     return page

А по пути page_path лежит примерно такое:

<!DOCTYPE html> <html> <head><!-- ... --></head>     <body>     <!-- ... -->     <div>$data_info</div>     <script type="text/javascript">         let covid_data = $covid_data         // ...     </script> </body> </html>

Профит! Данные инжектятся в страничку! Можно отправлять в браузер!

Как зайти на страничку?

Вот. Простейший веб сервер на чистейшем Python:

from http.server import BaseHTTPRequestHandler from lib.data_loader import Covid19DataLoader from lib.page_maker import get_html  class Handler(BaseHTTPRequestHandler):     def do_GET(self):         if self.path == '/':             self.send_response(200)             self.send_header('Content-Type', 'text/html')             self.end_headers()             try:                 response = get_html(Covid19DataLoader().load())                 self.wfile.write(response.encode('utf-8'))             except Exception as e:                 self.send_error(500)                 print(f'{type(e).__name__}: {e}')         else:             self.send_error(404)

Не рекомендуется для продакшна, но для продакшна у нас будет GitLab Pages, не переключайте канал вкладку!

Внимательный читатель кода мог заметить, что данные качаются при каждом запросе, что ну совсем не подходит для продакшна, да и для медленного дачного Интернета не очень. Решение: кэширование в файл. Для инвалидации кэша просто удаляем файл (например, из cron). Проще не придумать, и ведь работает!

Данные для правильных графиков

Как завещал Aatish Bhatia, по оси Y должно быть количество новых случаев заражения за последнюю неделю, а по оси X — общее количество заражённых на момент времени.

Только мы возьмём не за неделю, а за 3 дня, чтобы график был более чувствительным и быстрее отражал ситуацию. Вопрос "Какое окно брать?" мною детально не исследовался, если Вам кажется, что лучше взять подлиннее, и колебания графика туда-сюда (которые наблюдаются в данных за последнюю неделю) только мешают, пишите в коменты!

from datetime import timedelta from functools import reduce  y_axis_window = timedelta(days=3).total_seconds()  def get_cases_in_window(data, current_time):     window_open_time = current_time - y_axis_window     cases_in_window = list(filter(lambda s: window_open_time <= s['ts'] < current_time, data))     return cases_in_window  def differentiate(data):     result = [data[0]]     for prev_i, cur_sample in enumerate(data[1:]):         result.append({             'ts': cur_sample['ts'],             'value': cur_sample['value'] - data[prev_i]['value']         })     return result  def get_trend(histogram):     trend = []     new_cases = differentiate(histogram)     for sample in histogram:         current_time = sample['ts']         total_cases = sample['value']         new_cases_in_window = get_cases_in_window(new_cases, current_time)         total_new_cases_in_window = reduce(lambda a, c: a + c['value'], new_cases_in_window, 0)         trend.append({'x': total_cases,'y': total_new_cases_in_window})     return trend  def get_trends(data_items):     return { area['name']: get_trend(area['histogram']) for area in data_items }

Фронтэнд

Тут нет ничего, чем я хотел бы поделиться (разве что восторгом от CSS Grid, который впервые попробовал), так как разбираюсь во фронтэнде хуже всего. Прошу поправить меня мёржреквестом, если я где-то накосячил. Вот код.

Как опубликовать сервис?

Я очень люблю Docker, и первый вариант, который мне пришёл на ум — склонировать репозиторий на свой VPS, позвать docker-compose up --build -d, настроить cron на удаление кэш-файла (хотя постойте, какой крон внутри контейнера?? ладно, потом разберёмся…), и, вопреки предупреждениям о том, что http.server не для продакшна, открыть порт во внешку. А потом натравить GitLab CI на обновление сервиса по ssh (я сто раз уже так делал).

Но тут я заметил, что страничка, отправляемая в браузер, весьма статична.

Это позволяет её просто выгрузить в файл и захостить бесплатно в GitLab Pages:

import os from lib.data_loader import Covid19DataLoader from lib.data_processor import get_trends from lib.page_maker import get_html  page_dir = 'public' page_name = 'index.html'  print('Updating Covid-19 data from Yandex...') raw_data = Covid19DataLoader().load() print('Calculating trends...') trends = get_trends(raw_data['items']) page = get_html({'raw_data': raw_data, 'trends': trends})  if not os.path.isdir(page_dir):     os.mkdir(page_dir) page_path = os.path.join(page_dir, page_name) open(page_path, 'w', encoding='utf-8').write(page) print(f'Page saved as "{page_path}"')

Для того, чтобы опубликовать страничку по симпатичному адресу https://himura.gitlab.io/covid19, достаточно написать следующий .gitlab-ci.yml:

image: python pages:     stage: deploy     only: [ master ]     script:         - python ./get_static_html.py     artifacts:       paths: [ public ]

А обновлять через Pipeline Schedules:

Результат работы выглядит как-то так:

А результат деплоймента — как на КДПВ:

Всё?

Кодснипеты в статье немного упрощены, а весь реальный код в репозитории на GitLab: https://gitlab.com/himura/covid19

Я почти уверен, что много чего упустил, так как на весь код и статью в сумме было потрачено примерно 2 рабочих дня. Пожалуйста, давайте вместе что-нибудь улучшим в этом MVP, если Вам кажется, что что-то не так. Пишите комментарии, пишите баги, а лучше всего — пишите мёржреквесты. В числе known issues:

  • Добавить даты в тултип точкам
  • Добавить мобильную версию (с css grid должно быть радикально просто)
  • Убедиться, что математика работает как надо

Надеюсь, статья была интересна и/или полезна, а также очень надеюсь, что вскоре мы все будем наблюдать, как линии графиков устремятся вниз.
Будьте здоровы и не вносите в свой код зависимостей, без которых можно обойтись!

ссылка на оригинал статьи https://habr.com/ru/post/501246/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *