Эпидемиологическая статистика

Рассмотрим онлайн-приложение https://epistat.wiv-isp.be .

Изучите явления:

  1. Сезонность - Аденовирус, пневмония, хламидия, параинфлюенза.

  2. Согласие в 2014 между динамикой бореллии и ротавируса.

  3. Влияние фактора возраста - аденовирус, ВИЧ, пневмония.

Набор данных, которыми оперирует приложение [MDL+16], содержит колонки:

  • Age возраст пациента (в годах);

  • DateMonday дата понедельника недели, когда делали запись;

  • Gender пол пациента;

  • Subject категория заболевания (возбудитель);

  • NUTS2 административное подразделение Бельгии, где сделали запись.

Каждая строчка в наборе данных - это запись о заболевании конкретного человека. Т.е. по одной записи можно установить, когда и с кем случилась неприятность. В некоторых случаях это серьезные заболевания вроде СПИД.

Однако, при анализе нас интересует количество случаев заболеваний за период времени в том или ином районе среди той или иной части населения. Чтобы получать такие числа мы должны правильно отбирать строки в таблице и затем просто считать количество строк в полученных выборках.

Эпидемиология - наука на стыке микробиологии и социологии.

Загрузим набор данных и попробуем автоматизировать некоторые задачи.

D = pd.read_csv("d/epistat.csv")  #https://epistat.wiv-isp.be/data/public_cases.csv")
D.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 295320 entries, 0 to 295319
Data columns (total 5 columns):
 #   Column      Non-Null Count   Dtype  
---  ------      --------------   -----  
 0   Age         289056 non-null  float64
 1   DateMonday  295320 non-null  object 
 2   Gender      295320 non-null  object 
 3   Subject     295320 non-null  object 
 4   NUTS2       282641 non-null  float64
dtypes: float64(2), object(3)
memory usage: 11.3+ MB

Как видно из количества ненулевых значений в наборе данных есть незначительное количество пропусков в данных о возрасте и регионе.

D.Age.hist(bins=30);
xlabel('Возраст');
_images/i_epistat_9_0.png

На гистограмме выделяются две возрастные группы, подверженные инфекционным заболеваниям - дети до 7 лет и молодежь после 20.

D.Gender.value_counts()
M      151441
F      140440
U        2316
UNK      1123
Name: Gender, dtype: int64

Пол хоть и указан во всех записях, но обозначения U и UNK вставлены на местах отсутствующих значений.

D.loc[D.Gender.isin(['U','UNK']), 'Gender'] = NaN
D.Gender.value_counts()
M    151441
F    140440
Name: Gender, dtype: int64

Информация о возбудителях закодирована коротким кодом.

D.Subject.value_counts()
V_RSV     65945
CAM_SP    54545
CHLTRA    34782
SALM      26764
V_RTV     17332
V_ADV     12355
BRRBUR    12283
GIA_SP    10109
HIV        9006
V_PIV      8412
PNEU       8291
NEIGON     7727
V_HCV      7596
CRS_SP     3187
BORPER     2998
SHIG       2941
STRPYO     2612
YERENT     2211
ENTHIS     1555
V_HAV      1302
HAEINF      839
V_HTV       668
LIS_SP      579
MENI        558
VTEC        510
CHIK        135
CHLPSI       77
ZIKA          1
Name: Subject, dtype: int64

Преобладают простудные и кишечные инфекции. Странный единичный случай вируса Зика.

D[D.Subject=='ZIKA']
Age DateMonday Gender Subject NUTS2
247437 27.0 2015-12-07 F ZIKA 31.0

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

Создадим словарик соответствия кода и латинского названия. Структура данных dict используется для удобного доступа к множеству значений по ключу (коду).

kod_spec = {
    'BORPER':'Bordetella pertussis',
    'BRRBUR':'Borrelia burgdorferi',
    'CAM_SP':'Campylobacter sp.',
    'CHIK  ':'Chikungunya virus',
    'CHLPSI':'Chlamydia psittaci',
    'CHLTRA':'Chlamydia trachomatis',
    'CRS_SP':'Cryptosporidium',
    'ENTHIS':'Entamoeba histolytica',
    'GIA_SP':'Giardia sp.',
    'HAEINF':'Haemophilus influenzae',
    'HIV   ':'Lentivirus "HIV"',
    'LIS_SP':'Listeria monocytogenes',
    'MENI  ':'Neisseria meningitidis',
    'NEIGON':'Neisseria gonorrhoeae',
    'PNEU  ':'Streptococcus pneumoniae',
    'SALM  ':'Salmonella',
    'SHIG  ':'Shigella',
    'STRPYO':'Streptococcus pyogenes',
    'VTEC  ':'Escherichia coli "VTEC"',
    'V_ADV ':'Adenovirus',
    'V_HAV ':'Hepatitis A virus',
    'V_HCV ':'Hepatitis C virus',
    'V_HTV ':'Hantavirus',
    'V_PIV ':'Parainfluenza',
    'V_RSV ':'Respiratory Syncytial Virus',
    'V_RTV ':'Rotavirus',
    'YERENT':'Yersinia enterocolitica',
    'ZIKA  ':'Flavivirus "ZIKA"',
}
# пробелы проще вычистить в цикле
kod_spec = {k.strip():v for k,v in kod_spec.items()}
{'BORPER': 'Bordetella pertussis',
 'BRRBUR': 'Borrelia burgdorferi',
 'CAM_SP': 'Campylobacter sp.',
 'CHIK': 'Chikungunya virus',
 'CHLPSI': 'Chlamydia psittaci',
 'CHLTRA': 'Chlamydia trachomatis',
 'CRS_SP': 'Cryptosporidium',
 'ENTHIS': 'Entamoeba histolytica',
 'GIA_SP': 'Giardia sp.',
 'HAEINF': 'Haemophilus influenzae',
 'HIV': 'Lentivirus "HIV"',
 'LIS_SP': 'Listeria monocytogenes',
 'MENI': 'Neisseria meningitidis',
 'NEIGON': 'Neisseria gonorrhoeae',
 'PNEU': 'Streptococcus pneumoniae',
 'SALM': 'Salmonella',
 'SHIG': 'Shigella',
 'STRPYO': 'Streptococcus pyogenes',
 'VTEC': 'Escherichia coli "VTEC"',
 'V_ADV': 'Adenovirus',
 'V_HAV': 'Hepatitis A virus',
 'V_HCV': 'Hepatitis C virus',
 'V_HTV': 'Hantavirus',
 'V_PIV': 'Parainfluenza',
 'V_RSV': 'Respiratory Syncytial Virus',
 'V_RTV': 'Rotavirus',
 'YERENT': 'Yersinia enterocolitica',
 'ZIKA': 'Flavivirus "ZIKA"'}

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

species = pd.Series(kod_spec)
species
BORPER           Bordetella pertussis
BRRBUR           Borrelia burgdorferi
CAM_SP              Campylobacter sp.
CHIK                Chikungunya virus
CHLPSI             Chlamydia psittaci
CHLTRA          Chlamydia trachomatis
CRS_SP                Cryptosporidium
ENTHIS          Entamoeba histolytica
GIA_SP                    Giardia sp.
HAEINF         Haemophilus influenzae
HIV                  Lentivirus "HIV"
LIS_SP         Listeria monocytogenes
MENI           Neisseria meningitidis
NEIGON          Neisseria gonorrhoeae
PNEU         Streptococcus pneumoniae
SALM                       Salmonella
SHIG                         Shigella
STRPYO         Streptococcus pyogenes
VTEC          Escherichia coli "VTEC"
V_ADV                      Adenovirus
V_HAV               Hepatitis A virus
V_HCV               Hepatitis C virus
V_HTV                      Hantavirus
V_PIV                   Parainfluenza
V_RSV     Respiratory Syncytial Virus
V_RTV                       Rotavirus
YERENT        Yersinia enterocolitica
ZIKA                Flavivirus "ZIKA"
dtype: object

Если заглянуть в исходный код веб-страницы (нажать Ctrl-U на сайте Epistat), то можно найти много других расшифровок кодовых обозначений в таблице.

Определите, кто скрывается за аббревиатурами:

  • TUBECULT (неужели культ ютуба?)

  • TREPAL

  • HANTA

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

s='''case 10:
          e.province = 'Brussels';
          break;
        case 21:
          e.province = 'Antwerp';
          break;
        case 22:
          e.province = 'Limburg';
          break;
        case 23:
          e.province = 'East Flanders';
          break;
        case 24:
          e.province = 'Flemish Brabant';
          break;
        case 25:
          e.province = 'West Flanders';
          break;
        case 31:
          e.province = 'Walloon Brabant';
          break;
        case 32:
          e.province = 'Hainaut';
          break;
        case 33:
          e.province = 'Liege';
          break;
        case 34:
          e.province = 'Luxembourg';
          break;
        case 35:
          e.province = 'Namur';
          break;
        default:
          e.province = 'Unknown'
      }'''
tutu = re.findall(r'case (?P<code>\d+):.+?\.province = \'(?P<name>[\w ]+?)\'', s, re.S)
tutu
[('10', 'Brussels'),
 ('21', 'Antwerp'),
 ('22', 'Limburg'),
 ('23', 'East Flanders'),
 ('24', 'Flemish Brabant'),
 ('25', 'West Flanders'),
 ('31', 'Walloon Brabant'),
 ('32', 'Hainaut'),
 ('33', 'Liege'),
 ('34', 'Luxembourg'),
 ('35', 'Namur')]

Пары значений NUTS2-провинция мы можем оформить в виде таблички.

A = pd.Series(dict(tutu), name='Province').to_frame()
A.index = A.index.astype(int)
A
Province
10 Brussels
21 Antwerp
22 Limburg
23 East Flanders
24 Flemish Brabant
25 West Flanders
31 Walloon Brabant
32 Hainaut
33 Liege
34 Luxembourg
35 Namur

На всякий случай можем добавить численность населения провинций.

s='''switch (e.NUTS2) {
        case 10:
          e.incidence = 100000 / 1119088;
          break;
        case 21:
          e.incidence = 100000 / 1764773;
          break;
        case 22:
          e.incidence = 100000 / 844621;
          break;
        case 23:
          e.incidence = 100000 / 1445831;
          break;
        case 24:
          e.incidence = 100000 / 1086446;
          break;
        case 25:
          e.incidence = 100000 / 1164967;
          break;
        case 31:
          e.incidence = 100000 / 382866;
          break;
        case 32:
          e.incidence = 100000 / 1317284;
          break;
        case 33:
          e.incidence = 100000 / 1077203;
          break;
        case 34:
          e.incidence = 100000 / 271352;
          break;
        case 35:
          e.incidence = 100000 / 476835;
          break;
        default:
          e.incidence = 0.1
'''
tutu = re.findall(r'case (?P<code>\d+):.+?\.incidence = 100000 / (?P<num>\d+)', s, re.S)
popu = pd.Series(dict(tutu), name='Population')
popu.index = popu.index.astype(int)

A = A.join(popu)
A
Province Population
10 Brussels 1119088
21 Antwerp 1764773
22 Limburg 844621
23 East Flanders 1445831
24 Flemish Brabant 1086446
25 West Flanders 1164967
31 Walloon Brabant 382866
32 Hainaut 1317284
33 Liege 1077203
34 Luxembourg 271352
35 Namur 476835
D.NUTS2.unique()
array([32., 23., 22., 21., 10., 24., 31., nan, 33., 25., 35., 34.])
D.NUTS2 = D.NUTS2.fillna(0).astype(int)
A.loc[0, 'Province'] = '-'

Подготовка данных про инфекционные заболевания

Для удобства сделаем колонки для отбора по годам и по месяцам.

D['Y'] = D.DateMonday.str.slice(0,4).astype(int)
D.Y.value_counts()
2015    42679
2012    40957
2013    40794
2014    39773
2011    37556
2008    31695
2010    31421
2009    30445
Name: Y, dtype: int64
D['YM'] = D.DateMonday.str.slice(0,7)

D['M'] = D.DateMonday.str.slice(5,7).astype(int)
D.M.value_counts().sort_index()
1     24352
2     20684
3     24863
4     20825
5     19795
6     21279
7     17888
8     20329
9     20575
10    22538
11    39180
12    43012
Name: M, dtype: int64

Для удобства можно добавить новые колонки с латинскими названиями возбудителей и названиями провинций.

D['Species'] = D.Subject.apply(lambda code: species[code])
D['Province'] = D.NUTS2.apply(lambda code: A.loc[code,'Province'])

Анализ пустот

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

Чувствительные данные, относящиеся к самим пациентам, могут отсутствовать умышленно из соображений анонимности при сборе данных. Колонки возраст, пол и провинция могут косвенно указывать на носителей заболевания.

Посмотрим, для каких возбудителей чаще всего не указана провинция.

D[D['Province']=='-'].Species.value_counts()
Salmonella                     2999
Lentivirus "HIV"               2369
Campylobacter sp.              1484
Respiratory Syncytial Virus    1453
Chlamydia trachomatis           940
Rotavirus                       498
Giardia sp.                     404
Hepatitis C virus               342
Borrelia burgdorferi            334
Adenovirus                      328
Neisseria gonorrhoeae           287
Shigella                        273
Parainfluenza                   204
Entamoeba histolytica           152
Streptococcus pneumoniae        110
Bordetella pertussis             95
Cryptosporidium                  86
Streptococcus pyogenes           76
Yersinia enterocolitica          63
Hepatitis A virus                47
Listeria monocytogenes           41
Haemophilus influenzae           30
Hantavirus                       29
Escherichia coli "VTEC"          22
Neisseria meningitidis           11
Chikungunya virus                 1
Chlamydia psittaci                1
Name: Species, dtype: int64

Интересно, много случаев СПИД без указания провинции. Еще больше сальмонеллёза. Давайте рассчитаем процент «скрытых» адресов относительно общей численности записей по каждой инфекции.

(D[D['Province']=='-'].Species.value_counts() / D.Species.value_counts()).sort_values() * 100
Chikungunya virus               0.740741
Chlamydia psittaci              1.298701
Streptococcus pneumoniae        1.326740
Neisseria meningitidis          1.971326
Respiratory Syncytial Virus     2.203351
Parainfluenza                   2.425107
Adenovirus                      2.654796
Cryptosporidium                 2.698463
Chlamydia trachomatis           2.702547
Borrelia burgdorferi            2.719205
Campylobacter sp.               2.720689
Yersinia enterocolitica         2.849389
Rotavirus                       2.873298
Streptococcus pyogenes          2.909648
Bordetella pertussis            3.168779
Haemophilus influenzae          3.575685
Hepatitis A virus               3.609831
Neisseria gonorrhoeae           3.714249
Giardia sp.                     3.996439
Escherichia coli "VTEC"         4.313725
Hantavirus                      4.341317
Hepatitis C virus               4.502370
Listeria monocytogenes          7.081174
Shigella                        9.282557
Entamoeba histolytica           9.774920
Salmonella                     11.205350
Lentivirus "HIV"               26.304686
Flavivirus "ZIKA"                    NaN
Name: Species, dtype: float64

Действительно, чаще всего фиксировались без указания адреса случаи заражения ВИЧ (HIV) - 26% от всей статистики.

А что с возрастом инфицированных ВИЧ в Бельгии?

X = D[(D.Subject=='HIV') & (D.Age.notnull())]

sns.kdeplot(X[X.Province == '-'].Age, label='Без адреса')
sns.kdeplot(X[X.Province != '-'].Age, label='Провинция указана');
_images/i_epistat_44_0.png

Большинство случаев заражения ВИЧ в детском возрасте (до 15 лет) в базе без указания адреса. В остальном распределения примерно совпадают.

А при каких заболеваниях чаще не указывали возраст?

(D[D.Age.isnull()].Species.value_counts() / D.Species.value_counts()).sort_values(ascending=False) * 100
Salmonella                     9.247497
Shigella                       3.264196
Neisseria gonorrhoeae          2.588327
Respiratory Syncytial Virus    2.312533
Chlamydia trachomatis          1.811282
Entamoeba histolytica          1.672026
Streptococcus pneumoniae       1.616210
Cryptosporidium                1.506119
Bordetella pertussis           1.501001
Neisseria meningitidis         1.433692
Hepatitis A virus              1.382488
Escherichia coli "VTEC"        1.176471
Giardia sp.                    1.147492
Hepatitis C virus              0.974197
Adenovirus                     0.874140
Listeria monocytogenes         0.863558
Borrelia burgdorferi           0.854840
Yersinia enterocolitica        0.814111
Campylobacter sp.              0.790173
Haemophilus influenzae         0.715137
Rotavirus                      0.675052
Streptococcus pyogenes         0.421133
Lentivirus "HIV"               0.410837
Hantavirus                     0.299401
Parainfluenza                  0.273419
Chikungunya virus                   NaN
Chlamydia psittaci                  NaN
Flavivirus "ZIKA"                   NaN
Name: Species, dtype: float64

Сальмонеллез - кишечная инфекция. Ничего особенного.

Динамика

При рассмотрении динамики заболеваемости во времени важно четко представлять иерархичность циклов, которыми оперируют эпидемиологи (как и другие люди).

Глобальные тренды оценивают по годам. Внутри года смотрят помесячную динамику. В данном наборе минимальный срок - неделя, которая имеет четкую длительность - 7 дней.

Проблема осложняется тем, что в месяце нет четкого количества недель, потому что количество дней в разных месяцах варьирует. Для правильного пересчета значений в иные периодические шкалы нужно использовать календарь. Функционал пересчета дат и периодов из одной шкалы в другую поддерживается в библиотеке pandas.

# создадим новую колонку, которая будет содержать объекты типа `datetime64`, со значениями, 
# дублирующими колонку DateMonday, которая содержит строки из 10 символов.
D['W'] = pd.to_datetime(D['DateMonday'])
# Чтобы легко было суммировать случаи заболевания, добавим в каждую строку по единице.
D['n']= 1

# Теперь полученную колонку сделаем индексом
D.set_index('W', drop=False, inplace=True)
# Индекс преобразуем в период - вместо одного дня понедельника будет диапазон из 7 дней
D = D.to_period('W', copy=False)
D.index.name = 'Time'

При трактовке динамики полезно понимать к какому типу инфекции относится то или иное заболевание.

Эпидемиологические вспышки напрямую зависят от способа заражения. А он принципиально разный для:

  • простудно-легочных заболеваний (Bordetella, Streptococcus, Haemophilus)

  • кишечных расстройств (Shigella, Salmonella, Giardia)

  • ЗППП (Neisseria gonorrhoeae, Chlamydia trachomatis)

  • в общем безвредных паразитов, которые в редких случаях переходят в тяжелые системные заболевания (дизентерийная амёба, ВИЧ, Borrelia, Listeria, Neisseria meningitidis)

Предсказуемость динамики заболеваемости осложняется тем, что безобидный микроорганизм, который легко распознается и ликвидируется иммунными клетками, в отдельных случаях приводит к заболеванию. Самый яркий пример - детские инфекции. Несмотря на то, что у новорожденных сильный иммунитет, часть которого приобретена пассивно через плаценту, при встрече с некоторыми микропаразитами врожденный иммунитет бессилен. А значит каждому ребенку «надо переболеть» ротавирусной (понос) или аденовирусной (сопли) инфекциями.

Сезонная динамика

Восприимчивость к патогенам зависит от погоды: зимой легче заболевают одними инфекционными болезнями, летом - другими.

X = D[D.Species=='Rotavirus']

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

X.groupby('W').n.count().plot();
_images/i_epistat_57_0.png

Так в колонке n одни единички, можно вместо подсчета использовать сумму. График будет тот же самый.

X.groupby('W').n.sum().plot();
_images/i_epistat_59_0.png

Для наложения графиков годовой динамики один поверх другого нужно сделать то же самое, но для выборки по каждому году. При итерации групп в цикле мы на каждом шаге получаем имя группы и выборку строк для этой группы.

for glabel,g in X.groupby('Y'):
    # группируем по неделям и берем суммарные значения
    vv = g.groupby('W').n.sum().values    
    plot(vv, label=glabel)

xlabel('Номер недели')
ylabel('Заболеваний / нед')
legend();
_images/i_epistat_61_0.png

Четко виден резкий рост заболеваемости на 5-10 неделях года.

Для более гладкой динамики можно сгруппировать по месяцам.

for glabel,g in X.groupby('Y'):
    # группируем по неделям и берем суммарные значения
    vv = g.groupby('M').n.sum()#.values
    
    plot(vv, label=glabel)

xlabel('Месяц')
ylabel('Заболеваний / мес')
legend();
_images/i_epistat_63_0.png

Напомним возрастной состав заболевших ротавирусной инфекцией.

X.Age.hist(bins=50);
_images/i_epistat_65_0.png

Маленькие дети и несколько глубоких старичков.

Рассмотрим детальней каждую категорию с учетом пола. При наложении нескольких распределений удобно строить ядерные оценки плотности (KDE, Kernel Density Estimation).

X_ = X[X.Age>60]
sns.kdeplot(X_[X_.Gender == 'M'].Age, label='M', color='cyan');
sns.kdeplot(X_[X_.Gender == 'F'].Age, label='F', color='coral');
_images/i_epistat_67_0.png

Если строить классические гистограммы, то полупрозрачность дает возможность легко обнаруживать преобладание той или другой группы.

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

X_ = X[X.Age<20]
classes = arange(16)
hist(X_[X_.Gender == 'M'].Age, label='M', color='cyan', alpha=.5, bins=classes);
hist(X_[X_.Gender == 'F'].Age, label='F', color='coral',alpha=.5, bins=classes);
xticks(classes+.5, classes)
legend();
_images/i_epistat_69_0.png

А старички также болеют весной?

Наложим нормализованную динамику для двух возрастных групп.

X_ = X[X.Age>60]
(X_.groupby('W').n.sum() / X_.n.sum()).plot(label='старички');
X_ = X[X.Age<8]
(X_.groupby('W').n.sum() / X_.n.sum()).plot(label='груднички');
legend();
_images/i_epistat_71_0.png

Да, тоже по весне, но не так регулярно из-за малого количества случаев.

Годовая динамика

Почему же все-таки в 2014 году была аномалия в заболеваемости ротавирусной инфекцией? Почему детки из северной Бельгии в марте 2014 болели мало, а переболели только в следующем 2015 году?

Чтобы ответить на этот вопрос удобно рассмотреть изменения между годами независимо от изменений внутри года. Расположим оси смены лет и смены месяцев перпендикулярно, как это мы уже делали для визуализации температурных изменений.

# momo = [datetime.date.strftime(x,'%b') for x in pd.DatetimeIndex(start='2000-01', end='2001-01', freq='M')]
momo = [datetime.date.strftime(x,'%b') for x in pd.date_range(start='2000-01', end='2001-01', freq='M')]

N = X.groupby(['Y','M']).n.count().unstack()
N.columns=momo
sns.heatmap(N, cmap='Blues');
_images/i_epistat_75_0.png

Группировка по двум осям является типичной операцией, поэтому есть специальный метод - на русский переводится как «сводная таблица».

N = X.pivot_table('n', 'Y', 'M', 'sum')
N.columns=momo
sns.heatmap(N, cmap='Blues');
_images/i_epistat_77_0.png

Давайте посмотрим отдельно для грудничков и годовалых младенцев.

fig, ax = subplots(1,2, sharey=True, figsize=(12,4))

for iage, age in enumerate([0,1]):
    sca(ax[iage])
    N = X[X.Age==age].pivot_table('n', 'Y', 'M', 'sum')
    N.columns=momo
    sns.heatmap(N, cmap='Blues');
    title('Age = {}'.format(age))
_images/i_epistat_79_0.png

Паттерн не различался. Значит дело не в возрасте.

Посмотрим паттерн температур для Западной Фландрии с административным центром — городом Брюгге.

По координатам 51°00′ с. ш. 3°00′ в. д. можно найти данные метеорологических станций например с помощью сервиса http://climexp.knmi.nl/selectstation.cgi . Ближайшая станция в Лилле, Франция.

На исходных данных Рис. 25 видно отсутствие понижения среднемесячной температуры зимой 2014 года ниже 5 градусов по Цельсию.

Рис. 25 Среднемесячная температура в г. Лилль. Источник - http://climexp.knmi.nl/data/ta7015_2008:2018.png .

Действительно в начале 2014 года температура держалась выше, чем обычно на 2 градуса.

Отобразим эти данных в виде heatmap.

u = 'http://climexp.knmi.nl/data/ta7015.dat'
T = pd.read_table(u, delim_whitespace=True, comment='#', header=None, na_values=-999.9,
                 names=momo)
T.tail()
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
2015 3.8 3.5 6.8 10.4 13.1 16.2 18.8 19.1 13.7 10.6 10.1 9.5
2016 5.3 5.3 6.3 8.9 14.4 16.4 18.8 19.1 18.0 10.4 6.6 4.1
2017 1.6 6.2 9.7 9.6 15.6 19.4 19.3 18.5 14.5 13.6 7.0 4.9
2018 6.2 1.4 5.6 12.7 15.9 17.8 22.3 19.3 15.4 12.5 7.3 6.2
2019 3.5 6.6 9.0 10.9 12.6 18.2 20.1 NaN NaN NaN NaN NaN
fig, ax = subplots(1,2, sharey=True, figsize=(12,4))

sca(ax[0])
sns.heatmap(T.loc[2008:2015], cmap='RdBu_r', center=8)
title('Температура');


sca(ax[1])
N = X[X.Province.isin(['West Flanders','East Flanders'])].pivot_table('n', 'Y', 'M', 'sum')
N.columns=momo
sns.heatmap(N, cmap='Blues');
title('Заболеваемость ротавирусной инфекцией во Фландрии');
_images/i_epistat_85_0.png

В 2013 году была относительно холодная зима и март, что сопровождалось всплеском заболеваемости в марте и апреле. В 2014 году было необычно тепло и весеннее обострение прошло без мартовского пика.

Дабы выразить связь в численном виде посчитаем корреляцию между рядами для марта.

N['Mar'].plot(legend=False)
ylabel('Заболеваемость')
T.loc[2008:2015,'Mar'].plot(label='T', secondary_y=True);
ylabel('Температура')
legend();
_images/i_epistat_87_0.png
corrcoef(N['Mar'], T.loc[2008:2015,'Mar'])
array([[ 1.        , -0.36375115],
       [-0.36375115,  1.        ]])

Если мы сравниваем два ряда данных, то получаем четыре коэффициента, два из которых 1.0 (сам с собой всегда 1) и два - корреляция между рядами. В данном случае это заметная отрицательная корреляция.

Из рисунка понятно, что если мы ограничим диапазон лет после 2010 года, то отрицательная корреляция будет еще сильнее.

corrcoef(N.loc[2011:2015, 'Mar'], T.loc[2011:2015,'Mar'])[0,1]
-0.41637479283604817

В 2012 году тоже был теплый март, но перед ним был холодный февраль. Можно усреднить данные за несколько месяцев.

iiyear = slice(2011,2015)
iimo = ['Jan','Feb','Mar','Apr','May']

corrcoef(N.loc[iiyear, iimo[:-1]].mean(axis=1), T.loc[iiyear, iimo[1:]].mean(axis=1))[0,1]
-0.5529228215230547

Итого мы обнаружили сильную отрицательную корреляцию между температурой в начале весны и масштабом весенней вспышки ротавирусной инфекции среди детей.

Корреляция может быть между любыми сходными по размеру данными. Например, корреляция изменений температуры в марте с другими месяцами с начала 21-века. Для таблиц pandas есть удобный метод для этого.

C = T.loc[2000:2018].corr()
C
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
Jan 1.000000 0.293384 0.173692 0.271980 0.433983 -0.203485 -0.314160 -0.072938 -0.032642 -0.095994 -0.158574 0.233414
Feb 0.293384 1.000000 0.451556 0.254995 0.309534 0.026118 -0.573048 -0.173683 0.018472 -0.067392 0.130988 -0.034507
Mar 0.173692 0.451556 1.000000 0.176712 0.348536 0.408093 -0.510846 0.066309 -0.217353 -0.182912 0.070101 -0.059709
Apr 0.271980 0.254995 0.176712 1.000000 0.174350 0.169347 -0.172060 -0.071371 0.002518 0.053499 0.238175 0.083499
May 0.433983 0.309534 0.348536 0.174350 1.000000 0.178999 -0.109865 0.106526 -0.066111 -0.036186 0.042643 0.142281
Jun -0.203485 0.026118 0.408093 0.169347 0.178999 1.000000 0.280509 0.051595 0.128666 0.049547 -0.112241 -0.077268
Jul -0.314160 -0.573048 -0.510846 -0.172060 -0.109865 0.280509 1.000000 -0.087980 0.146808 0.228014 0.000477 -0.018975
Aug -0.072938 -0.173683 0.066309 -0.071371 0.106526 0.051595 -0.087980 1.000000 -0.287177 -0.506997 0.019322 0.154475
Sep -0.032642 0.018472 -0.217353 0.002518 -0.066111 0.128666 0.146808 -0.287177 1.000000 0.195307 0.138791 0.053726
Oct -0.095994 -0.067392 -0.182912 0.053499 -0.036186 0.049547 0.228014 -0.506997 0.195307 1.000000 -0.076999 0.049747
Nov -0.158574 0.130988 0.070101 0.238175 0.042643 -0.112241 0.000477 0.019322 0.138791 -0.076999 1.000000 0.566915
Dec 0.233414 -0.034507 -0.059709 0.083499 0.142281 -0.077268 -0.018975 0.154475 0.053726 0.049747 0.566915 1.000000

Чтобы единицы по диагонали не мешали восприятию рисунка, их и половину значений маскируют.

mask = np.triu(np.ones_like(C))
mask
array([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
       [0., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
       [0., 0., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
       [0., 0., 0., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
       [0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., 1.],
       [0., 0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1.],
       [0., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., 1.],
       [0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1.],
       [0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]])
sns.heatmap(C, cmap='RdBu_r', center=0, square=True, mask=mask);
_images/i_epistat_99_0.png

На пересечении месяцев между друг другом выделяются противоположности Июль и Февраль, а также Октябрь и Август. Сходную динамику (высокая положительная корреляция) демонстрируют Февраль-Март и Ноябрь-Декабрь. И, внезапно также Май и Январь.

Тот же прием для данных по годовой динамике заболеваемости ротавирусной инфекцией.

C = N.corr()
sns.heatmap(C, cmap='RdBu_r', center=0, square=True, mask=mask);
_images/i_epistat_102_0.png

Разложение на компоненты

X = D[(D.Subject=='CHLTRA')]
X.groupby('W').n.count().plot();
_images/i_epistat_104_0.png

На графике видны колебания, но при этом наблюдается тенденция к увеличению. Компоненты динамики (тренд, сезонная составляющая и др.) можно разделить специальными сезонными моделями из пакета statsmodels.

from statsmodels.tsa.seasonal import seasonal_decompose

y = X.groupby('W').Subject.count()
result=seasonal_decompose(y, model='multiplicable', period=52)  #52.1714

result.trend.plot();
_images/i_epistat_106_0.png

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

result.plot();
_images/i_epistat_108_0.png

Фазовые сдвиги

Корреляцию можно считать между любыми рядами данных.

В колонках таблицы рассчитаем помесячную динамику. Исключим редкие болезни с невыраженной динамикой. Чтобы подчеркнуть циклические колебания также уберем глобальный тренд.

После вычитания среднего примерно половина значений становится отрицательными. Т.е. значения - это не количество, а отклонение количества от среднего тренда.

N = D.groupby(['YM','Species']).n.count().unstack()
N = N.loc[:,N.sum()>1000].fillna(0)
N[:] = detrend(N.values, 'linear', axis=0)
N.index =pd.to_datetime(N.index).to_period('M')
ax = N.plot(legend=False, figsize=(10,5));
legend(bbox_to_anchor=(1.01, 1));
_images/i_epistat_111_0.png

Резко выделяется ярко выраженной годовой динамикой Респираторно-синцитиальный вирус, он же ортопневмовирус человека - возбудитель обычной простуды.

C = N.corr()
mask = np.triu(np.ones_like(C))

figure(figsize=(8,8))
sns.heatmap(C, cmap='RdBu_r', center=0, square=True, mask=mask);
_images/i_epistat_113_0.png

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

Если мы сдвигаем ряд с циклической динамикой на несколько шагов, то корреляция падает, затем, когда мы доходим до противофазы (попадаем пиком туда, где ямка), то корреляция становится отрицательной, затем опять повышается. Первый большой пик автокорреляционной функции соответствует периоду основных колебаний.

# pd.tools.plotting.autocorrelation_plot(N['Hepatitis C virus']); # old version
pd.plotting.autocorrelation_plot(N['Hepatitis C virus']);
_images/i_epistat_115_0.png
pd.plotting.autocorrelation_plot(N['Parainfluenza']);
_images/i_epistat_116_0.png
pd.plotting.autocorrelation_plot(N['Rotavirus']);
_images/i_epistat_117_0.png

Для расчета исходной корреляционной функции без построения рисунка используют соответствующую функцию.

vv = N['Rotavirus']
c = correlate(vv, vv, 'full')
plot(c);
vv.shape, c.shape
((96,), (191,))
_images/i_epistat_119_1.png

Поскольку мы можем двигать один ряд значений относительно другого и вперед и назад, то полная функция имеет удвоенную длину. На предыдущем готовом рисунке была показана только ее правая половина, причем нормализованная к общей мощности сигнала, чтобы не выходить за рамки диапазона от -1 до 1.

Давайте получим нормализованные значения корреляции.

c = c[len(c)//2:]
c = c / (vv.var()) / len(c)
plot(c); grid(True);
c.shape
(96,)
_images/i_epistat_121_1.png

Вместо того, чтобы делить на дисперсию исходного сигнала, правильнее нормализовать входные ряды данных. Мы уже вычли тренд из динамики. Давайте теперь поделим каждую колонку на стандартное отклонение ее значений.

N = N / N.std()
ax = N.plot(legend=False, figsize=(10,5));
legend(bbox_to_anchor=(1.01, 1));
_images/i_epistat_123_0.png

Резко преобладающие по численности пики ортопневмовируса (RSV) теперь незаметны на фоне остальных.

Годовые циклы имеют лаг в 12 месяцев. Однако какой лаг между динамикой разных заболеваний? Например, ротавирусной и ортопневмовирусной инфекциями?

vv1 = N['Rotavirus']
vv2 = N['Respiratory Syncytial Virus']
c = correlate(vv1, vv2, 'full')
c = c[len(c)//2:]
c = c/len(c)
plot(c);
_images/i_epistat_125_0.png
c.max(), c.argmax()
(0.6241196119581467, 4)

Между рядами высокая корреляция - 0.62, но сезонный пик в первом ряду значений сдвинут на 4 месяца относительно сезонного пика во втором ряду значений.

vv1.plot()
vv2.plot()
legend();
_images/i_epistat_128_0.png

Скачкообразная динамика

Некоторые болезни «привязаны» к некоторым странам, потому что там живут специфические переносчики каких-то из жизненных стадий паразитов.

Например, вирус chikungunya из Африки передается при укусах комаров, которые в Бельгии жить не должны. Это тропическая лихорадка. В Бельгии очень мало случаев. Посмотрим помесячную динамику.

Сделаем помесячный индекс, охватывающий все данные в наборе.

monthindex = pd.to_datetime(D.DateMonday.unique()).to_period('M').unique()
# monthindex = pd.PeriodIndex(start='2008-01', end='2015-12')
monthindex
PeriodIndex(['2008-01', '2008-02', '2008-03', '2008-04', '2008-05', '2008-06',
             '2008-07', '2008-08', '2008-09', '2008-10', '2008-11', '2008-12',
             '2009-01', '2009-02', '2009-03', '2009-04', '2009-05', '2009-06',
             '2009-07', '2009-08', '2009-09', '2009-10', '2009-11', '2009-12',
             '2010-01', '2010-02', '2010-03', '2010-04', '2010-05', '2010-06',
             '2010-07', '2010-08', '2010-09', '2010-10', '2010-11', '2010-12',
             '2011-01', '2011-02', '2011-03', '2011-04', '2011-05', '2011-06',
             '2011-07', '2011-08', '2011-09', '2011-10', '2011-11', '2011-12',
             '2012-01', '2012-02', '2012-03', '2012-04', '2012-05', '2012-06',
             '2012-07', '2012-08', '2012-09', '2012-10', '2012-11', '2012-12',
             '2013-01', '2013-02', '2013-03', '2013-04', '2013-05', '2013-06',
             '2013-07', '2013-08', '2013-09', '2013-10', '2013-11', '2013-12',
             '2014-01', '2014-02', '2014-03', '2014-04', '2014-05', '2014-06',
             '2014-07', '2014-08', '2014-09', '2014-10', '2014-11', '2014-12',
             '2015-01', '2015-02', '2015-03', '2015-04', '2015-05', '2015-06',
             '2015-07', '2015-08', '2015-09', '2015-10', '2015-11', '2015-12'],
            dtype='period[M]', freq='M')
X = D[D.Subject=='CHIK']
N = X.groupby(['YM']).M.count()
N
YM
2012-02     1
2012-04     4
2012-05     1
2012-07     2
2012-10     1
2012-12     1
2013-07     6
2013-10     1
2014-03     1
2014-04     1
2014-05     3
2014-06     8
2014-07     9
2014-08    16
2014-09    15
2014-10     8
2014-11     6
2014-12     7
2015-01     4
2015-02     5
2015-04     4
2015-05     2
2015-06     7
2015-07     5
2015-08     6
2015-09     2
2015-10     3
2015-11     5
2015-12     1
Name: M, dtype: int64

Теперь сделаем пустую серию с полным индексом и заполним те значения, которые мы посчитали.

S = pd.Series(0, index=monthindex)
ii = pd.to_datetime(N.index).to_period('m')
S[ii] = N.values
S.plot();
_images/i_epistat_135_0.png

Видно, что случаи болезни были единичными - возможно болезнь привозили туристы из поездок в теплые страны. Но с 2014 количество заболевших этой тропической лихорадкой резко возросло.

И этот график идеально совпадает с волной мигрантов, захлестнувшей Европу в 2014-2015 гг. Среди этих мигрантов и были носители вируса Чикунгунья.

Эффекты развития здравоохранения

На таком коротком временном отрезке (8 лет) трудно говорить о влиянии развития медицины, хотя безусловно оно есть.

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

Для демонстрации сделаем сводную таблицу динамики по годам, причем нормализуем ее, отняв среднее и разделив на стандартное отклонение.

N = D.pivot_table('n', 'Y', 'Species', 'sum').fillna(0)
N = (N - N.mean())/ N.std()
figure(figsize=(8,6))
sns.heatmap(N.T, cmap='coolwarm', center=0);
_images/i_epistat_139_0.png

Сильный «скачок» вниз наблюдался для заболеваний, вызванных Entamoeba histolytica и ВИЧ. После 2008 года только снижалась заболеваемость сальмонеллезом и гепатитом А. Как мы знаем значимыми изменениями часто считают те, что выходят за 2 «сигмы». Поскольку те значения, что мы получили после нормализации, как раз в сигмах, то для наглядности мы можем скрыть значения, не достигшие «уровня значимости».

@interact(porog=(0.5, 2.5))
def _fig(porog=1.96):
    #figure(figsize=(8,6))
    sns.heatmap(N.T, cmap='RdBu_r', center=0, mask=N.T.abs()<porog);
_images/i_epistat_141_0.png

Другой подход оценить отклонения - зафиксировать исходный нулевой уровень на базе начальных данных, например, до 2012 года. Тогда изменения от этого уровня в левой части рисунка будут нивелированы, в правой будут отражать относительные сдвиги в последующие годы.

N_ = N - N[N.index < 2013].mean()
figure(figsize=(8,6))
sns.heatmap(N_.T, cmap='RdBu_r', center=0);
title('Все сдвиги')
figure(figsize=(8,6))
sns.heatmap(N_.T, cmap='RdBu_r', center=0, mask=N_.T>0);
title('Только снижение');
_images/i_epistat_143_0.png _images/i_epistat_143_1.png

Примеры решения задач

Задача на прогноз по предыдущим значениям

По данным epistat постройте прогноз динамики распространения хламидиоза на неделю, начинающуюся c 2014-09-01, на основании предыдущих данных, используя метод скользящего среднего с окном в 4 нед. Ошибка в расчётах составит по сравнению с реальным уровнем?

X = D[D.Subject=='CHLTRA']
N = X.groupby('DateMonday').DateMonday.count()

nreal = N['2014-09-01']
nreal
114
nnprev = N[ (N.index < '2014-09-01') & (N.index > '2014-07-01') ]
nnprev
DateMonday
2014-07-07    107
2014-07-14     99
2014-07-21     78
2014-07-28    117
2014-08-04    113
2014-08-11     88
2014-08-18    120
2014-08-25    116
Name: DateMonday, dtype: int64
nprognoz = round(nnprev.iloc[-4:].mean())
nerror = nprognoz - nreal
nprognoz, nreal, nerror
(109, 114, -5)

Задача на линейную экстраполяцию

Вы моделируете динамику заболеваемости по годам с помощью экстраполяции
линейной регрессионной модели за предыдущие 8 лет.
В каком регионе Бельгии прогнозируется самый сильный относительный рост
заболеваемости респираторно-синцитиальной инфекции в 2016 году (по отношению к 2015 году)?
Выберите один ответ:
a. Hainaut
b. Walloon Brabant
c. West Flanders
d. Antwerp
ny = 8
X = D[D.Subject=='V_RSV']
N = X.pivot_table('W', 'Y', 'Province', 'count').fillna(0)
N
Province - Antwerp Brussels East Flanders Flemish Brabant Hainaut Liege Limburg Luxembourg Namur Walloon Brabant West Flanders
Y
2008 116 1129 1355 1066 591 453 169 433 173 343 143 1347
2009 77 1268 1022 969 876 237 191 347 212 112 164 1263
2010 114 1560 1195 1032 722 436 430 472 367 193 105 970
2011 375 1497 1354 1057 812 522 293 620 324 335 224 1225
2012 160 1740 1306 1466 987 621 766 691 383 462 257 1719
2013 227 1313 1365 1138 799 440 665 269 314 309 198 1261
2014 271 1270 1179 1075 692 383 297 282 343 288 83 1467
2015 113 1410 1194 1393 905 543 777 655 291 168 42 1678
#берем последние 8 лет
N = N.iloc[-ny:,:]

Потренируемся на одной колонке.

y = N.iloc[:,4]
x = arange(len(y))
plot(x,y,'o');

# добавим тренд
res = stats.linregress(arange(len(y)), y)
print(res)
xx = arange(ny+1)
yy = res.intercept + res.slope*xx
plot(xx,yy, '-r');

prognoz = yy[-1]
plot(xx[-1], yy[-1], 'ro');
LinregressResult(slope=20.047619047619047, intercept=727.8333333333334, rvalue=0.3859663952074691, pvalue=0.34497278078037247, stderr=19.561858008321124)
_images/i_epistat_154_1.png

Применим ко всем провинциям…

def _linreg(y):
    res = stats.linregress(arange(len(y)), y)
    return pd.Series(res, index=['slope', 'intercept', 'r', 'p', 'stderr'])

R = N.apply(_linreg, axis=0).T
R
slope intercept r p stderr
Province
- 12.773810 136.916667 0.307570 0.458644 16.133216
Antwerp 17.607143 1336.750000 0.221236 0.598520 31.685545
Brussels 1.428571 1241.250000 0.029092 0.945483 20.038632
East Flanders 42.214286 1001.750000 0.574132 0.136671 24.577063
Flemish Brabant 20.047619 727.833333 0.385966 0.344973 19.561858
Hainaut 17.511905 393.083333 0.371981 0.364204 17.840104
Liege 71.000000 200.000000 0.687845 0.059351 30.587501
Limburg 8.226190 442.333333 0.119869 0.777389 27.814631
Luxembourg 16.440476 243.333333 0.546954 0.160637 10.273005
Namur 1.547619 270.833333 0.033573 0.937098 18.808444
Walloon Brabant -9.523810 185.333333 -0.318219 0.442396 11.583115
West Flanders 56.000000 1170.250000 0.552907 0.155206 34.453413
prognoz = (R.intercept + R.slope*(ny)).astype(int)
prognoz = prognoz.clip(lower=0)
prognoz
Province
-                   239
Antwerp            1477
Brussels           1252
East Flanders      1339
Flemish Brabant     888
Hainaut             533
Liege               768
Limburg             508
Luxembourg          374
Namur               283
Walloon Brabant     109
West Flanders      1618
dtype: int32

Прирост относительно предыдущего значения…

last = N.iloc[-1,:]
rost = (prognoz - last) / last
rost
Province
-                  1.115044
Antwerp            0.047518
Brussels           0.048576
East Flanders     -0.038765
Flemish Brabant   -0.018785
Hainaut           -0.018416
Liege             -0.011583
Limburg           -0.224427
Luxembourg         0.285223
Namur              0.684524
Walloon Brabant    1.595238
West Flanders     -0.035757
dtype: float64
rost.idxmax()
'Walloon Brabant'
pname = 'Walloon Brabant'
plot(N.index, N[pname], 'bo')
plot(2016, prognoz[pname], 'ro');
_images/i_epistat_161_0.png

В 2015 было всего 42 случая, но экстраполяция снижающегося тренда дала на 2016 год значение 109, что больше примерно в полтора раза.

Задача на лаг в неделях

Каков лаг в неделях между сезонными колебаниями заболеваемости шигеллёза и аденовирусной инфекции?

X = D[D.Subject.isin(['SHIG','V_ADV'])]
N = X.groupby(['W','Subject']).n.sum().unstack().fillna(0)
N[:] = detrend(N.values, 'linear', axis=0)
N = N / N.std()
N.plot();
_images/i_epistat_164_0.png
a = N.iloc[:,0]
b = N.iloc[:,1]
    
c = np.correlate(a, b, 'full')
c = c[len(c)//2:]
c = c/len(c)
    
plot(c);
_images/i_epistat_165_0.png
365/7
52.142857142857146

Более подробно кросс-корреляционная функция на год (~52 недели).

plot(c);
xlabel('Лаг, нед')
ylabel('Корреляция')
xlim(0, 52);
_images/i_epistat_168_0.png
c.argmax()
30

Тридцать недель - это примерно 6 месяцев, т.е. простудное заболевание и кишечное отравление имеют пики зимой и летом.

Задача на выявление корреляций

С каким заболеванием максимально коррелирует число случаев хламидиоза в Антверпене за весь период, отраженный в наборе данных за период с 1990 по 2014

Один из хламидиозов - хламидиоз попугайчиков очень редкий. Наверно имеется в виду более распространенное ЗППП.

X = D[(D.Subject=='CHLTRA') & (D.Province == 'Antwerp')]
X.groupby('DateMonday').Subject.count().plot();
_images/i_epistat_173_0.png

Мы видим рост заболеваемости. С какими наборами данных нужно посчитать корреляцию? Нужно получить динамику для всех инфекций в этом регионе.

X = D[(D.Province == 'Antwerp')]


Y = pd.pivot_table(X, 'Province', 'DateMonday', 'Subject', aggfunc='count', fill_value=0)
Y.plot();
_images/i_epistat_175_0.png
C = Y.corr()
figure(figsize=(12,2))
# удаляем пересечение с одноименной колонкой, потому что корреляция с самим собой всегда 1.0.
_C = C.loc[['CHLTRA']].T.drop('CHLTRA')

sns.heatmap(_C.T, cmap='coolwarm', center=0);
_images/i_epistat_176_0.png
_C['CHLTRA'].idxmax()
'NEIGON'
species['NEIGON']
'Neisseria gonorrhoeae'
_C.loc['NEIGON']
Subject
CHLTRA    0.529694
Name: NEIGON, dtype: float64

Корреляция 53% с динамикой другого ЗППП - гонореей.

См.также