Metodę określaną w biologii i epidemiologii mianem „analizy przeżycia” nazywa się w naukach społecznych „analizą historii zdarzeń”. Dzisiejszą notatkę poświęcam szacowaniu modeli historii zdarzeń w R.
Dane wykorzystane w tej notatce zaczerpnąłem z ciekawego artykułu poświęconego związkom między analizą przeżycia a regresją logistyczną. Zanim jednak przejdę do części „właściwej” kilka słów wprowadzenia. Analiza historii zdarzeń to ogólna nazwa całej klasy narzędzi statystycznych zajmujących się opisem i modelowaniem czasu przed wystąpieniem zdarzenia. Typowym przykładem problemu, do którego stosuje się analizę historii zdarzeń, jest czas przeżycia pacjentów po operacji lub terapii. W przykładzie tym mamy więc grupę pacjentów cierpiących na pewną chorobę (np. raka płuc), którzy poddali się leczeniu (np. chemioterapii lub chemioterapii połączonej z zabiegiem chirurgicznym). Interesuje nas czas, jaki upłynie przed śmiercią pacjenta lub remisją choroby. Dla wszystkich pacjentów objętych ryzykiem śmierci lub nawrotu choroby możemy określić stopę ryzyka, \(h(t)\): \[h(t) = \lim_{\Delta t \rightarrow 0} \frac{P\left(t \leqslant T < t + \Delta t \vert T \geqslant t\right)}{\Delta t},\] gdzie \(T\) oznacza rzeczywisty czas wystąpienia zdarzenia (śmierci lub nawrotu choroby w naszym przykładzie), zaś \(t\) to moment, dla którego szacujemy stopę ryzyka. Mówiąc inaczej, mianownik w wyrażeniu powyżej określa prawdopodobieństwo warunkowe zajścia zdarzenia w pewnym niewielkim przedziale czasowym \(\Delta t\), o ile nie wystąpiło ono przed momentem czasowym \(t\).
Inną wielkością, która odgrywa istotną rolę w analizie historii zdarzeń, jest funkcja przeżycia, którą definiuje się w terminach prawdopodobieństwa przeżycia co najmniej do czasu \(t\): \[S(t) = P(T\geqslant t)\] Mówiąc nieco inaczej, dla każdego momentu czasowego \(t\) funkcja przeżycia określa odsetek badanej zbiorowości, dla którego analizowane zdarzenia jeszcze nie zaszło.
Jeśli cały okres objęty analizą podzielimy na mniejsze przedziały (np. dni lub tygodnie lub miesiące), dla każdego przedziału możemy określić trzy następujące wielkości:
Stopę ryzyka szacuje się za pomocą tych trzech wielkości w następujący sposób: \[\hat{h}(t) = \frac{d(t)}{r(t) - w(t)}\] Z kolei oszacowanie funkcji przeżycia \(S(t)\) ma postać: \[\hat{S}(t) = \left[1 - \hat{h}(1)\right] \cdot \left[1 - \hat{h}(2)\right] \cdots \left[1 - \hat{h}(t - 1)\right]\]
Oczywiście, analiza przeżywalności po zabiegu czy terapii to nie jedyne zastosowania analizy historii zdarzeń. Przykłady użycia analizy historii zdarzeń w naukach społecznych obejmują:
O innych zastosowaniach i własnościach analizy historii zdarzeń można poczytać w literaturze źródłowej, np. w bardzo przystępnie napisanej książce Paula D. Allison; autor ten przygotował zresztą wiele różnych opracowań dotyczących analizy historii zdarzeń, możne je wyszukać tutaj. Sporo ciekawych materiałów na temat analizy historii zdarzeń można także znaleźć na stronie brytyjskiej statystyczki Fiony Steele. Materiałów na temat samej metody i jej zastosowań — od prostych wprowadzeń dla początkujących badaczy po zaawansowane opracowania statystyczne dotyczące arkanów szacowania stopy przeżycia — jest zresztą mnóstwo.
Jak wspomniałem, dane, które wykorzystuję w dzisiejszej notatce, pochodzą z cytowanego artykułu Efrona. Mówiąc dokładnie, Efron wykorzystał w swoim artykule dane dotyczące 96 pacjentów chorych na nowotwory szyi i głowy, których poddano dwóch rodzajom terapii: radioterapii lub radioterapii wspieranej chemioterapią. Badaczy interesowała różnica między tymi dwoma rodzajami terapii pod względem przeżywalności pacjentów. Mówiąc inaczej, zdarzeniem, które analizowano w opisywanym badaniu, była śmierć pacjenta lub nawrót choroby. Dla każdego pacjenta określono czas do wystąpienia tego zdarzenia.
Źródłowe dane dotyczące przeżywalności pacjentów znajdują się na stronach 4 i 5 artykułu Efrona, pod tabelami 1 i 2. Dane te to po prostu zbiór wartości liczbowych określających, dla każdego z pacjentów osobno, liczbę dni przed wystąpieniem zdarzenia. Wartości te pokazano w okienku poniżej:
## [1] "7" "34" "42" "63" "64" "74+" "83" "84"
## [9] "91" "108" "112" "129" "133" "133" "139" "140"
## [17] "140" "146" "149" "154" "157" "160" "160" "165"
## [25] "173" "176" "185+" "218" "225" "241" "248" "273"
## [33] "277" "279+" "297" "319+" "405" "417" "420" "440"
## [41] "523" "523+" "583" "594" "1101" "1116+" "1146" "1226+"
## [49] "1349+" "1412+" "1417" " 37" "84" "92" "94" "110"
## [57] "112" "119" "127" "130" "133" "140" "146" "155"
## [65] "159" "169+" "173" "179" "194" "195" "209" "249"
## [73] "281" "319" "339" "432" "469" "519" "528+" "547+"
## [81] "613+" "633" "725" "759+" "817" "1092+" "1245+" "1331+"
## [89] "1557" "1642+" "1771+" "1776" "1897+" "2023+" "2146+" "2297+"
Jak widzimy, poszczególne wartości są otoczone cudzysłowami. Oznacza to, że program traktuje te wartości jako łańcuchy tekstowe, a nie jako wartości liczbowe. Wynika to stąd, że w niektórych przypadkach wartości kończą się znakiem +, który wskazuje na obserwację uciętą. Innymi słowy, elementy wektora pokazanego powyżej zawierają w sobie dwa rodzaje informacji: (a) o liczbie dni przed wystąpieniem zdarzenia i (b) o tym, czy przypadek jest ucięty. Przed przystąpieniem do analizy musimy więc rozbić wektor na dwa wektory „składowe”. Zaczniemy od wyodrębnienia wartości liczbowych informujących o czasie przed wystąpieniem zdarzenia. W kolejnych poleceniach zakładamy, że wektor pokazany w ostatnim okienku zapisaliśmy pod nazwą x.
Aby przekształcić wektor x do postaci liczbowej, wystarczy wyeliminować znak + z poszczególnych elementów tego wektora, albo zastąpić go znakiem „pustym”. Wykorzystamy do tego celu funkcję str_replace() z pakietu stringr przygotowanego z myślą o łatwiejszym przekształcaniu i przetwarzaniu łańcuchów tekstowych.
str_replace(string = x, pattern = "\\+", replacement = "")
## [1] "7" "34" "42" "63" "64" "74" "83" "84" "91" "108"
## [11] "112" "129" "133" "133" "139" "140" "140" "146" "149" "154"
## [21] "157" "160" "160" "165" "173" "176" "185" "218" "225" "241"
## [31] "248" "273" "277" "279" "297" "319" "405" "417" "420" "440"
## [41] "523" "523" "583" "594" "1101" "1116" "1146" "1226" "1349" "1412"
## [51] "1417" " 37" "84" "92" "94" "110" "112" "119" "127" "130"
## [61] "133" "140" "146" "155" "159" "169" "173" "179" "194" "195"
## [71] "209" "249" "281" "319" "339" "432" "469" "519" "528" "547"
## [81] "613" "633" "725" "759" "817" "1092" "1245" "1331" "1557" "1642"
## [91] "1771" "1776" "1897" "2023" "2146" "2297"
Podwójny ukośnik jest w ostatnim poleceniu potrzebny, ażeby program potraktował znak + jako znak, a nie jako operator arytmetyczny. Jak widzimy, polecenie zwraca nam wektor, którego elementy w dalszym ciągu są traktowane przez program jako tekst, ale nie zawierają już kłopotliwego plusa. Aby uzyskać wartości liczbowe, wstawmy ostatnie polecenie jako argument dla funkcji as.numeric():
as.numeric(str_replace(string = x, pattern = "\\+", replacement = ""))
## [1] 7 34 42 63 64 74 83 84 91 108 112 129 133 133
## [15] 139 140 140 146 149 154 157 160 160 165 173 176 185 218
## [29] 225 241 248 273 277 279 297 319 405 417 420 440 523 523
## [43] 583 594 1101 1116 1146 1226 1349 1412 1417 37 84 92 94 110
## [57] 112 119 127 130 133 140 146 155 159 169 173 179 194 195
## [71] 209 249 281 319 339 432 469 519 528 547 613 633 725 759
## [85] 817 1092 1245 1331 1557 1642 1771 1776 1897 2023 2146 2297
Uzyskaliśmy zatem zbiór wartości liczbowych mierzących czas przed wystąpieniem zdarzenia (lub przed ostatnią obserwacją w przypadku obserwacji uciętych). Aby uzyskać zmienną identyfikującą obserwacje ucięte, skorzystamy z funkcji str_detect() z pakietu stringr. Funkcja ta sprawdza, czy zadany znak występuje w danym ciągu znaków i zwraca wartość logiczną TRUE, jeśli tak. W naszym przykładzie, jeśli element wektora x zawiera znak +, element ten wskazuje na obserwację uciętą:
str_detect(string = x, pattern = "\\+")
## [1] FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE
## [12] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [23] FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
## [34] TRUE FALSE TRUE FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE
## [45] FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE FALSE FALSE FALSE
## [56] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE
## [67] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [78] FALSE TRUE TRUE TRUE FALSE FALSE TRUE FALSE TRUE TRUE TRUE
## [89] FALSE TRUE TRUE FALSE TRUE TRUE TRUE TRUE
Ostatnia rzecz. Jak pamiętamy, dane w naszym przykładzie dotyczą pacjentów poddanych dwóm rodzajom terapii: (a) radioterapii (nazwijmy ją „terapią A”) oraz (b) radioterapii wspieranej chemioterapią („terapia B”). Pierwszych 51 przypadku objęto terapią A, pozostałe 45 — terapią B. Zmienną określającą rodzaj terapii tworzymy następująco:
rep(LETTERS[1:2], times = c(51, 45))
## [1] "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A"
## [18] "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A"
## [35] "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A" "A"
## [52] "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B"
## [69] "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B"
## [86] "B" "B" "B" "B" "B" "B" "B" "B" "B" "B" "B"
Wszystkie trzy utworzone przez nas zmienne połączymy teraz w jednej ramce danych, którą nazwiemy df:
df <- data.frame(
months = as.numeric(str_replace(string = x, pattern = "\\+", replacement = ""))/30.348,
status = !str_detect(string = x, pattern = "\\+"),
treatment = rep(LETTERS[1:2], times = c(51, 45))
)
Kilka słów tytułem wyjaśnienia. Informację o czasie do wystąpienia zdarzenia zapisywano w badaniu opisywanym przez Efrona w dniach. Efron podzielił liczbę dni przez 30,3, czyli średnią długość miesiąca, aby czas mierzony w dniach zamienić na czas mierzony w miesiącach. Zmienna status określa, czy analizowane zdarzenie (a więc śmierć lub nawrót choroby) wystąpiło czy nie. Zmienną tę uzyskaliśmy przez negację logiczną utworzonej wcześniej zmiennej określającej przypadki ucięte. Mówiąc inaczej, jeśli dana jednostka jest przypadkiem uciętym, oznacza to, że zdarzenie w przypadku tej jednostki nie wystąpiło. A zatem „bycie przypadkiem uciętym” implikuje brak zdarzenia.
Podgląd pierwszych kilku wierszy uzyskanej ramki danych pokazano niżej:
## months status treatment
## 1 0.231 TRUE A
## 2 1.120 TRUE A
## 3 1.384 TRUE A
## 4 2.076 TRUE A
## 5 2.109 TRUE A
## 6 2.438 FALSE A
## 7 2.735 TRUE A
## 8 2.768 TRUE A
## 9 2.999 TRUE A
## 10 3.559 TRUE A
Do szacowania funkcji przeżycia i modelowania historii zdarzeń wykorzystuje się funkcje w pakiecie survival. Pierwszym krokiem w analizie przeżycia jest utworzenie „zmiennej zależnej”, czyli tego, co chcemy modelować. Pamiętajmy, że przy szacowaniu funkcji przeżycia sama tylko informacja o czasie do wystąpienia zdarzenia nie wystarczy; potrzebna jest nam jeszcze informacja o obserwacjach uciętych. Za pomocą tych dwóch informacji oraz funkcji Surv() tworzy się specjalny obiekt, który następnie wykorzystuje się jako „zmienną zależną” w modelach:
library(survival)
Surv(time = df$months, event = df$status)
## [1] 0.231 1.120 1.384 2.076 2.109 2.438+ 2.735 2.768
## [9] 2.999 3.559 3.691 4.251 4.382 4.382 4.580 4.613
## [17] 4.613 4.811 4.910 5.074 5.173 5.272 5.272 5.437
## [25] 5.701 5.799 6.096+ 7.183 7.414 7.941 8.172 8.996
## [33] 9.127 9.193+ 9.786 10.511+ 13.345 13.741 13.839 14.498
## [41] 17.233 17.233+ 19.210 19.573 36.279 36.773+ 37.762 40.398+
## [49] 44.451+ 46.527+ 46.692 1.219 2.768 3.032 3.097 3.625
## [57] 3.691 3.921 4.185 4.284 4.382 4.613 4.811 5.107
## [65] 5.239 5.569+ 5.701 5.898 6.393 6.425 6.887 8.205
## [73] 9.259 10.511 11.170 14.235 15.454 17.102 17.398+ 18.024+
## [81] 20.199+ 20.858 23.890 25.010+ 26.921 35.983+ 41.024+ 43.858+
## [89] 51.305 54.106+ 58.356+ 58.521 62.508+ 66.660+ 70.713+ 75.689+
W poleceniu powyżej, podaliśmy dwa argumenty dla funkcji Surv: time, który określa czas do wystąpienia zdarzenia; w naszym przypadku argumentowi temu odpowiada zmienna months z naszej ramki danych df. Z kolei argument event określa, czy zdarzenie wystąpiło; temu argumentowi odpowiada zmienna status. W najprostszym przypadku te dwa argumenty wystarczą. W przyszłości opiszę reguły tworzenia obiektów type survival dla bardziej złożonych przypadków.
Aby oszacować funkcję przeżycia metodą Kaplana-Meiera (opisaną w cytowanym artykule Efrona), użyjemy funkcji survfit:
fit_1 <- survfit(Surv(time = months, event = status) ~ treatment, data = df)
Jak widzimy, funkcja survfit ma składnię dość podobną do składni funkcji szacujących modele liniowe. Pierwszym argumentem jest formuła, drugim — nazwa zbioru danych. Po lewej stronie formuły znajduje się nasza „zmienna zależna”, czyli obiekt zawierający informacje potrzebne do oszacowania stopy ryzyka dla każdego momentu czasowego, po prawej stronie zaś — zmienne wyjaśniające. W naszym przypadku jest to tylko jedna zmienna, a mianowicie — rodzaj terapii. Użycie tej zmiennej w formule oznacza, że oszacowania funkcji przeżycia zostaną wykonane osobno dla obu gru pacjentów.
Wynik oszacowania pokazano w okienku poniżej:
## Call: survfit(formula = Surv(time = months, event = status) ~ treatment,
## data = df)
##
## treatment=A
## time n.risk n.event survival std.err lower 95% CI upper 95% CI
## 0.231 51 1 0.980 0.0194 0.9431 1.000
## 1.120 50 1 0.961 0.0272 0.9090 1.000
## 1.384 49 1 0.941 0.0329 0.8788 1.000
## 2.076 48 1 0.922 0.0376 0.8507 0.998
## 2.109 47 1 0.902 0.0416 0.8239 0.987
## 2.735 45 1 0.882 0.0453 0.7975 0.975
## 2.768 44 1 0.862 0.0485 0.7719 0.962
## 2.999 43 1 0.842 0.0513 0.7470 0.949
## 3.559 42 1 0.822 0.0539 0.7227 0.934
## 3.691 41 1 0.802 0.0562 0.6989 0.920
## 4.251 40 1 0.782 0.0582 0.6755 0.905
## 4.382 39 2 0.742 0.0618 0.6299 0.873
## 4.580 37 1 0.722 0.0633 0.6076 0.857
## 4.613 36 2 0.681 0.0658 0.5640 0.823
## 4.811 34 1 0.661 0.0668 0.5426 0.806
## 4.910 33 1 0.641 0.0678 0.5214 0.789
## 5.074 32 1 0.621 0.0685 0.5005 0.771
## 5.173 31 1 0.601 0.0692 0.4799 0.753
## 5.272 30 2 0.561 0.0701 0.4393 0.717
## 5.437 28 1 0.541 0.0704 0.4193 0.698
## 5.701 27 1 0.521 0.0706 0.3996 0.680
## 5.799 26 1 0.501 0.0707 0.3800 0.661
## 7.183 24 1 0.480 0.0708 0.3597 0.641
## 7.414 23 1 0.459 0.0707 0.3397 0.621
## 7.941 22 1 0.438 0.0705 0.3199 0.601
## 8.172 21 1 0.418 0.0702 0.3004 0.580
## 8.996 20 1 0.397 0.0697 0.2811 0.560
## 9.127 19 1 0.376 0.0691 0.2621 0.539
## 9.786 17 1 0.354 0.0685 0.2420 0.517
## 13.345 15 1 0.330 0.0678 0.2207 0.494
## 13.741 14 1 0.307 0.0670 0.1998 0.470
## 13.839 13 1 0.283 0.0658 0.1793 0.446
## 14.498 12 1 0.259 0.0644 0.1594 0.422
## 17.233 11 1 0.236 0.0627 0.1400 0.397
## 19.210 9 1 0.210 0.0610 0.1185 0.371
## 19.573 8 1 0.183 0.0587 0.0979 0.344
## 36.279 7 1 0.157 0.0559 0.0783 0.316
## 37.762 5 1 0.126 0.0528 0.0552 0.286
## 46.692 1 1 0.000 NaN NA NA
##
## treatment=B
## time n.risk n.event survival std.err lower 95% CI upper 95% CI
## 1.22 45 1 0.978 0.0220 0.936 1.000
## 2.77 44 1 0.956 0.0307 0.897 1.000
## 3.03 43 1 0.933 0.0372 0.863 1.000
## 3.10 42 1 0.911 0.0424 0.832 0.998
## 3.62 41 1 0.889 0.0468 0.802 0.986
## 3.69 40 1 0.867 0.0507 0.773 0.972
## 3.92 39 1 0.844 0.0540 0.745 0.957
## 4.18 38 1 0.822 0.0570 0.718 0.942
## 4.28 37 1 0.800 0.0596 0.691 0.926
## 4.38 36 1 0.778 0.0620 0.665 0.909
## 4.61 35 1 0.756 0.0641 0.640 0.892
## 4.81 34 1 0.733 0.0659 0.615 0.875
## 5.11 33 1 0.711 0.0676 0.590 0.857
## 5.24 32 1 0.689 0.0690 0.566 0.838
## 5.70 30 1 0.666 0.0704 0.541 0.819
## 5.90 29 1 0.643 0.0716 0.517 0.800
## 6.39 28 1 0.620 0.0727 0.493 0.780
## 6.43 27 1 0.597 0.0735 0.469 0.760
## 6.89 26 1 0.574 0.0742 0.446 0.740
## 8.20 25 1 0.551 0.0747 0.423 0.719
## 9.26 24 1 0.528 0.0750 0.400 0.698
## 10.51 23 1 0.505 0.0752 0.377 0.676
## 11.17 22 1 0.482 0.0752 0.355 0.655
## 14.23 21 1 0.459 0.0750 0.333 0.633
## 15.45 20 1 0.436 0.0747 0.312 0.610
## 17.10 19 1 0.413 0.0742 0.291 0.588
## 20.86 15 1 0.386 0.0742 0.265 0.562
## 23.89 14 1 0.358 0.0739 0.239 0.537
## 26.92 12 1 0.328 0.0735 0.212 0.509
## 51.30 8 1 0.287 0.0749 0.172 0.479
## 58.52 5 1 0.230 0.0789 0.117 0.451
Okienko zawiera dwie tabele, osobną dla każdej grupy pacjentów. Wartości w kolumnie z nagłówkiem survival to oszacowania funkcji przeżycia \(S(t)\).
Tabele te są jednak mało czytelne. Znacznie lepiej wygląda wykres sporządzony na podstawie tych tabel:
plot(fit_1, main = "Oszacowania funkcji przeżycia w dwóch grupach pacjentów\nw zależności od rodzaju terapii",
xlab = "Miesiące", ylab = "Prawdopodobieństwo przeżycia", col = c("navyblue",
"firebrick"), xlim = c(0, 80), las = 1)
grid()
legend("top", lty = 1, legend = c("Terapia A", "Terapia B"), col = c("navyblue",
"firebrick"), bty = "n", ncol = 2)
Widoczne na wykresie plusy identyfikują przypadki ucięte. Jeśli nie chcemy ich pokazywać, wystarczy dodać argument mark.time = FALSE do funkcji plot():
plot(fit_1, main = "Oszacowania funkcji przeżycia w dwóch grupach pacjentów\nw zależności od rodzaju terapii",
xlab = "Miesiące", ylab = "Prawdopodobieństwo przeżycia", col = c("navyblue",
"firebrick"), xlim = c(0, 80), las = 1, mark.time = FALSE)
grid()
legend("top", lty = 1, legend = c("Terapia A", "Terapia B"), col = c("navyblue",
"firebrick"), bty = "n", ncol = 2)