ЧАСТЬ 1. От одного теста к группе

Чем опасно?

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

К примеру при \(\alpha = 0.05\), из 1000 тестов мы получим примерно 50 результатов, говорящих о достоверных отличиях, даже выборки в действительности не различаются.

Плохо? Плохо!

ЧАСТЬ 2. Контролируем ошибки: путь FWER

Family-Wise Error Rate

Familywise error rate – допустимая частота ошибок I рода для всего семейства тестов.

Обычно задается достаточно жестко: например при тестировании лекарств \(FWER \le 0.05\) на всю батарею тестов. То есть вероятность совершить хотя бы одну ошибку I рода составляет 0.05.

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

Поправка Бонферрони

Исторически первый метод корректировки. Выполняется в один шаг.

Полученные в каждом тесте р-значения (\(p_i\)) нужно умножить на общее число тестов (m) и только после этого сравнить с критическим значением:

\(p_i * m \le \alpha\)

Например:

P-values (sorted): 0.005, 0.011, 0.02, 0.04, 0.13

m = 5; Significance level: 0.05

Corrected p-value: 0.005 * 5 = 0.025 < 0.05 Reject

Corrected p-value: 0.011 * 5 = 0.055 Don’t reject

Corrected p-value: 0.02 * 5 = 0.1 Don’t reject

Corrected p-value: 0.04 * 5 = 0.2 Don’t reject

Corrected p-value: 0.13 * 5 = 0.65 Don’t reject

Поправка Бонферрони

Плюс - теперь не так-то просто совершить хотя бы одну ошибку I рода на всю группу тестов.

Минус - выросли шансы совершить ошибку II рода, т.е. "потерять" реально существующие различия.

Поправка Хольма-Бонферрони

Более щадящий способ это метод Хольма-Бонферрони.

Процедура применения поправки пошаговая. Сначала мы сортируем p-values, полученные в тестах, в порядке возрастания и присваиваем каждой соответствующий ранг j от 1 до m.

Затем применяем поправку \(p_i * (m – j + 1)\), после чего сравниваем с уровнем значимости.

Поправка Хольма-Бонферрони

Например:

P-values (sorted): 0.005, 0.011, 0.02, 0.04, 0.13

m = 5; Significance level: 0.05

j = 1 Corrected p-value: 0.005 * (5 – 1 +1) = 0.025 < 0.05: Reject

j = 2 Corrected p-value: 0.011 * (5 – 2 + 1) = 0.044: Reject

j = 3 Corrected p-value: 0.02 * (5 – 3 + 1) = 0.06: Don’t reject

Дальше можно не проверять: все следующие \(p_j\) будут больше \(\alpha\).

j = 4 Corrected p-value: 0.04 * 2 = 0.08: Don’t reject

j = 5 Corrected p-value: 0.13 * 1 = 0.13: Don’t reject

ЧАСТЬ 3. Контролируем ошибки: путь FED

False Discovery Rate

Для случаев, когда нам важнее сохранить истинно-положительные результаты, чем не допустить ложно-положительных, можно регулировать False Discovery Rate.

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

Эта цифра обозначается как \(\gamma\) и обычно 10% или меньше считается вполне приемлемой величиной.

Метод Беньямини-Хохберга

Процедура Беньямини-Хохберга пошаговая. Начинаем опять с сортировки и придания рангов р-значениям.

Затем находим такое p-value с наибольшим рангом j чтобы \(p_j \le \gamma * (j/m)\)

Все тесты с рангами меньше j считаем значимыми.

Например:

P-values (sorted): 0.005, 0.011, 0.02, 0.04, 0.13

m = 5; Significance level: 0.05

j=1 0.005 < 0.1*(1/5) = 0.02 Reject

j=2 0.011 < 0.1*(2/5) = 0.04 Reject

j=3 0.02 < 0.1*(3/5) = 0.06 Reject

j=4 0.04 < 0.1*(4/5) = 0.08 Reject

j=5 0.13 > 0.1*(5/5) = 0.1 Don't reject

Метод Беньямини-Хохберга

Как видим, это либеральный метод. Да, здесь могут быть ложноположительные результаты, но мы к этому готовы. Главное – истинно-положительные результаты не пропали.

Множественные сравнения (t-тесты) в R

Case study 1: Fake gene expression

Прочитаем таблицу симулированных данных и посмотрим как они устроены:

expr <- read.table("data/fake_expression_samples.csv", header=T, sep = ",")
head(expr)
##   X gene_ID sample1 sample2
## 1 1       1    7.83    11.5
## 2 2       1   11.87    10.5
## 3 3       1    3.90    11.3
## 4 4       1    8.56    11.3
## 5 5       1    4.95    14.9
## 6 6       1    7.46    10.2
str(expr)
## 'data.frame':    150 obs. of  4 variables:
##  $ X      : int  1 2 3 4 5 6 7 8 9 10 ...
##  $ gene_ID: int  1 1 1 1 1 1 1 1 1 1 ...
##  $ sample1: num  7.83 11.87 3.9 8.56 4.95 ...
##  $ sample2: num  11.5 10.5 11.3 11.3 14.9 ...

Case study 1: Fake gene expression

Нам надо сравнить выборки 1 и 2 по каждому из 10 генов. Начнем с первого

t.test(expr$sample1[expr$gene_ID=="1"],
       expr$sample2[expr$gene_ID=="1"])
## 
##  Welch Two Sample t-test
## 
## data:  expr$sample1[expr$gene_ID == "1"] and expr$sample2[expr$gene_ID == "1"]
## t = -6, df = 20, p-value = 0.000002
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
##  -5.36 -2.67
## sample estimates:
## mean of x mean of y 
##      7.54     11.56

Case study 1: Fake gene expression

Нам понадобятся только p-value, поэтому хорошо если мы сможем ее извлечь и записать в новую переменную

t_result <- t.test(expr$sample1[expr$gene_ID=="1"],
                   expr$sample2[expr$gene_ID=="1"])
t_result$p.value
## [1] 0.00000198

Мы можем добиться того же самого и одной командой:

t1 <- t.test(expr$sample1[expr$gene_ID=="1"],
             expr$sample2[expr$gene_ID=="1"])$p.value
t1
## [1] 0.00000198

Case study 1: Fake gene expression

Теперь надо лишь выполнить эту же операцию для оставшихся девяти сравнений

t2 <- t.test(expr$sample1[expr$gene_ID=="2"],
             expr$sample2[expr$gene_ID=="2"])$p.value
t3 <- t.test(expr$sample1[expr$gene_ID=="3"],
             expr$sample2[expr$gene_ID=="3"])$p.value
t4 <- t.test(expr$sample1[expr$gene_ID=="4"],
             expr$sample2[expr$gene_ID=="4"])$p.value
t5 <- t.test(expr$sample1[expr$gene_ID=="5"],
             expr$sample2[expr$gene_ID=="5"])$p.value
t6 <- t.test(expr$sample1[expr$gene_ID=="6"],
             expr$sample2[expr$gene_ID=="6"])$p.value
t7 <- t.test(expr$sample1[expr$gene_ID=="7"],
             expr$sample2[expr$gene_ID=="7"])$p.value
t8 <- t.test(expr$sample1[expr$gene_ID=="8"],
             expr$sample2[expr$gene_ID=="8"])$p.value
t9 <- t.test(expr$sample1[expr$gene_ID=="9"],
             expr$sample2[expr$gene_ID=="9"])$p.value
t10 <- t.test(expr$sample1[expr$gene_ID=="10"],
              expr$sample2[expr$gene_ID=="10"])$p.value

Case study 1: Fake gene expression

Мы посчитали десять p-values для всех генов. Соберем их в один вектор и присвоим имена.

pvals <- c(t1,t2,t3,t4,t5,t6,t7,t8,t9,t10)
names(pvals) <- c("t1","t2","t3","t4","t5","t6","t7","t8","t9","t10")
pvals
##          t1          t2          t3          t4          t5 
## 0.000001985 0.008085760 0.004916486 0.035231583 0.000000128 
##          t6          t7          t8          t9         t10 
## 0.000038369 0.478963037 0.240548497 0.028976352 0.564333804

Сколько генов, достоверно меняющих экспрессию, мы нашли в "сырых" t-тестах?

sum(pvals <= 0.05)
## [1] 7

А теперь внесем поправки.

Case study 1: Fake gene expression

Поправки к p-values в R можно сделать при помощи функции p.adjust() Аргумент method задает тип поправки.

p_bonf <- p.adjust(pvals, method = "bonferroni")

У скольких генов экспрессия достоверно различается после поправки Бонферрони?

sum(p_bonf <= 0.05, na.rm = TRUE)
## [1] 4

После поправки Хольма?

p_holm <- p.adjust(pvals, method = "holm")
sum(p_holm <= 0.05, na.rm = TRUE)
## [1] 5

Case study 1: Fake gene expression

А сколько — после применения процедуры Беньямини-Хохберга?

p_bh <- p.adjust(pvals, method = "BH")
sum(p_bh <= 0.05, na.rm = TRUE)
## [1] 6

Так же можем посмотреть результаты каких именно тестов показали значимые различия:

names(p_bh[p_bh <= 0.05])
## [1] "t1" "t2" "t3" "t5" "t6" "t9"

Case study 2: Экскреция метаболитов и синдром Кушинга. Самостоятельная работа

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

Датасет Cushings содержит данные по объему выведения с мочой двух метаболитов стероидных гормонов.

Cushings <- read_excel("data/Cushings.xlsx", sheet = 1)
head(Cushings)
str(Cushings)
## Classes 'tbl_df', 'tbl' and 'data.frame':    27 obs. of  3 variables:
##  $ Tetrahydrocortisone: num  3.1 3 1.9 3.8 4.1 1.9 8.3 3.8 3.9 7.8 ...
##  $ Pregnanetriol      : num  11.7 1.3 0.1 0.04 1.1 0.4 1 0.2 0.6 1.2 ...
##  $ Type               : chr  "a" "a" "a" "a" ...

Case study 2: Экскреция метаболитов и синдром Кушинга.

Как видим, в таблице данных три переменные:

Tetrahydrocortisone – объем (мг в сутки) выведения тетрагидрокортизона

Pregnanetriol – объем (мг в сутки) выведения прегнантриола

Type – разновидности синдрома: a = adenoma, b = bilateral hyperplasia, c = carcinoma и u = unknown.

Case study 2: Экскреция метаболитов и синдром Кушинга.

Названия переменных слишком замысловатые, переназовем их покороче.

names(Cushings)[names(Cushings)=="Tetrahydrocortisone"] <- "Tetr"
names(Cushings)[names(Cushings)=="Pregnanetriol"] <- "Pre"

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

Решение. Tetrahydrocortisone

a.b <- t.test(Cushings$Tetr[Cushings$Type=="a"],
              Cushings$Tetr[Cushings$Type=="b"])$p.value
a.c <- t.test(Cushings$Tetr[Cushings$Type=="a"],
              Cushings$Tetr[Cushings$Type=="c"])$p.value
a.u <- t.test(Cushings$Tetr[Cushings$Type=="a"],
              Cushings$Tetr[Cushings$Type=="u"])$p.value
b.c <- t.test(Cushings$Tetr[Cushings$Type=="b"],
              Cushings$Tetr[Cushings$Type=="c"])$p.value
b.u <- t.test(Cushings$Tetr[Cushings$Type=="b"],
              Cushings$Tetr[Cushings$Type=="u"])$p.value
c.u <- t.test(Cushings$Tetr[Cushings$Type=="c"],
              Cushings$Tetr[Cushings$Type=="u"])$p.value

Tetr_pvals <- c(a.b, a.c, a.u, b.c, b.u, c.u)
names(Tetr_pvals) <- c("a.b", "a.c", "a.u", "b.c", "b.u", "c.u")

Решение. Tetrahydrocortisone

names(Tetr_pvals[Tetr_pvals <= 0.05])
## [1] "a.b" "a.u"
p_bonf <- p.adjust(Tetr_pvals, method = "bonferroni")
names(p_bonf[p_bonf <= 0.05])
## [1] "a.b"
p_holm <- p.adjust(Tetr_pvals, method = "holm")
names(p_holm[p_holm <= 0.05])
## [1] "a.b"
p_bh <- p.adjust(Tetr_pvals, method = "BH")
names(p_bh[p_bh <= 0.05])
## [1] "a.b"

Решение. Pregnanetriol

a_b <- t.test(Cushings$Pre[Cushings$Type=="a"],
              Cushings$Pre[Cushings$Type=="b"])$p.value
a_c <- t.test(Cushings$Pre[Cushings$Type=="a"],
              Cushings$Pre[Cushings$Type=="c"])$p.value
a_u <- t.test(Cushings$Pre[Cushings$Type=="a"],
              Cushings$Pre[Cushings$Type=="u"])$p.value
b_c <- t.test(Cushings$Pre[Cushings$Type=="b"],
              Cushings$Pre[Cushings$Type=="c"])$p.value
b_u <- t.test(Cushings$Pre[Cushings$Type=="b"],
              Cushings$Pre[Cushings$Type=="u"])$p.value
c_u <- t.test(Cushings$Pre[Cushings$Type=="c"],
              Cushings$Pre[Cushings$Type=="u"])$p.value

Pre_pvals <- c(a_b, a_c, a_u, b_c, b_u, c_u)
names(Pre_pvals) <- c("a_b", "a_c", "a_u", "b_c", "b_u", "c_u")

Решение. Pregnanetriol

names(Pre_pvals[Pre_pvals <= 0.05])
## [1] "b_c" "c_u"
p_bonf <- p.adjust(Pre_pvals, method = "bonferroni")
names(p_bonf[p_bonf <= 0.05])
## character(0)
p_holm <- p.adjust(Pre_pvals, method = "holm")
names(p_holm[p_holm <= 0.05])
## character(0)
p_bh <- p.adjust(Pre_pvals, method = "BH")
names(p_bh[p_bh <= 0.05])
## [1] "b_c" "c_u"

Take home messages

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