Анализ данных о COVID19
Contents
Анализ данных о COVID19
#
Продемонстрируем возможности pandas
на реальных данных. Возьмём для этих целей данные о коронавирусе от университета Джонса Хопкинса и от Яндекса.
Университет Джонса Хопкинса собирает данные по коронавирусу и хранит их в открытом доступе в репозитории. Среди прочего, в них содержится информация о количестве заболевших, погибших и выздоровевших. Данные хранятся не в типичном для
pandas
формате: информация за одни день хранится в столбце, а не в строках, т.е. не в соответствии с форматом панельных данных. Чтобы не усложнять изложение, я составил скрипт, который скачивает таблицы из репозитория, извлекает из них информацию только по России и сохраняет их в панельном формате. Результат работы этого скрипта от 25.11.2021 хранится по ссылке.Яндекс агрегирует информацию о коронавирусе по всей россии и предоставляет возможность скачать их данные. К сожалению, чтобы получить эти данные, нужно проделать куда больше шагов, чем в примере выше (с инструкцией можно ознакомится здесь). Загруженные от 25.11.2021 данные хранятся здесь.
from pathlib import Path
folder = Path("..", "..") / "assets" / "data" / "tables"
Данные о коронавирусе от университета Джонса Хопкинса#
Первые 5 строк файла выглядят так.
Date,recovered,deaths,confirmed
2020-01-22,0,0,0
2020-01-23,0,0,0
2020-01-24,0,0,0
2020-01-25,0,0,0
2020-01-26,0,0,0
Видим, что столбец Date
хранит даты в стандартном для pandas
формате YYYY-MM-DD
. Считываем таблицу, указав в качестве индекса этот столбец, не забывая указать pandas
, что в нем хранятся даты. С помощью метода info печатаем информацию о таблице.
import pandas as pd
import numpy as np
filename = "CSSE_Russia.csv"
path = folder / filename
df = pd.read_csv(path, index_col="Date", parse_dates=["Date"])
df.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 672 entries, 2020-01-22 to 2021-11-23
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 recovered 672 non-null int64
1 deaths 672 non-null int64
2 confirmed 672 non-null int64
dtypes: int64(3)
memory usage: 21.0 KB
Расшифруем полученный вывод.
Первая строка сообщает, что индексом таблицы является DatetimeIndex в диапазоне от 22 января 2020 гола до 23 ноября 2021 года (формат (YYYY-MM-DD)). Из первых двух строчек можно понять, что в таблице 3 столбца (+ как бы столбец индекса) и 672 строки.
Далее идет информация о каждом столбце таблицы.
recovered
— суммарное количество зарегистрированных выздоровевших отcovid19
пациентов;deaths
— суммарное количество зарегистрированных смертей отcovid19
;confirmed
— суммарное количество зарегистрированных заболевшихcovid19
. Под суммарным количество здесь имеется ввиду то, что на каждую дату в таблице приводятся данные о, например, количестве зарегистрированных больных не только за текущий день, но и за все предшествующие.
Кроме того, из вывода команды info
можно понять, что во всех столбцах отсутствуют пропущенные значения NA
(672 non-null
в 672 строках). Тем не менее это ещё не значит, что в таблице присутствуют данные за все дни в указанном диапазоне. Проверим это пользуясь тем, что можно высчитывать временной интервал между двумя объектами datetime
оператором -
.
date_range = df.index.max() - df.index.min()
print(date_range.days)
671
Видим, что между 22.01.2020 и 23.11.2021 прошел 671 день, т.е. данные представлены за все дни из этого диапазона, если предположить, что нет двух строк с одной и той же датой. Проявим максимальную осторожность и проверим, что в таблице присутствуют строки для каждого дня в указанном диапазоне. Для этого создадим даты в указанном диапазоне с частотой в день функцией pd.date_range и посмотрим разницу с индексом таблицы.
from datetime import date
start = date(year=2020, month=1, day=22)
end = date(year=2021, month=11, day=23)
date_range = pd.date_range(start=start, end=end, freq="D")
print(f"Созданный диапазон: {date_range}")
difference = date_range.difference(df.index)
print(f"Разница с индексом таблицы: {difference}")
Созданный диапазон: DatetimeIndex(['2020-01-22', '2020-01-23', '2020-01-24', '2020-01-25',
'2020-01-26', '2020-01-27', '2020-01-28', '2020-01-29',
'2020-01-30', '2020-01-31',
...
'2021-11-14', '2021-11-15', '2021-11-16', '2021-11-17',
'2021-11-18', '2021-11-19', '2021-11-20', '2021-11-21',
'2021-11-22', '2021-11-23'],
dtype='datetime64[ns]', length=672, freq='D')
Разница с индексом таблицы: DatetimeIndex([], dtype='datetime64[ns]', freq='D')
Из последней строки вывода видно, что есть данные за каждый день.
Распечатаем таблицу.
df
recovered | deaths | confirmed | |
---|---|---|---|
Date | |||
2020-01-22 | 0 | 0 | 0 |
2020-01-23 | 0 | 0 | 0 |
2020-01-24 | 0 | 0 | 0 |
2020-01-25 | 0 | 0 | 0 |
2020-01-26 | 0 | 0 | 0 |
... | ... | ... | ... |
2021-11-19 | 0 | 256669 | 9099253 |
2021-11-20 | 0 | 257891 | 9135149 |
2021-11-21 | 0 | 259107 | 9170898 |
2021-11-22 | 0 | 260319 | 9205431 |
2021-11-23 | 0 | 261526 | 9238330 |
672 rows × 3 columns
Вначале идут нули по всем строкам, что объясняется видимо тем, что
Первый официально подтверждённый случай заболевания коронавирусной инфекцией COVID-19 в стационарных учреждениях социальной защиты был зарегистрирован 11 апреля 2020 года. wikipedia.
Так же в столбце по выздоровевшим наблюдаются только нули, что не соответствует действительности, т.е. судя по всему в таблице все же есть отсутствующие значения, которые заполнены нулями, а не np.nan
. Визуализируем данные, чтобы а) легче их воспринимать; б) посмотреть на них целиком. Воспользуемся для этого библиотекой plotly
, т.к. мы ещё не знаем, что мы ищем и интерактивность может пригодиться.
import plotly.express as px
from IPython.display import display, HTML
fig = px.line(df, labels={"value": "Total № of patients"})
fig
Из построенного графика можно сделать вывод о том, что данные начали поступать с 12 февраля (несмотря на утверждении в википедии), а начиная с 5 августа 2021 года данные о выздоровевших перестали обновляться.
В данном случае разумно отбросить нулевые строки и заменить пропущенные данные на np.nan
.
drop_date = date(year=2021, month=8, day=5)
df = df.loc[np.any(df != 0, axis=1)].copy()
df.loc[drop_date:, "recovered"] = np.nan
fig = px.line(df, labels={"value": "Total № of patients"})
fig
Более или менее понятно, что приблизительно должно выполнятся равенство confirmed = recovered + deaths, т.к. большинство заболевших в итоге выздоравливают или нет. Посмотрим растёт ли невязка этого равенства со временем.
residual = df.confirmed - df.deaths - df.recovered
residual.name = "confirmed - deaths - recovered"
fig = px.line(residual, labels={"value": "Total № of patients"})
fig
У этого графика наблюдается тенденция роста со временем, что наталкивает на мысль, что
формула не верна;
данные собираются не самым точном образом;
растет средняя продолжительность болезни.
Чтобы оценить динамику, удобно также посмотреть на суточные показатели. Суточный прирост — разница между показателем за выбранный день и за предыдущий день. Чтобы его вычислить, надо посчитать нечто похожее на x[:-1] - x[1:]
. Для таких вычислений есть встроенный метод DataFrame.diff (аналогичный метод есть и у pd.Series
).
daily_df = df.diff()
fig = px.line(daily_df, labels={"value": "Daily № of patients"})
fig
Оба графика довольно сильно осциллируют. Применим метод скользящего среднего по неделе, чтобы избавиться от наблюдаемого “шума”. Для этого воспользуемся методом pd.DataFrame.rolling, который предназначен для вычислений с скользящим окном.
smoothed_daily_df = daily_df.rolling(7).mean()
fig = px.line(smoothed_daily_df, labels={"value": "Daily № of patients"})
fig
Данные о коронавирусе в России от яндекса#
Первый 5 строк файла выглядят так.
"Дата","Регион","Заражений","Выздоровлений","Смертей","Смертей за день","Заражений за день","Выздоровлений за день"
20.09.2020,Томская обл.,6775,5554,79,1,62,92
26.09.2020,Костромская обл.,4553,3329,67,5,54,44
02.09.2021,Ямало-Ненецкий АО,47772,45810,625,4,102,121
15.12.2020,Сахалинская обл.,13406,10292,11,0,142,366
Видно, что в столбце “Дата” хранятся даты в формате DD.MM.YYYY
, который не является стандартным для pandas
. Чтобы корректно прочитать эти данные, определим функцию, которая преобразует строку с датой в формате DD.MM.YYYY
в datetime
объект и при чтении файла передадим эту функцию в качестве парсера дат.
import os
import pandas as pd
from datetime import datetime
def date_parser(x):
return datetime.strptime(x, r"%d.%m.%Y")
filename = "yandex_data.csv"
path = folder / filename
df = pd.read_csv(path, parse_dates=["Дата"], index_col="Дата", date_parser=date_parser)
df.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 52938 entries, 2020-09-20 to 2021-04-20
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Регион 52938 non-null object
1 Заражений 52938 non-null int64
2 Выздоровлений 52938 non-null int64
3 Смертей 52938 non-null int64
4 Смертей за день 52938 non-null int64
5 Заражений за день 52938 non-null int64
6 Выздоровлений за день 52938 non-null int64
dtypes: int64(6), object(1)
memory usage: 3.2+ MB
Что мы видим:
53938 строк;
7 полностью заполненных столбцов;
даты от 20 сентября 2021 года до 20 апреля 2021 года в качестве индекса;
Посмотрим первые 5 строк таблицы.
df.head()
Регион | Заражений | Выздоровлений | Смертей | Смертей за день | Заражений за день | Выздоровлений за день | |
---|---|---|---|---|---|---|---|
Дата | |||||||
2020-09-20 | Томская обл. | 6775 | 5554 | 79 | 1 | 62 | 92 |
2020-09-26 | Костромская обл. | 4553 | 3329 | 67 | 5 | 54 | 44 |
2021-09-02 | Ямало-Ненецкий АО | 47772 | 45810 | 625 | 4 | 102 | 121 |
2020-12-15 | Сахалинская обл. | 13406 | 10292 | 11 | 0 | 142 | 366 |
2020-09-21 | Волгоградская обл. | 14158 | 12376 | 131 | 0 | 95 | 13 |
Можно заметить, что таблица не упорядоченна по индексу. Отсортируем её, для нашего удобства.
df.sort_index(inplace=True)
df.head()
Регион | Заражений | Выздоровлений | Смертей | Смертей за день | Заражений за день | Выздоровлений за день | |
---|---|---|---|---|---|---|---|
Дата | |||||||
2020-03-12 | Сахалинская обл. | 0 | 0 | 0 | 0 | 0 | 0 |
2020-03-12 | Северная Осетия | 0 | 0 | 0 | 0 | 0 | 0 |
2020-03-12 | Камчатский край | 0 | 0 | 0 | 0 | 0 | 0 |
2020-03-12 | Липецкая обл. | 3 | 0 | 0 | 0 | 3 | 0 |
2020-03-12 | Крым | 0 | 0 | 0 | 0 | 0 | 0 |
Кстати, можно заметить, что в начало попали строки от 4 января 2020 года, что гораздо раньше диапазона дат, полученного в команде df.info
.
df.info()
fig = px.line(df, y=["Заражений за день"], color="Регион", title="Заражений за день")
fig
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 52938 entries, 2020-03-12 to 2021-11-24
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Регион 52938 non-null object
1 Заражений 52938 non-null int64
2 Выздоровлений 52938 non-null int64
3 Смертей 52938 non-null int64
4 Смертей за день 52938 non-null int64
5 Заражений за день 52938 non-null int64
6 Выздоровлений за день 52938 non-null int64
dtypes: int64(6), object(1)
memory usage: 3.2+ MB
Т.е. в таблице данные представлены от 4 января 2020 года до 24 ноября 2021 года.
Т.к. python
работает с unicode
, то можно оставить имена столбцов и на русском языке, но в целях демонстрации переименуем столбцы на английский язык методом DataFrame.rename. На вход он принимает словарь, в котором ключи — старые названия столбцов, значения — новые. По умолчанию этот метод возвращает копию таблицы с новыми столбцами имен. Изменить это можно параметром inplace=True
.
Индекс переименовывается отдельно.
df.rename({
"Регион": "region",
"Заражений": "cases",
"Выздоровлений": "recoveries",
"Смертей": "deaths",
"Смертей за день": "deaths per day",
"Заражений за день": "cases per day",
"Выздоровлений за день": "recoveries per day"
}, inplace=True, axis="columns")
df.index.rename("date", inplace=True)
df
region | cases | recoveries | deaths | deaths per day | cases per day | recoveries per day | |
---|---|---|---|---|---|---|---|
date | |||||||
2020-03-12 | Сахалинская обл. | 0 | 0 | 0 | 0 | 0 | 0 |
2020-03-12 | Северная Осетия | 0 | 0 | 0 | 0 | 0 | 0 |
2020-03-12 | Камчатский край | 0 | 0 | 0 | 0 | 0 | 0 |
2020-03-12 | Липецкая обл. | 3 | 0 | 0 | 0 | 3 | 0 |
2020-03-12 | Крым | 0 | 0 | 0 | 0 | 0 | 0 |
... | ... | ... | ... | ... | ... | ... | ... |
2021-11-24 | Владимирская обл. | 70566 | 54233 | 2432 | 9 | 318 | 131 |
2021-11-24 | Курганская обл. | 43855 | 34847 | 854 | 6 | 207 | 122 |
2021-11-24 | Волгоградская обл. | 115111 | 98848 | 4360 | 37 | 458 | 381 |
2021-11-24 | Красноярский край | 148381 | 132019 | 7286 | 33 | 710 | 695 |
2021-11-24 | Еврейская АО | 10330 | 8862 | 327 | 0 | 42 | 51 |
52938 rows × 7 columns
Сравним статистику по регионам. Для этого выберем последней день данных и построим круговую диаграмму заболеваемости по регионам.
last_day = df.index.max()
last_day_df = df.loc[last_day]
last_day_df
region | cases | recoveries | deaths | deaths per day | cases per day | recoveries per day | |
---|---|---|---|---|---|---|---|
date | |||||||
2021-11-24 | Ингушетия | 25647 | 23801 | 414 | 3 | 83 | 90 |
2021-11-24 | Пензенская обл. | 89547 | 61771 | 4097 | 19 | 391 | 291 |
2021-11-24 | Вологодская обл. | 87778 | 77468 | 2499 | 16 | 397 | 417 |
2021-11-24 | Московская обл. | 577907 | 509872 | 10116 | 56 | 1842 | 2180 |
2021-11-24 | Ленинградская обл. | 88124 | 68927 | 2747 | 10 | 425 | 435 |
... | ... | ... | ... | ... | ... | ... | ... |
2021-11-24 | Владимирская обл. | 70566 | 54233 | 2432 | 9 | 318 | 131 |
2021-11-24 | Курганская обл. | 43855 | 34847 | 854 | 6 | 207 | 122 |
2021-11-24 | Волгоградская обл. | 115111 | 98848 | 4360 | 37 | 458 | 381 |
2021-11-24 | Красноярский край | 148381 | 132019 | 7286 | 33 | 710 | 695 |
2021-11-24 | Еврейская АО | 10330 | 8862 | 327 | 0 | 42 | 51 |
85 rows × 7 columns
fig = px.pie(last_day_df, values="cases", names='region')
fig.update_traces(textposition="inside")
fig
Диаграмма получилась очень загроможденной. Продемонстрируем работу метода pd.unique и посчитаем количество регионов, которые встречаются в таблице.
unique_regions = df["region"].unique()
print(f"{len(unique_regions)} регионов, среди которых")
print(np.sort(unique_regions))
85 регионов, среди которых
['Адыгея' 'Алтай' 'Алтайский край' 'Амурская обл.' 'Архангельская обл.'
'Астраханская обл.' 'Башкортостан' 'Белгородская обл.' 'Брянская обл.'
'Бурятия' 'Владимирская обл.' 'Волгоградская обл.' 'Вологодская обл.'
'Воронежская обл.' 'Дагестан' 'Еврейская АО' 'Забайкальский край'
'Ивановская обл.' 'Ингушетия' 'Иркутская обл.' 'Кабардино-Балкария'
'Калининградская обл.' 'Калмыкия' 'Калужская обл.' 'Камчатский край'
'Карачаево-Черкессия' 'Карелия' 'Кемеровская обл.' 'Кировская обл.'
'Коми' 'Костромская обл.' 'Краснодарский край' 'Красноярский край' 'Крым'
'Курганская обл.' 'Курская обл.' 'Ленинградская обл.' 'Липецкая обл.'
'Магаданская обл.' 'Марий Эл' 'Мордовия' 'Москва' 'Московская обл.'
'Мурманская обл.' 'Ненецкий АО' 'Нижегородская обл.' 'Новгородская обл.'
'Новосибирская обл.' 'Омская обл.' 'Оренбургская обл.' 'Орловская обл.'
'Пензенская обл.' 'Пермский край' 'Приморский край' 'Псковская обл.'
'Ростовская обл.' 'Рязанская обл.' 'Самарская обл.' 'Санкт-Петербург'
'Саратовская обл.' 'Саха (Якутия)' 'Сахалинская обл.' 'Свердловская обл.'
'Севастополь' 'Северная Осетия' 'Смоленская обл.' 'Ставропольский край'
'Тамбовская обл.' 'Татарстан' 'Тверская обл.' 'Томская обл.'
'Тульская обл.' 'Тыва' 'Тюменская обл.' 'Удмуртия' 'Ульяновская обл.'
'ХМАО – Югра' 'Хабаровский край' 'Хакасия' 'Челябинская обл.' 'Чечня'
'Чувашия' 'Чукотский АО' 'Ямало-Ненецкий АО' 'Ярославская обл.']
В таблице присутствуют данные по всем 85 регионам РФ.
85 субъектов Федерации: 22 республики, 9 краев, 46 областей, 3 города федерального значения, 1 автономная область, 4 автономных округа.
Для простоты изложения выберем 3 региона, с наибольшим количеством заболевших за весь период времени, и 2 региона с наименьшим количеством заболевших.
Сначала найдем такие регионы. Для этого воспользуемся методами DataFrame.nlargest и DataFrame.nsmallest, которые позволяют отобрать n
строк таблицы с наибольшими значениями в нужном столбце (или в столбцах). В таблице last_day_df
в столбце cases
как раз хранятся нужные нам значения по регионам. Применив эти два метода к этой таблице, получаем таблицу из нужных нам строк. Нам нужны только регионы, то извлекаем из этой таблицы столбец region
.
Объединить два полученных столбца в один можно методом pd.concat. Т.к. столбцы необходимо совместить “друг под другом”, то в axis
указываем index
. Индекс этих столбцов берется из таблицы исходной таблицы last_day_df
, т.е. это индекс с одной и той же датой. Не учитывать такой индекс при объедении можно указав параметр ignore_index=True
.
largest_regions = last_day_df.nlargest(3, columns=["cases"])["region"]
smallest_regions = last_day_df.nsmallest(2, columns=["cases"])["region"]
regions = pd.concat([largest_regions, smallest_regions], axis="index", ignore_index=True)
print(regions)
0 Москва
1 Санкт-Петербург
2 Московская обл.
3 Чукотский АО
4 Ненецкий АО
Name: region, dtype: object
Теперь из исходной таблицы можно отобрать строки с нужными регионами, проверяя значение в столбце region
, т.е. методом логического отбора. Удобно использовать здесь метод isin.
small_df = df[
df["region"].isin(regions)
].copy()
small_df
region | cases | recoveries | deaths | deaths per day | cases per day | recoveries per day | |
---|---|---|---|---|---|---|---|
date | |||||||
2020-03-12 | Москва | 21 | 0 | 0 | 0 | 21 | 0 |
2020-03-12 | Московская обл. | 4 | 0 | 0 | 0 | 4 | 0 |
2020-03-12 | Санкт-Петербург | 1 | 0 | 0 | 0 | 1 | 0 |
2020-03-13 | Московская обл. | 5 | 0 | 0 | 0 | 1 | 0 |
2020-03-13 | Санкт-Петербург | 4 | 0 | 0 | 0 | 3 | 0 |
... | ... | ... | ... | ... | ... | ... | ... |
2021-11-24 | Московская обл. | 577907 | 509872 | 10116 | 56 | 1842 | 2180 |
2021-11-24 | Чукотский АО | 2793 | 2406 | 24 | 0 | 10 | 15 |
2021-11-24 | Москва | 1926701 | 1745284 | 33635 | 93 | 2533 | 5393 |
2021-11-24 | Санкт-Петербург | 775795 | 709724 | 25802 | 73 | 1387 | 3147 |
2021-11-24 | Ненецкий АО | 3145 | 2865 | 72 | 0 | 8 | 39 |
3105 rows × 7 columns
Построим график заражений только для отобранных регионов.
fig = px.line(small_df, y="cases per day", color="region")
fig
На глаз заметны волны. Кроме того, кажется, что эти волны происходят почти синхронно. Вычислим взаимные корреляции этих графиков. Для этого удобно использовать метод DataFrame.correlate, который вычисляет корреляцию между всеми столбцами таблицы. Создадим таблицу с нужными нам столбцами.
data = {}
for region in regions:
col = small_df[small_df["region"] == region]["cases per day"]
data[region] = col.copy()
to_correlate = pd.DataFrame(data)
to_correlate
Москва | Санкт-Петербург | Московская обл. | Чукотский АО | Ненецкий АО | |
---|---|---|---|---|---|
date | |||||
2020-03-12 | 21 | 1 | 4 | NaN | NaN |
2020-03-13 | 5 | 3 | 1 | NaN | NaN |
2020-03-14 | 9 | 1 | 1 | NaN | NaN |
2020-03-15 | 0 | 0 | 3 | NaN | NaN |
2020-03-16 | 18 | 3 | -1 | NaN | NaN |
... | ... | ... | ... | ... | ... |
2021-11-20 | 3239 | 2637 | 1885 | 25.0 | 16.0 |
2021-11-21 | 3438 | 2496 | 1909 | 10.0 | 16.0 |
2021-11-22 | 2786 | 2215 | 1784 | 11.0 | 16.0 |
2021-11-23 | 2749 | 1060 | 1871 | 10.0 | 14.0 |
2021-11-24 | 2533 | 1387 | 1842 | 10.0 | 8.0 |
623 rows × 5 columns
corr = to_correlate.corr()
corr
Москва | Санкт-Петербург | Московская обл. | Чукотский АО | Ненецкий АО | |
---|---|---|---|---|---|
Москва | 1.000000 | 0.678963 | 0.691879 | 0.357444 | 0.349546 |
Санкт-Петербург | 0.678963 | 1.000000 | 0.724287 | 0.576447 | 0.511084 |
Московская обл. | 0.691879 | 0.724287 | 1.000000 | 0.680546 | 0.639753 |
Чукотский АО | 0.357444 | 0.576447 | 0.680546 | 1.000000 | 0.757208 |
Ненецкий АО | 0.349546 | 0.511084 | 0.639753 | 0.757208 | 1.000000 |
fig = px.imshow(corr, zmin=-1, zmax=1)
fig