Правильные графики 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 должно быть радикально просто)
    • Убедиться, что математика работает как надо

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

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 22

      +1
      Университет Джона-Хопкинса выкладывает на GitHub статистику по распространению, на основе которой строит свой дашборд, который появился одним из первых. Правда там нет отдельных данных по регионам РФ.
        +3

        Изначальная задача была именно по регионам РФ построить такие графики

          +1
          По регионам РФ можно отсюда в свой аккаунт экспортировать данные и с ними поиграться datalens.yandex/7o7is1q6ikh23
            0

            Спасибо, не нашел вовремя этот сервис

        0
          +1

          щитоподелать, какие (официальные) данные имеются, с теми и работаем

          +1

          Нужна возможность переключиться в линейные шкалы.
          К виду параболы в логарифмических — я ещё не привык.

            +3

            Изначальный смысл ухода от графиков, в которых время откладывается по оси X именно в том, чтобы отображать экспоненциальный рост в логарифмических координатах, так как для таких процессов это более естесственно. В логарифмическом масштабе можно сказать что экспоненциальный рост прекратился если график отклонится от прямой линии и пойдёт вниз. В линейных, критерий уже сложнее. Не уверен, что возможность переключиться в линейный масштаб необходима, но если будет мёржреквест, с радостью приму.

              +1
              Изначальный смысл ухода от графиков, в которых время откладывается по оси X

              Для автокаталитических процессов, происходящих при ограниченных ресурсах, функция y, определённая как f' = y(f) исходной функции, выглядит как парабола.


              https://youtu.be/OmcNJLJD9uU?t=1861


              Говорить об экспоненциальном росте имеет смысл относительно времени. А указанная функция y — это не экспонента.


              мёржреквест, с радостью приму.

              Увы, питоном не владею.

            +1
            Не проще воспользоваться официальным инструментом?
              0

              Спасибо за информацию, не нашёл этот сервис вовремя, но зато удалось пощупать парсинг HTML чистыми средствами питона.

              0

              Ого, судя по этим графикам Россия в который раз соревнование с США устраивает.
              Даже некарантиновая Швеция сос… отстала.

              –2
              На портале открытых данных москвы регистрируется общее количество смертей по месяцам. В апреле рекорд за 10 лет. Прошлый рекорд был на 9% меньше в 2018 году. На последствия ковида, включая алкоголизм от карантина, таким образом можно отнести по Москве до 1 тысяч смертей на 13 миллионов человек. Вы же помните страшную эпидемию весны 2018-ого года? Тогда тоже был тотальный карантин. Интересно, что в марте 2018-ого показатель был На 16% выше, чем в этом году. Так что в марте пандемия примерно полторы тысячи человек оживила!

              data.mos.ru/opendata/7704111479-dinamika-registratsii-aktov-grajdanskogo-sostoyaniya/row/1035521177?pageNumber=13&versionNumber=3&releaseNumber=42
                +1
                Отличный сайт, и информация интересная, но есть одно но.
                Судя по провалу статистики рождений за Апрель, не всё там хорошо сейчас с регистрацией актов ГС.
                Да и что им мешает в итоговых отчетах писать меньшую цифру чтобы не нагнетать?
                Либо просто люди не сильно спешат в загс за свидетельствами о смерти.
                Вряд-ли там есть контрольная сумма или проверка четности.
                И, кстати, что там было в 2010 году в июле-августе?
                  +2
                  Сам себе отвечу — торфяные пожары, смог и смертность подскочила в полтора раза!
                    0
                    Ну это не у них проблема. Разница в том, что смерть регистрируется по факту смерти, даже если труп не опознанный и никто его не забрал документы на него оформлены будут. А вот рождение регистрируется когда вы подаёте документы на получение свидетельства о рождении, которое вам можно не получать в течение месяца, а потом тоже можно, но это выховет некоторые проблемы при устройстве в больницу, да и всё (По паспорту мамы записывать не будут).

                    Поэтому моя дочь, которой уже 18 дней от роду свидетельство до сих пор не получила, и соответственно в статистику ЗАГСА не попала. Тупо сейчас очень неудобно идти в ЗАГС по понятным причинам, а многие из них тупо сейчас не работают.
                  0
                  отсутствие сортировки в списке сводит на нет возможность нормального использования. какая бы интерсная математика не была бы заложена внутри, если пользоваться этим в крайней мере неудобно, то вся работа равна уровню самого слабого звена.
                    0

                    возможность "нормального использования" имеется:


                    Впрочем, проблемы юзабилити тоже имеются, но вовсе не такие фатальные

                      0
                      отсортировать 2 группы — задача 5 минут для любого языка программирования. а не заставлять пользователя делать стопятьсот действий. в принципе с темже успехом могли бы выложить просто таблицы с данными и советовать в экселе графики построить. \

                      решение простое же — просто отсортировать список регионов. даже если просто весь список отсортируете уже намного удобнее станет. чуть дольше вторую сотрировку для регионов рф сделать или вообще выделить в отдельный список, но тут уже дополнительные моменты возникают с выбором. так что просто сортировка ))) заодно вспомнить методы сортировки всегда полезно и написать их на ванили )))
                        0

                        Найти нужный нужное место на странице через Ctrl+F — это не стопятьсот действий, а наиболее логичное решение когда на странице много несистематизированного контента. Это быстро и удобно, а длинный список никогда не станет удобным сколько его не сортируй и не группируй, так что тратить время на имплементацию его сортировки нецелесообразно. Таблицы с сырыми данными уже выложены Яндексом, выше целых два комента про это.
                        Если Вам так уж мешает отсутствие сортировки, Merge Requests are Welcome

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое