Kata Pengantar
Epidemiologi berasal dari kata epi (atas), demos (masyarakat), dan logos (ilmu). Secara etimologis, epidemiologi berarti ilmu yang mempelajari penyakit yang menimpa sekelompok masyarakat. Dalam perkembangannya, epidemiologi tidak hanya mempelajari penyakit menular, tetapi juga berbagai kondisi kesehatan lain yang berdampak pada populasi, termasuk penyakit tidak menular, kecelakaan, dan faktor risiko lingkungan maupun perilaku.
Ringkasnya: sejarah epidemiologi menunjukkan pergeseran paradigma dari pemahaman mistis menuju pendekatan ilmiah berbasis data.
Secara umum, epidemiologi didefinisikan sebagai ilmu yang mempelajari distribusi dan determinan masalah kesehatan dalam populasi, serta penerapan hasil kajiannya untuk mengendalikan masalah kesehatan.
Beberapa definisi penting:
- Hippokrates (400 SM): penyakit tidak muncul secara
acak, tetapi memiliki penyebab yang dapat ditelusuri dari lingkungan,
pola hidup, dan faktor host.
- MacMahon & Pugh (1970): studi distribusi penyakit
dalam populasi manusia serta faktor yang menentukan distribusinya.
- Last (1988): studi distribusi dan determinan
keadaan/kejadian kesehatan dalam populasi tertentu, serta penerapannya
untuk mengendalikan masalah kesehatan.
- CDC (2002): ilmu dasar kesehatan masyarakat yang
menyediakan metode ilmiah untuk menemukan penyebab, pencegahan, serta
dasar kebijakan kesehatan.
Dengan demikian, epidemiologi tidak hanya menghitung angka penyakit, tetapi juga menganalisis siapa, di mana, kapan, dan mengapa penyakit terjadi.
Kesimpulan: epidemiologi adalah fondasi kesehatan masyarakat, dengan ruang lingkup deskriptif, analitik, eksperimental, terapan, hingga bidang modern.
Epidemiologi disebut sebagai ilmu dasar kesehatan masyarakat (basic science of public health). Perannya meliputi:
Kesimpulan: epidemiologi sangat penting dalam semua aspek kesehatan masyarakat: identifikasi masalah, analisis risiko, perencanaan, surveilans, hingga kebijakan berbasis bukti.
Terjadinya penyakit pada manusia merupakan hasil interaksi antara agent, host, dan environment. Konsep ini dikenal sebagai epidemiologic triad atau segitiga epidemiologi.
Hubungan ketiganya menentukan apakah penyakit muncul atau tidak. Penyakit timbul ketika ada ketidakseimbangan antara host, agent, dan lingkungan.
Tidak semua penyakit disebabkan oleh satu agen saja. Banyak penyakit bersifat multifaktorial, artinya terjadi akibat interaksi berbagai faktor risiko.
WHO membagi determinan kesehatan ke dalam beberapa kelompok:
- Faktor biologis (umur, genetik).
- Faktor perilaku (gaya hidup).
- Faktor sosial-ekonomi (pendidikan, pendapatan, pekerjaan).
- Faktor lingkungan (air bersih, sanitasi, polusi).
- Akses pelayanan kesehatan.
Kesimpulan: Proses terjadinya penyakit merupakan hasil interaksi antara agen, host, dan lingkungan. Penyakit tidak selalu disebabkan oleh satu faktor, tetapi seringkali merupakan kombinasi multifaktor. Dinamika penyakit dalam populasi mencakup proses transmisi, distribusi, hingga upaya eliminasi atau eradikasi.
Dalam epidemiologi, ada beberapa istilah penting untuk menggambarkan pola kejadian penyakit di masyarakat:
Kasus Sporadik
Penyakit yang terjadi secara terpisah-pisah, jumlahnya kecil, dan tidak
menunjukkan pola tertentu.
Contoh: kasus rabies pada manusia di wilayah tertentu yang muncul
sesekali.
Endemi
Penyakit yang selalu ada di suatu wilayah dengan frekuensi relatif
konstan dalam jangka panjang.
Contoh: malaria di Papua, atau demam berdarah di daerah
tropis.
Epidemi (Kejadian Luar Biasa / KLB)
Peningkatan jumlah kasus penyakit dalam suatu wilayah dan waktu
tertentu, melebihi angka normal atau baseline.
Contoh: wabah campak di suatu kota dengan lonjakan kasus
mendadak.
Outbreak
Istilah lain untuk epidemi, biasanya digunakan untuk wilayah yang lebih
kecil atau terbatas.
Contoh: outbreak keracunan makanan di sekolah atau pesta
pernikahan.
Pandemi
Epidemi yang meluas hingga ke berbagai negara atau benua, menjangkiti
populasi global.
Contoh: pandemi COVID-19 (2019–sekarang), pandemi influenza
1918.
Selain istilah epidemi, epidemiologi juga menggunakan ukuran dasar untuk menggambarkan kejadian penyakit:
Kesimpulan:
Jenis kejadian penyakit dalam masyarakat dapat berupa sporadik, endemi, epidemi/outbreak, maupun pandemi. Untuk mengukur frekuensinya, epidemiologi menggunakan ukuran kasus, insidensi, dan prevalensi, yang menjadi dasar analisis lebih lanjut dalam kesehatan masyarakat.
Dalam epidemiologi, terdapat beberapa ukuran utama yang digunakan untuk menggambarkan dinamika penyakit dan faktor risikonya. Di antaranya adalah incidence rate, cumulative incidence, prevalensi, serta ukuran asosiasi yang melibatkan exposure seperti Relative Risk (RR), Odds Ratio (OR), dan ukuran tambahan seperti Attributable Risk (AR), Population Attributable Risk (PAR), dan Case Fatality Rate (CFR).
Definisi: Jumlah kasus baru suatu penyakit dalam periode tertentu dibagi dengan total waktu pemajanan (person-time) dari populasi berisiko. Ukuran ini menggambarkan kecepatan munculnya kasus baru.
Rumus:
\[ IR = \frac{\text{Kasus Baru}}{\text{Total Person-Time}} \]
Contoh: Jika terdapat 5 kasus baru TBC dalam 1 tahun pada populasi 1.000 orang, namun total person-time yang terhitung adalah 950 person-years, maka:
\[ IR = \frac{5}{950} = 0.00526 \; \text{per person-year} \]
atau 5,26 per 1.000 person-years.
Definisi: Proporsi individu yang mengalami penyakit baru dalam suatu periode tertentu di antara populasi berisiko pada awal periode. Menggambarkan risiko rata-rata terkena penyakit dalam periode tersebut.
Rumus:
\[ CI = \frac{\text{Kasus Baru dalam Periode}}{\text{Populasi Berisiko Awal}} \]
Contoh: Dari 1.000 orang sehat di awal tahun, terdapat 20 kasus baru diabetes selama 1 tahun. Maka:
\[ CI = \frac{20}{1000} = 0.02 = 2\% \]
Artinya, risiko terkena diabetes dalam setahun adalah 2%.
Definisi: Proporsi individu yang memiliki penyakit tertentu (kasus lama + baru) pada suatu titik waktu (point prevalence) atau periode tertentu (period prevalence). Menggambarkan beban penyakit pada populasi.
Rumus:
\[ P = \frac{\text{Kasus (baru + lama)}}{\text{Total Populasi}} \]
Contoh: Pada 1 Januari 2025, di kota berpenduduk 10.000 orang, terdapat 200 penderita hipertensi. Maka:
\[ P = \frac{200}{10000} = 0.02 = 2\% \]
Prevalensi hipertensi = 2%.
Dalam epidemiologi, kita sering meneliti hubungan antara exposure (paparan faktor risiko) dan outcome (penyakit). Untuk itu digunakan ukuran asosiasi seperti Relative Risk (RR) dan Odds Ratio (OR).
Definisi: Perbandingan risiko penyakit antara kelompok yang terpapar dan kelompok yang tidak terpapar. RR hanya dapat dihitung pada studi kohort.
Rumus:
\[ RR = \frac{CI_{exposed}}{CI_{unexposed}} \]
Contoh:
Maka:
\[ RR = \frac{0.20}{0.05} = 4 \]
Artinya, risiko kanker paru pada perokok 4 kali lebih tinggi dibanding bukan perokok.
Definisi: Perbandingan odds (peluang) terjadinya penyakit pada kelompok terpapar dibanding kelompok tidak terpapar. OR biasanya digunakan pada studi kasus-kontrol.
Rumus (dari tabel 2x2):
| Paparan / Penyakit | Penyakit (+) | Tidak Penyakit (-) |
|---|---|---|
| Terpapar (+) | a | b |
| Tidak Terpapar (-) | c | d |
\[ OR = \frac{a/b}{c/d} = \frac{ad}{bc} \]
Contoh:
\[ OR = \frac{30 \times 40}{20 \times 10} = \frac{1200}{200} = 6 \]
Artinya, perokok memiliki 6 kali odds lebih tinggi terkena kanker paru dibanding bukan perokok.
Definisi: Selisih risiko penyakit antara kelompok terpapar dan tidak terpapar. Menggambarkan beban absolut risiko yang diatribusikan pada paparan.
Rumus:
\[ AR = CI_{exposed} - CI_{unexposed} \]
Contoh: Jika \(CI_{exposed} = 0.20\) dan \(CI_{unexposed} = 0.05\), maka:
\[ AR = 0.20 - 0.05 = 0.15 \]
Artinya, ada 15 kasus tambahan per 100 orang pada kelompok terpapar akibat paparan.
Definisi: Proporsi insidensi penyakit dalam populasi yang dapat dicegah bila paparan dihilangkan. Mengukur dampak paparan pada tingkat populasi.
Rumus:
\[ PAR = CI_{population} - CI_{unexposed} \]
atau dalam bentuk proporsi:
\[ PAR\% = \frac{CI_{population} - CI_{unexposed}}{CI_{population}} \times 100\% \]
Contoh: Jika \(CI_{population} = 0.12\) dan \(CI_{unexposed} = 0.05\), maka:
\[ PAR = 0.12 - 0.05 = 0.07 \]
Artinya, 7 kasus per 100 orang dalam populasi dapat dicegah jika paparan dihilangkan.
Definisi: Proporsi kasus suatu penyakit yang berakhir dengan kematian. Menggambarkan tingkat keparahan penyakit.
Rumus:
\[ CFR = \frac{\text{Jumlah kematian akibat penyakit}}{\text{Jumlah kasus penyakit}} \times 100\% \]
Contoh: Jika dari 100 kasus COVID-19, terdapat 2 kematian, maka:
\[ CFR = \frac{2}{100} \times 100\% = 2\% \]
Dengan memahami ukuran-ukuran ini, peneliti dan praktisi kesehatan dapat menggambarkan distribusi penyakit, mengukur beban kesehatan, serta menilai dampak faktor risiko baik pada individu maupun populasi.
Jawablah soal-soal berikut dengan menunjukkan langkah perhitungan secara jelas. Gunakan data yang diberikan dan tuliskan interpretasi hasilnya.
Dalam sebuah studi, ditemukan 8 kasus baru penyakit X dalam 1 tahun pada populasi 1.200 orang. Karena ada migrasi dan kematian, total person-time yang berhasil dicatat adalah 1.050 person-years.
Dalam sebuah penelitian kohort terhadap 500 orang sehat di awal periode, setelah 2 tahun pengamatan ditemukan 25 kasus baru hipertensi.
Pada survei kesehatan di kota berpenduduk 20.000 orang, ditemukan 400 orang menderita diabetes pada saat survei dilakukan.
Data kohort:
Tentukan:
Penelitian kasus-kontrol memberikan data berikut:
| Paparan / Penyakit | Penyakit (+) | Tidak Penyakit (-) |
|---|---|---|
| Terpapar (+) | 45 | 30 |
| Tidak Terpapar (-) | 20 | 55 |
Hitung Odds Ratio (OR).
Apa makna hasil tersebut terkait hubungan paparan dengan penyakit?
Pada suatu wabah, ditemukan 250 kasus dengan 10 di antaranya meninggal.
Petunjuk Umum Tuliskan langkah perhitungan dengan jelas dan berikan interpretasi pada setiap jawaban. Hitung pula jika diminta dalam skala per 1.000 atau 100.000 agar mudah diinterpretasikan.
Diketahui: 8 kasus baru selama 1 tahun; total person-time = 1.050 person-years (PY).
Rumus umum:
\[ \text{Incidence Rate (IR)} =
\frac{\text{Kasus baru}}{\text{Total person-time}} \; (\text{per PY})
\]
Langkah hitung:
\[ IR = \frac{8}{1050} = 0{.}007619 \;
\text{per PY} \]
Dalam per 1.000 PY:
\[ IR_{(per\,1000)} = 0{.}007619 \times 1000
= 7{.}619 \approx \mathbf{7{.}62\;kasus/1000\;PY} \]
Interpretasi: Terdapat kira-kira 7,62 kasus baru penyakit X per 1.000 person-years.
cases <- 8; py <- 1050
IR <- cases/py
IR_per1000 <- IR*1000
IR; IR_per1000
## [1] 0.007619048
## [1] 7.619048
Diketahui: Kohort 500 orang sehat; selama 2 tahun muncul 25 kasus baru.
Rumus:
\[ \text{Cumulative Incidence (CI)} =
\frac{\text{Kasus baru pada periode}}{\text{Populasi berisiko di awal}}
\]
Hitung:
\[ CI = \frac{25}{500} = 0{.}05 =
\mathbf{5\%} \;\text{(selama 2 tahun)} \]
Interpretasi: Risiko seseorang di kohort untuk mengalami hipertensi selama 2 tahun adalah 5% (≈ 1 dari 20 orang).
new_cases <- 25; n0 <- 500
CI <- new_cases/n0
CI
## [1] 0.05
Diketahui: Populasi 20.000; kasus diabetes saat survei = 400.
Rumus:
\[ \text{Prevalensi} = \frac{\text{Kasus
(lama + baru) saat itu}}{\text{Total populasi}} \]
Hitung:
\[ P = \frac{400}{20000} = 0{.}02 =
\mathbf{2\%} \]
Interpretasi: Pada saat survei, 2% penduduk adalah penyandang diabetes (menggambarkan beban penyakit).
prev <- 400/20000
prev
## [1] 0.02
Data kohort:
- Perokok: 40 sakit dari 200 → \(CI_{exp} =
40/200\)
- Non-perokok: 15 sakit dari 300 → \(CI_{unexp} = 15/300\)
(1) CI Kelompok:
\[ CI_{exp} = \frac{40}{200} = 0{.}20 = 20\%
\]
\[ CI_{unexp} = \frac{15}{300} = 0{.}05 = 5\%
\]
(2) Relative Risk (RR):
\[ RR = \frac{CI_{exp}}{CI_{unexp}} =
\frac{0{.}20}{0{.}05} = \mathbf{4{.}0} \]
(3) Attributable Risk (AR) / Risk Difference:
\[ AR = CI_{exp} - CI_{unexp} = 0{.}20 -
0{.}05 = \mathbf{0{.}15} = \mathbf{15\%\;points} \]
(Opsional) Attributable Fraction among Exposed
(AF_e):
\[ AF_e = \frac{RR-1}{RR} = \frac{3}{4} =
0{.}75 = 75\% \]
Interpretasi: Perokok memiliki risiko 4 kali lebih tinggi mengalami penyakit paru kronis dibanding bukan perokok. Sekitar 15 poin persentase dari risiko pada perokok dapat diatribusikan pada kebiasaan merokok (≈ 75% dari risiko pada perokok terkait paparan).
a <- 40; n_exp <- 200
c_ <- 15; n_unexp <- 300
CI_exp <- a/n_exp
CI_unexp <- c_/n_unexp
RR <- CI_exp/CI_unexp
AR <- CI_exp - CI_unexp
AF_e <- (RR-1)/RR
c(CI_exp=CI_exp, CI_unexp=CI_unexp, RR=RR, AR=AR, AF_e=AF_e)
## CI_exp CI_unexp RR AR AF_e
## 0.20 0.05 4.00 0.15 0.75
Tabel kasus-kontrol:
| Paparan / Penyakit | Penyakit (+) | Tidak Penyakit (-) |
|---|---|---|
| Terpapar (+) | 45 | 30 |
| Tidak Terpapar (-) | 20 | 55 |
Notasi: \(a=45,\; b=30,\; c=20,\; d=55\).
Rumus OR:
\[ OR = \frac{a\times d}{b\times c} =
\frac{45\times 55}{30\times 20} = \frac{2475}{600} = \mathbf{4{.}125}
\]
Interpretasi: Peluang (odds) paparan di antara kasus adalah ≈ 4,13 kali peluang paparan di antara kontrol → ada asosiasi positif antara paparan dan penyakit.
a <- 45; b <- 30; c <- 20; d <- 55
OR <- (a*d)/(b*c)
OR
## [1] 4.125
Diketahui: 250 kasus; 10 meninggal.
Rumus:
\[ CFR =
\frac{\text{Meninggal}}{\text{Kasus}} \times 100\% \]
Hitung:
\[ CFR = \frac{10}{250} \times 100\% = 4\%
\]
Interpretasi: Sekitar 4% dari kasus pada wabah tersebut berujung kematian → menunjukkan tingkat keparahan penyakit ringan–sedang (interpretasi akhir bergantung pada konteks klinis & pembanding historis).
death <- 10; cases <- 250
CFR <- death/cases*100
CFR
## [1] 4
Pengumpulan data dalam epidemiologi sangat penting untuk memantau kesehatan masyarakat, mendeteksi wabah, serta mengevaluasi program intervensi. Metode yang digunakan dapat berupa surveilans, sumber data primer maupun sekunder, serta teknik lapangan untuk pencatatan dan registrasi kasus.
Kesimpulan Metode pengumpulan data epidemiologi mencakup surveilans (aktif dan pasif), pemanfaatan sumber data primer dan sekunder, serta penggunaan teknik lapangan dan registrasi kasus. Contoh pada dengue disease dan HIV menunjukkan bagaimana surveilans membantu deteksi dini, pengendalian penyakit, dan perencanaan program kesehatan masyarakat.
Di bawah ini disajikan bagan alur (flowchart) untuk
dua penyakit: Dengue dan HIV.
Saya sediakan dua bentuk: ASCII flowchart (pasti
tampil) dan Mermaid/DiagrammeR (opsional untuk HTML
jika paket tersedia).
Fasilitas Kesehatan (RS/PKM/Lab)
|
v
Formulir Kasus DBD Terisi (standar Kemenkes)
|
v
Sistem Pelaporan Elektronik (SIRS/EL-Surveilans)
|
v
Dinas Kesehatan Kab/Kota --(validasi & agregasi)--> Dinkes Provinsi
| |
v v
Analisis Tren (insidens, CFR, peta) Analisis Provinsi (KLB?)
| |
v v
Umpan Balik ke Fasyankes + Rekomendasi Tindak Lanjut (PSN, fogging terbatas, komunikasi risiko)
Signal Kenaikan Kasus / KLB
|
v
Tim Lapangan Dinkes Turun (PE: Penyelidikan Epidemiologi)
|
v
- Wawancara Kasus Indeks & Kontak
- Verifikasi Diagnosis (Lab RDT/NS1/IgM/IgG)
- Survey Jentik (ABJ, HI, CI, BI)
|
v
Input Temuan ke Sistem + Pemetaan Spasial
|
v
Tindakan Cepat: PSN 3M+, Fogging Fokus, Edukasi Masyarakat
|
v
Monitoring 7–14 hari & Evaluasi Efektivitas
Layanan VCT / CST / RS
|
v
Tes & Konseling (Consent) --> Hasil Lab (ELISA/RT-PCR)
|
v
Pencatatan Kasus (register HIV, ART status, CD4/VL)
|
v
Pelaporan Rutin ke Dinkes + Program HIV
|
v
Analisis Tren (insidens, cascade of care, gap ART)
|
v
Perencanaan Logistik ARV & Intervensi Pencegahan (PMTCT, harm reduction)
Penentuan Populasi Kunci (WPS, LSL, Waria, Penasun)
|
v
Desain Survei (Sentinel/IBBS) + Etik + Sampling
|
v
Pengumpulan Data (kuesioner perilaku + tes biologis teranonimkan)
|
v
Analisis Tren Prevalensi & Perilaku Berisiko
|
v
Kebijakan: Targeting Intervensi, Edukasi, Distribusi Kondom/Alat Suntik Steril
Catatan: Untuk output HTML, Anda bisa mengaktifkan versi diagram dengan Mermaid/DiagrammeR berikut (opsional). Pastikan paket
DiagrammeRterpasang.
# install.packages("DiagrammeR") # bila belum ada
library(DiagrammeR)
mermaid("
flowchart TD
A[Fasyankes (RS/PKM/Lab)] --> B[Formulir Kasus DBD]
B --> C[Sistem Pelaporan Elektronik]
C --> D[Dinkes Kab/Kota]
D --> E{Validasi & Agregasi}
E --> F[Dinkes Provinsi]
D --> G[Analisis Tren]
F --> H[Analisis Provinsi (KLB?)]
G --> I[Umpan Balik & Rekomendasi]
H --> I
")
library(DiagrammeR)
mermaid("
flowchart TD
A[Signal Kenaikan Kasus/KLB] --> B[Tim Lapangan (PE)]
B --> C[Wawancara & Verifikasi Lab]
B --> D[Survey Jentik (ABJ/HI/CI/BI)]
C --> E[Input Sistem + Peta]
D --> E
E --> F[Tindakan Cepat (PSN, Fogging Fokus, Edukasi)]
F --> G[Monitoring 7-14 hari & Evaluasi]
")
library(DiagrammeR)
mermaid("
flowchart TD
A[Layanan VCT/CST/RS] --> B[Tes & Konseling -> Hasil Lab]
B --> C[Registrasi HIV (ART, CD4, VL)]
C --> D[Pelaporan ke Dinkes/Program HIV]
D --> E[Analisis Tren: Cascade of Care]
E --> F[Perencanaan ARV & Pencegahan]
")
library(DiagrammeR)
mermaid("
flowchart TD
A[Populasi Kunci] --> B[Desain Survei + Etik + Sampling]
B --> C[Pengumpulan Data (Perilaku + Tes)]
C --> D[Analisis Tren Prevalensi & Risiko]
D --> E[Kebijakan & Target Intervensi]
")
Desain studi epidemiologi digunakan untuk menjawab pertanyaan tentang frekuensi, determinans, dan efektivitas intervensi pada masalah kesehatan. Dua kelompok besar desain adalah studi observasional dan studi eksperimental (intervensi).
Pada studi observasional, peneliti tidak memberikan intervensi; hanya mengamati hubungan antara paparan (exposure) dan kejadian penyakit (outcome).
Definisi: Mengukur paparan dan outcome pada
satu titik waktu (snapshot).
Tujuan utama: Mengestimasi prevalensi
dan mengeksplorasi asosiasi awal (hipotesis).
Ciri & Analisis:
Kelebihan:
Kelemahan:
Definisi: Memilih kasus (sakit) dan
kontrol (tidak sakit), lalu menelusuri
paparan di masa lalu.
Tujuan utama: Menguji hipotesis asosiasi
paparan–penyakit, efisien untuk penyakit langka.
Ciri & Analisis:
Kelebihan:
Kelemahan:
Definisi: Mengikuti kelompok terpapar dan tidak terpapar ke depan waktu untuk melihat kejadian penyakit. Dapat prospektif (ke depan) atau retrospektif (menggunakan data historis).
Ciri & Analisis:
Kelebihan:
Kelemahan:
Pada studi eksperimental, peneliti memberikan intervensi dan mengacak partisipan ke kelompok perlakuan vs kontrol bila memungkinkan.
Definisi: Peserta diacak
(randomized) ke kelompok intervensi dan
kelompok kontrol (plasebo/standar).
Tujuan: Menilai efikasi/efektivitas
intervensi (obat, vaksin, program).
Prinsip kunci:
Kelebihan:
Kelemahan:
| Aspek | Cross-sectional | Case-control | Cohort | RCT |
|---|---|---|---|---|
| Arah waktu | Sekali ukur | Mundur (retrospektif) | Maju (prospektif) / mundur (retrospektif) | Maju (intervensi) |
| Ukuran utama | Prevalensi | OR | Insidens, RR/HR | Efek kausal (RR, HR) |
| Cocok untuk | Beban penyakit, hipotesis awal | Penyakit langka/latent | Paparan langka | Uji intervensi |
| Kekuatan | Cepat, murah | Efisien untuk penyakit langka | Temporalitas jelas | Bias minimal, kausal kuat |
| Kelemahan | Tidak tahu temporality | Recall/selection bias; tidak ada insidens | Mahal, lama; loss to follow-up | Biaya, etika, generalisasi |
| Bukti kausal | Lemah | Sedang (tergantung bias) | Baik | Sangat kuat |
Tabel 2×2 (paparan × penyakit):
| Penyakit (+) | Penyakit (−) | Total | |
|---|---|---|---|
| Terpapar (+) | a | b | a+b |
| Tidak terpapar (−) | c | d | c+d |
| Total | a+c | b+d | N |
Odds Ratio (OR): \(OR =
\frac{a\times d}{b\times c}\).
Risk pada terpapar: \(CI_{exp} = a/(a+b)\).
Risk pada tidak terpapar: \(CI_{unexp} = c/(c+d)\).
Relative Risk (RR): \(RR =
CI_{exp}/CI_{unexp}\).
# Contoh kecil menghitung OR dan RR
a <- 45; b <- 30; c <- 20; d <- 55
OR <- (a*d)/(b*c)
CI_exp <- a/(a+b); CI_unexp <- c/(c+d)
RR <- CI_exp/CI_unexp
c(OR=OR, CI_exp=CI_exp, CI_unexp=CI_unexp, RR=RR)
## OR CI_exp CI_unexp RR
## 4.1250000 0.6000000 0.2666667 2.2500000
Latar Belakang & Desain Studi Studi case–control tentang hubungan wadah air tidak tertutup di rumah tangga dengan kejadian Demam Berdarah Dengue (DBD) di Kota X (periode Jan–Jun 2025).
Data Ringkas (Tabel 2×2) Berikut data fiktif konsisten dengan contoh:
| Paparan | Kasus (DBD) | Kontrol |
|---|---|---|
| Wadah tak tertutup (Exposed +) | a = 78 | b = 72 |
| Tidak (Exposed −) | c = 42 | d = 168 |
a <- 78; b <- 72; c <- 42; d <- 168
tab <- matrix(c(a,b,c,d), nrow = 2, byrow = TRUE,
dimnames = list(Exposure = c("Exposed +","Exposed -"),
Status = c("Case +","Control -")))
tab
## Status
## Exposure Case + Control -
## Exposed + 78 72
## Exposed - 42 168
Odds Ratio (OR) & 95% CI
Perhitungan Manual (Wald CI) Rumus OR: \( OR = \frac{a\times
d}{b\times c} \).
SE Wald untuk \(\ln(OR)\): \( SE = \sqrt{1/a + 1/b + 1/c + 1/d}
\).
CI 95% Wald untuk OR: \( \exp\{\ln(OR) \pm 1.96\,SE\} \).
OR <- (a*d)/(b*c)
SE <- sqrt(1/a + 1/b + 1/c + 1/d)
LCI <- exp(log(OR) - 1.96*SE)
UCI <- exp(log(OR) + 1.96*SE)
c(OR = OR, CI95_L = LCI, CI95_U = UCI)
## OR CI95_L CI95_U
## 4.333333 2.719828 6.904031
Interpretasi singkat: Odds DBD pada rumah tangga dengan wadah air tak tertutup sekitar 4.33 kali dibanding tanpa paparan. CI 95% Wald: 2.72–6.9.
OR & CI dari exact test (Fisher)
fisher.test() memberikan OR dan CI eksak (berbasis
hipergeometrik).
ft <- fisher.test(tab)
c(OR_Fisher = unname(ft$estimate), CI95_L = ft$conf.int[1], CI95_U = ft$conf.int[2], P_value = ft$p.value)
## OR_Fisher CI95_L CI95_U P_value
## 4.314131e+00 2.651348e+00 7.101069e+00 2.792061e-10
Catatan: Nilai OR eksak bisa sedikit berbeda dari perhitungan Wald, namun interpretasinya sama.
Regresi Logistik
Bentuk Teragregasi (disarankan untuk data 2×2) Kita
memodelkan peluang menjadi kasus sebagai fungsi
paparan menggunakan glm dengan respon
binomial berbobot kasus vs kontrol per
kelompok paparan.
df_agg <- data.frame(
exposed = c(1, 0),
cases = c(a, c),
controls = c(b, d)
)
fit_agg <- glm(cbind(cases, controls) ~ exposed, family = binomial, data = df_agg)
summary(fit_agg)
##
## Call:
## glm(formula = cbind(cases, controls) ~ exposed, family = binomial,
## data = df_agg)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -1.3863 0.1725 -8.036 9.30e-16 ***
## exposed 1.4663 0.2376 6.170 6.81e-10 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 40.417 on 1 degrees of freedom
## Residual deviance: 0.000 on 0 degrees of freedom
## AIC: 14.821
##
## Number of Fisher Scoring iterations: 3
# OR dan CI 95% (Wald) dari koefisien logit
beta <- coef(fit_agg)["exposed"]
se <- sqrt(vcov(fit_agg)["exposed","exposed"])
OR_glm <- exp(beta)
LCI_glm <- exp(beta - 1.96*se)
UCI_glm <- exp(beta + 1.96*se)
c(OR_glm = OR_glm, CI95_L = LCI_glm, CI95_U = UCI_glm)
## OR_glm.exposed CI95_L.exposed CI95_U.exposed
## 4.333333 2.719828 6.904031
Konsistensi: OR dari regresi logistik (teragregasi) harus sama dengan OR dari perhitungan 2×2 (perbedaan minimal karena pembulatan).
Bentuk Data Individu Kita dapat memperluas data menjadi baris per individu dan menyesuaikan model logistik yang sama.
dat <- rbind(
data.frame(case = 1, exposed = 1, n = a),
data.frame(case = 0, exposed = 1, n = b),
data.frame(case = 1, exposed = 0, n = c),
data.frame(case = 0, exposed = 0, n = d)
)
# ekspansi baris
dat_full <- dat[rep(1:nrow(dat), dat$n), c("case","exposed")]
fit_ind <- glm(case ~ exposed, family = binomial, data = dat_full)
summary(fit_ind)
##
## Call:
## glm(formula = case ~ exposed, family = binomial, data = dat_full)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -1.3863 0.1725 -8.036 9.30e-16 ***
## exposed 1.4663 0.2376 6.170 6.81e-10 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 458.29 on 359 degrees of freedom
## Residual deviance: 417.87 on 358 degrees of freedom
## AIC: 421.87
##
## Number of Fisher Scoring iterations: 4
exp(cbind(OR = coef(fit_ind), confint.default(fit_ind)))
## OR 2.5 % 97.5 %
## (Intercept) 0.250000 0.1782764 0.3505792
## exposed 4.333333 2.7198516 6.9039714
Pada data case–control, koefisien
exposeddariglm(logit) mengestimasi log(OR); sehinggaexp(coef)= OR.
Pelaporan
Kesimpulan: Ada asosiasi positif antara wadah air tak tertutup dan DBD pada studi case–control ini; peluang DBD meningkat sekitar 4.33 kali pada rumah tangga dengan paparan.
Catatan Lanjutan
survival::clogit).glm.# Analisis Data Epidemiologi
Pengolahan data epidemiologi merupakan tahap awal yang penting sebelum analisis dilakukan. Beberapa langkah utama meliputi:
Tujuan utama pembersihan data adalah memastikan kualitas data agar analisis yang dilakukan valid dan dapat diinterpretasikan dengan benar.
Analisis deskriptif bertujuan untuk menggambarkan data epidemiologi secara umum, meliputi:
Analisis inferensial digunakan untuk menguji hubungan atau perbedaan dalam data epidemiologi, meliputi:
Uji hipotesis digunakan untuk menilai apakah perbedaan atau asosiasi yang ditemukan signifikan secara statistik.
Beberapa uji yang umum digunakan dalam epidemiologi:
Interpretasi hasil uji hipotesis harus mempertimbangkan konteks epidemiologi, potensi bias, dan validitas data.
Disease mapping adalah representasi spasial (peta) dari distribusi suatu penyakit, biasanya dalam bentuk peta tematik yang menunjukkan tingkat prevalensi, insidensi, atau risiko penyakit pada wilayah tertentu. Pendekatan ini memadukan epidemiologi dengan geografi dan statistika spasial.
Tujuan Disease Mapping
Mengidentifikasi distribusi geografis penyakit Untuk melihat pola sebaran penyakit di berbagai wilayah, apakah terkonsentrasi (cluster), menyebar merata, atau mengikuti pola tertentu (misalnya di sepanjang sungai, daerah padat penduduk, dll).
Mendeteksi area risiko tinggi (hotspot) Peta dapat membantu menemukan daerah dengan prevalensi tinggi sehingga bisa difokuskan untuk intervensi.
Membandingkan antarwilayah Disease mapping memungkinkan perbandingan prevalensi penyakit antar kabupaten, provinsi, bahkan antarnegara.
Menyediakan dasar perencanaan kebijakan kesehatan Data spasial penyakit sangat penting untuk alokasi sumber daya medis (vaksin, tenaga kesehatan, rumah sakit lapangan).
Mendukung hipotesis epidemiologi Misalnya untuk menduga hubungan penyakit dengan faktor lingkungan (polusi udara, sanitasi, iklim).
Manfaat Disease Mapping dalam Studi Epidemiologi
Visualisasi kompleks menjadi sederhana: Data angka kasus yang besar dapat dipahami lebih cepat jika ditampilkan dalam bentuk peta.
Meningkatkan kewaspadaan: Aparat kesehatan dapat lebih cepat tanggap terhadap wabah bila tahu letak spasial penyebarannya.
Intervensi yang lebih tepat sasaran: Misalnya, program penyemprotan insektisida difokuskan ke daerah dengan peta risiko malaria tertinggi.
Mendukung komunikasi publik: Peta mudah dipahami masyarakat umum, sehingga lebih efektif untuk kampanye kesehatan.
Contoh Disease Mapping
Peta Malaria di Papua Pemetaan insidensi malaria per kabupaten menunjukkan daerah endemis tinggi (Merauke, Mimika) sehingga program distribusi kelambu difokuskan ke sana.
Peta COVID-19 di Indonesia (2020–2022) Pemerintah membuat peta zonasi (merah, oranye, hijau) berdasarkan jumlah kasus aktif. Peta ini digunakan untuk menentukan kebijakan PPKM di tiap daerah.
Peta Stunting di Jawa Barat Data prevalensi stunting divisualisasikan per desa/kecamatan, untuk menentukan desa prioritas dalam program perbaikan gizi anak.
Peta TBC di Kota Besar Cluster pasien TBC di pemukiman padat penduduk sering muncul jelas pada peta, memunculkan hipotesis hubungan dengan kualitas udara dan sanitasi.
Peta Demam Berdarah Dengue (DBD) Kasus DBD sering dipetakan hingga tingkat kelurahan untuk mendukung fogging dan eliminasi sarang nyamuk.
Dalam materi ini kita akan menggunakan data kasus Diare di Kota Bandung untuk contoh pemetaan.
library(spdep)
library(sp)
library(sf)
library(ggplot2)
library(dplyr)
# Arahkan ke folder kerja Anda
setwd("/Users/mindra/Library/CloudStorage/GoogleDrive-mindra@unpad.ac.id/My Drive/Spatial")
# Data kasus
Diare <- read.csv("Diare.csv", sep=";")
# Peta administratif Indonesia level kecamatan (gadm36 level 3)
Indo_Kec <- readRDS('gadm36_IDN_3_sp.rds')
# Ambil hanya Kota Bandung
Bandung <- Indo_Kec[Indo_Kec$NAME_2 == "Kota Bandung",]
plot(Bandung)
# Tambahkan id untuk join
Bandung$id <- c(1:30)
# Ubah ke format sf
Bandung_sf <- st_as_sf(Bandung)
# Gabungkan dengan data kasus
Bandung_merged <- Bandung_sf %>%
left_join(Diare, by = "id")
ggplot() +
geom_sf(data=Bandung_merged, aes(fill = Diare), color=NA) +
theme_bw() +
scale_fill_gradient(low = "yellow", high = "red") +
theme(panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
axis.text.x = element_blank(),
axis.text.y = element_blank(),
legend.position = "right") +
labs(title = "Peta Kasus Diare Kota Bandung",
fill = "Jumlah Kasus")
Interpretasi: Warna merah menandakan kasus lebih tinggi, kuning lebih rendah.
# Interval kategori
breaks <- c(-Inf, 550, 800, 1200, Inf)
labels <- c("Very Low", "Low", "High", "Very High")
# Buat variabel diskrit
Bandung_merged$Diare_Discrete <- cut(Bandung_merged$Diare,
breaks = breaks,
labels = labels,
right = TRUE)
ggplot() +
geom_sf(data=Bandung_merged, aes(fill = Diare_Discrete), color=NA) +
theme_bw() +
scale_fill_manual(values = c("Very Low" = "yellow",
"Low" = "orange",
"High" = "red",
"Very High" = "red3")) +
theme(axis.text.x = element_blank(),
axis.text.y = element_blank(),
legend.position = "right") +
labs(title = "Kategori Kasus Diare Kota Bandung",
fill = "Kategori")
Interpretasi: Kategori “High” dan “Very High” dapat mengindikasikan wilayah dengan risiko lebih besar.
Autokorelasi spasial digunakan untuk melihat apakah data memiliki pola keterkaitan antar wilayah tetangga.
# Matriks bobot spasial
row.names(Bandung) <- as.character(1:30)
W <- poly2nb(Bandung, row.names(Bandung), queen=FALSE)
WB <- nb2mat(W, style='B', zero.policy = TRUE)
WL <- nb2listw(W)
# Plot jaringan tetangga
CoordK <- coordinates(Bandung)
plot(Bandung, axes=T, col="gray90")
text(CoordK[,1], CoordK[,2], row.names(Bandung), col="black", cex=0.8, pos=1.5)
points(CoordK[,1], CoordK[,2], pch=19, cex=0.7, col="blue")
plot.nb(W, CoordK, add=TRUE, col="red", lwd=2)
# Global Moran's I
Global_Moran <- moran.test(Diare$Diare, WL)
Global_Moran
##
## Moran I test under randomisation
##
## data: Diare$Diare
## weights: WL
##
## Moran I statistic standard deviate = 0.67226, p-value = 0.2507
## alternative hypothesis: greater
## sample estimates:
## Moran I statistic Expectation Variance
## 0.04045578 -0.03448276 0.01242630
Interpretasi: Nilai Moran’s I menunjukkan apakah ada pola clustering (positif) atau sebaran acak.
Local Moran’s I digunakan untuk mengidentifikasi cluster lokal (misalnya high-high, low-low).
Local_Moran <- localmoran(Diare$Diare, WL)
# Ambil informasi quadran
mean_values_char <- as.character(attr(Local_Moran, "quadr")$mean)
Bandung_sf$mean_values <- mean_values_char
# Plot hasil LISA
ggplot() +
geom_sf(data = Bandung_sf, aes(fill = mean_values)) +
scale_fill_manual(values = c("Low-Low" = "blue",
"High-Low" = "green",
"Low-High" = "yellow",
"High-High" = "red")) +
labs(title = "Local Moran's I Kota Bandung",
fill = "Cluster Type") +
theme_minimal()
Interpretasi:
High-High (Merah): cluster wilayah dengan kasus tinggi dikelilingi wilayah tinggi.
Low-Low (Biru): cluster wilayah dengan kasus rendah dikelilingi wilayah rendah.
Low-High / High-Low: wilayah dengan nilai berbeda dari tetangganya (potensi outlier spasial).
Materi menjelaskan standardized morbidity/mortality ratio (SMR) serta direct dan indirect standardization dalam epidemiologi. Tujuannya adalah:
Catatan: SMR pada konteks ini bisa berarti Standardized Morbidity Ratio (morbiditas) atau Standardized Mortality Ratio (mortalitas). Secara matematis identik; yang berbeda hanyalah jenis kejadian (kasus sakit vs kematian).
Mengapa Perlu Standardisasi?
Angka penyakit/kematian mentah sering tidak dapat dibandingkan langsung antar wilayah/kelompok karena perbedaan struktur umur (atau variabel demografis lainnya). Penyakit degeneratif, misalnya, meningkat tajam pada usia lanjut; wilayah yang lebih “tua” akan tampak lebih “sakit” jika kita tidak menyesuaikan perbedaan ini. Standardisasi menghilangkan bias komposisi ini agar perbandingan menjadi adil.
Definisi dan Notasi Dasar
SMR (Indirect Standardization) didefinisikan sebagai \[ \mathrm{SMR} \;=\; \frac{O}{E}, \qquad E = \sum_i r_i^{(s)} \, N_i. \]
Directly Standardized Rate (DSR) didefinisikan sebagai \[ \mathrm{DSR} \;=\; \sum_i w_i \, r_i \;=\; \frac{\sum_i r_i \, N_i^{(s)}}{\sum_i N_i^{(s)}}. \]
| Aspek | Direct Standardization | Indirect Standardization (SMR) |
|---|---|---|
| Data yang dibutuhkan | Rate spesifik umur (\(r_i\)) stabil di populasi yang diteliti | Rate spesifik umur tersedia di populasi standar (\(r_i^{(s)}\)); di populasi yang diteliti mungkin kecil/tidak stabil |
| Keluaran utama | Angka kejadian terstandar (DSR) yang bisa dibandingkan langsung antar wilayah | Rasio SMR = \(O/E\) yang membandingkan populasi yang diteliti terhadap standar |
| Kegunaan | Membandingkan banyak wilayah secara setara | Menilai apakah satu populasi “lebih/kurang berisiko” daripada standar |
| Stabilitas pada populasi kecil | Bisa tidak stabil (varian besar) | Lebih stabil untuk populasi kecil |
Contoh 1 — Indirect Standardization (SMR)
Misal kita menilai penyakit paru di Daerah X. Kita punya ukuran populasi Daerah X per umur (\(N_i\)) dan rate standar (\(r_i^{(s)}\)) per 1.000 dari populasi nasional. Kita juga punya kasus diamati (\(O_i\)).
indirect_df <- data.frame(
umur = c("20–39", "40–59", "60+"),
N_i = c(4000, 3000, 2000),
r_std_per1000 = c(2, 6, 12),
O_i = c(10, 20, 30)
)
indirect_df$E_i <- indirect_df$r_std_per1000/1000 * indirect_df$N_i
indirect_df
## umur N_i r_std_per1000 O_i E_i
## 1 20–39 4000 2 10 8
## 2 40–59 3000 6 20 18
## 3 60+ 2000 12 30 24
Perhitungan manual:
O <- sum(indirect_df$O_i)
E <- sum(indirect_df$E_i)
SMR <- O / E
O; E; SMR
## [1] 60
## [1] 50
## [1] 1.2
Interpretasi: Jika Daerah X memiliki risiko seperti populasi standar, kita mengharapkan 50 kasus. Nyatanya ada 60 kasus, sehingga SMR = 1.2. Nilai > 1 berarti lebih tinggi dari harapan.
Interval Kepercayaan 95% untuk SMR
Dengan asumsi \(O \sim \mathrm{Poisson}(E \cdot \mathrm{SMR})\), CI eksak (berbasis \(\chi^2\)) untuk SMR adalah: \[ \left[ \frac{\chi^2_{\alpha/2, \, 2O}}{2E}, \; \frac{\chi^2_{1-\alpha/2, \, 2(O+1)}}{2E} \right]. \]
alpha <- 0.05
lower <- if (O > 0) qchisq(alpha/2, 2*O) / (2*E) else 0
upper <- qchisq(1 - alpha/2, 2*(O + 1)) / (2*E)
c(lower=lower, upper=upper)
## lower upper
## 0.9157264 1.5446379
Pelaporan: SMR = 1.2 (95% CI 0.92–1.54).
Contoh 2 — Direct Standardization (DSR)
Sekarang kita bandingkan Daerah A dan Daerah B. Kita memiliki kasus dan populasi per umur (sehingga dapat menghitung \(r_i\)), serta populasi standar \(N_i^{(s)}\).
direct_df <- data.frame(
umur = c("<40", "40–59", "60+"),
# Populasi & kasus di A
n_A = c(6000, 3000, 1000),
c_A = c(12, 18, 15),
# Populasi & kasus di B
n_B = c(5000, 3500, 1500),
c_B = c(15, 28, 35),
# Populasi standar
N_std = c(5000, 3000, 2000)
)
# Rate spesifik umur (per 1.000 untuk presentasi; gunakan per 1 untuk perhitungan)
direct_df$r_A <- direct_df$c_A / direct_df$n_A
direct_df$r_B <- direct_df$c_B / direct_df$n_B
direct_df$w <- direct_df$N_std / sum(direct_df$N_std)
direct_df$DSR_A <- sum(direct_df$w * direct_df$r_A)
direct_df$DSR_B <- sum(direct_df$w * direct_df$r_B)
# Varians aproksimasi (Greenwood-like): Var(DSR) ≈ sum w_i^2 * r_i / n_i
var_DSR_A <- sum(direct_df$w^2 * direct_df$r_A / direct_df$n_A)
var_DSR_B <- sum(direct_df$w^2 * direct_df$r_B / direct_df$n_B)
se_A <- sqrt(var_DSR_A)
se_B <- sqrt(var_DSR_B)
ci_A <- c(lower = direct_df$DSR_A - 1.96*se_A, upper = direct_df$DSR_A + 1.96*se_A)
ci_B <- c(lower = direct_df$DSR_B - 1.96*se_B, upper = direct_df$DSR_B + 1.96*se_B)
list(
table = direct_df[, c("umur","n_A","c_A","n_B","c_B","N_std","w","r_A","r_B")],
DSR_A = direct_df$DSR_A,
DSR_B = direct_df$DSR_B,
CI_A = ci_A,
CI_B = ci_B
)
## $table
## umur n_A c_A n_B c_B N_std w r_A r_B
## 1 <40 6000 12 5000 15 5000 0.5 0.002 0.00300000
## 2 40–59 3000 18 3500 28 3000 0.3 0.006 0.00800000
## 3 60+ 1000 15 1500 35 2000 0.2 0.015 0.02333333
##
## $DSR_A
## [1] 0.0058 0.0058 0.0058
##
## $DSR_B
## [1] 0.008566667 0.008566667 0.008566667
##
## $CI_A
## lower1 lower2 lower3 upper1 upper2 upper3
## 0.003978852 0.003978852 0.003978852 0.007621148 0.007621148 0.007621148
##
## $CI_B
## lower1 lower2 lower3 upper1 upper2 upper3
## 0.006628409 0.006628409 0.006628409 0.010504924 0.010504924 0.010504924
Untuk presentasi per 1.000 penduduk:
DSR_A_1000 <- direct_df$DSR_A * 1000
DSR_B_1000 <- direct_df$DSR_B * 1000
CI_A_1000 <- c(lower = (direct_df$DSR_A - 1.96*se_A)*1000,
upper = (direct_df$DSR_A + 1.96*se_A)*1000)
CI_B_1000 <- c(lower = (direct_df$DSR_B - 1.96*se_B)*1000,
upper = (direct_df$DSR_B + 1.96*se_B)*1000)
c(DSR_A_per1000 = DSR_A_1000,
DSR_B_per1000 = DSR_B_1000)
## DSR_A_per10001 DSR_A_per10002 DSR_A_per10003 DSR_B_per10001 DSR_B_per10002
## 5.800000 5.800000 5.800000 8.566667 8.566667
## DSR_B_per10003
## 8.566667
CI_A_1000; CI_B_1000
## lower1 lower2 lower3 upper1 upper2 upper3
## 3.978852 3.978852 3.978852 7.621148 7.621148 7.621148
## lower1 lower2 lower3 upper1 upper2 upper3
## 6.628409 6.628409 6.628409 10.504924 10.504924 10.504924
Interpretasi: Setelah menyesuaikan struktur umur standar, rate terstandar Daerah B lebih tinggi dari A. Perbedaan ini mencerminkan risiko yang lebih tinggi, bukan sekadar perbedaan struktur umur.
Ringkasan Perbedaan Direct vs Indirect
Fungsi Bantuan di R
Berikut fungsi generik untuk menghitung SMR (dengan CI eksak) dan DSR (dengan CI normal-approx).
# SMR dengan CI eksak berbasis chi-square
smr_ci <- function(O, E, alpha = 0.05){
lower <- if (O > 0) qchisq(alpha/2, 2*O) / (2*E) else 0
upper <- qchisq(1 - alpha/2, 2*(O + 1)) / (2*E)
list(SMR = O/E, lower = lower, upper = upper)
}
# DSR dengan Var ≈ sum w_i^2 * r_i / n_i
dsr_ci <- function(cases, pop, std_pop, alpha = 0.05){
r <- cases / pop
w <- std_pop / sum(std_pop)
dsr <- sum(w * r)
var <- sum((w^2) * r / pop)
se <- sqrt(var)
z <- qnorm(1 - alpha/2)
list(DSR = dsr, lower = dsr - z*se, upper = dsr + z*se)
}
Contoh pakai fungsi:
# Indirect
O <- sum(indirect_df$O_i); E <- sum(indirect_df$E_i)
smr_ci(O, E)
## $SMR
## [1] 1.2
##
## $lower
## [1] 0.9157264
##
## $upper
## [1] 1.544638
# Direct
dsr_ci(cases = direct_df$c_A, pop = direct_df$n_A, std_pop = direct_df$N_std)
## $DSR
## [1] 0.0058
##
## $lower
## [1] 0.003978885
##
## $upper
## [1] 0.007621115
dsr_ci(cases = direct_df$c_B, pop = direct_df$n_B, std_pop = direct_df$N_std)
## $DSR
## [1] 0.008566667
##
## $lower
## [1] 0.006628445
##
## $upper
## [1] 0.01050489
Catatan Praktis dan Batasan
Standardisasi memungkinkan perbandingan yang adil dengan menyesuaikan perbedaan komposisi umur. SMR (indirect) menjawab pertanyaan “apakah suatu populasi lebih/kurang berisiko dibanding standar?”, sedangkan DSR (direct) memberikan rate terstandar yang dapat dibandingkan lintas wilayah.
Rekomendasi pelaporan minimal: sebutkan metode (direct/indirect), standar yang dipakai, tabel input per umur, nilai ringkas (SMR atau DSR) beserta 95% CI, dan interpretasi substantifnya.
Crude rate (angka kasar) adalah jumlah kejadian dibagi total populasi, tanpa penyesuaian struktur umur/jenis kelamin: \[ \text{Crude Rate} = \frac{\text{Total Kasus}}{\text{Total Populasi}} \times k \] dengan \(k\) lazimnya 1.000 atau 100.000.
Keterbatasan: crude rate bias apabila struktur umur antar wilayah berbeda, terutama untuk penyakit yang sangat dipengaruhi umur. Karena itu, perbandingan antarwilayah dengan crude rate saja bisa menyesatkan.
Mengapa Perlu Standardisasi (Direct & Indirect)
Ringkasnya, crude rate menjawab “berapa kejadian apa adanya”, sedangkan standardized rate/ratio menjawab “apakah risikonya berbeda setelah mengendalikan struktur umur”.
Ilustrasi Numerik Kecil
Misalkan ada dua wilayah dengan struktur umur berbeda kuat. Kita bandingkan crude rate vs DSR dan SMR secara ringkas.
crude_demo <- data.frame(
wilayah = c("Kota A", "Kota B"),
kasus_total = c(80, 55),
pop_total = c(10000, 10000)
)
crude_demo$crude_per1000 <- with(crude_demo, kasus_total/pop_total*1000)
crude_demo
## wilayah kasus_total pop_total crude_per1000
## 1 Kota A 80 10000 8.0
## 2 Kota B 55 10000 5.5
Crude rate memberi kesan Kota A lebih “buruk”. Namun apakah itu murni karena risikonya lebih tinggi, atau karena struktur umur Kota A lebih tua? Jawaban adilnya diperoleh melalui standardisasi (DSR/SMR).
Contoh Perhitungan Terpadu: Crude vs DSR vs SMR
Kita gunakan data pada bagian sebelumnya dan hitung secara berdampingan untuk Daerah A dan Daerah B (DSR) serta Daerah X (SMR).
# Crude untuk A dan B (tanpa umur)
crude_AB <- data.frame(
wilayah = c("A","B"),
kasus_total = c(sum(direct_df$c_A), sum(direct_df$c_B)),
pop_total = c(sum(direct_df$n_A), sum(direct_df$n_B))
)
crude_AB$crude_per1000 <- with(crude_AB, kasus_total/pop_total*1000)
# DSR (sudah dihitung di atas)
DSR_A_1000 <- direct_df$DSR_A * 1000
DSR_B_1000 <- direct_df$DSR_B * 1000
# SMR untuk Daerah X (indirect)
O_X <- sum(indirect_df$O_i)
E_X <- sum(indirect_df$E_i)
SMR_X <- O_X / E_X
list(
crude_AB = crude_AB,
DSR_A_per1000 = DSR_A_1000,
DSR_B_per1000 = DSR_B_1000,
SMR_X = SMR_X
)
## $crude_AB
## wilayah kasus_total pop_total crude_per1000
## 1 A 45 10000 4.5
## 2 B 78 10000 7.8
##
## $DSR_A_per1000
## [1] 5.8 5.8 5.8
##
## $DSR_B_per1000
## [1] 8.566667 8.566667 8.566667
##
## $SMR_X
## [1] 1.2
Interpretasi umum:
Visualisasi Sederhana
# install.packages("ggplot2") # jika belum ada
library(ggplot2)
plot_df <- data.frame(
wilayah = factor(rep(c("A","B"), each = 2), levels = c("A","B")),
jenis = factor(rep(c("Crude","DSR"), times = 2), levels = c("Crude","DSR")),
nilai = c(crude_AB$crude_per1000[match("A", crude_AB$wilayah)],
DSR_A_1000,
crude_AB$crude_per1000[match("B", crude_AB$wilayah)],
DSR_B_1000)
)
ggplot(plot_df, aes(x = wilayah, y = nilai, fill = jenis)) +
geom_col(position = position_dodge(width = 0.7), width = 0.6) +
labs(x = NULL, y = "Rate per 1.000", fill = NULL) +
scale_y_continuous(expand = expansion(mult = c(0, .1))) +
theme_minimal(base_size = 12)
Catatan: Pada data sintetis ini, perbedaan antara Crude dan DSR menunjukkan dampak penyesuaian umur. Jika DSR mengecilkan celah antardaerah dibanding crude, berarti sebagian perbedaan tadi hanyalah efek struktur umur.
Materi ini menyajikan Standardized Morbidity Ratio (SMR) untuk disease mapping dengan asumsi proses Poisson pada area kecil. Fokusnya pada standar internal: \[ E_i = \Big(\frac{\sum_{j=1}^m O_j}{\sum_{j=1}^m N_j}\Big)\,N_i,\qquad SMR_i = \frac{O_i}{E_i},\quad i=1,\dots,m. \]
Diikuti hubungan konsep ini dengan SMR tidak langsung (indirect) pada umumnya, penurunan model probabilistik, cara menghitung CI 95%, serta contoh numerik dan kode R untuk analisis praktis dan flagging hotspot/coldspot.
Notasi
Intuisi: jika semua lokasi memiliki risiko yang sama dengan rata-rata regional \(\bar r\), maka kasus harapan di lokasi \(i\) adalah \(E_i\). Deviasi \(O_i\) dari \(E_i\) menunjukkan kemungkinan hotspot (\(>1\)) atau coldspot (\(<1\)).
Hubungan dengan SMR Indirect “Umum” SMR tidak langsung
(umum) menggunakan \[
E_i = \sum_a r_a^{(s)}\,N_{i,a},\qquad SMR_i = \frac{O_i}{E_i},
\] dengan \(r_a^{(s)}\) =
rate standar per strata (misalnya kelompok umur) dari
populasi standar (bisa eksternal, seperti nasional/WHO,
atau internal).
Formulasi pada bagian ini adalah kasus khusus: 1) Tidak
ada stratifikasi (satu strata), sehingga \(r^{(s)}\) tunggal.
2) Standar internal: ambil \(r^{(s)} = \bar r = (\sum O)/(\sum
N)\).
Maka \(E_i = r^{(s)}N_i = \bar r N_i\)
dan \(SMR_i = O_i/E_i\) adalah
SMR tak langsung berbasis standar internal per
lokasi.
Model probabilistik lazim untuk small-area disease mapping: \[ O_i \;\sim\; \text{Poisson}(E_i\,\theta_i), \] dengan \(\theta_i\) = relative risk lokasi \(i\). Estimator sederhana: \[ \widehat{\theta}_i = SMR_i = \frac{O_i}{E_i}. \]
95% CI eksak untuk \(\theta_i\) (dan setara untuk \(SMR_i\)) berbasis \(\chi^2\): \[ \left[ \frac{\chi^2_{\alpha/2,\,2O_i}}{2E_i},\; \frac{\chi^2_{1-\alpha/2,\,2(O_i+1)}}{2E_i} \right]. \]
Catatan: pada lokasi dengan \(O_i\) kecil, CI akan lebar; praktik umum adalah melakukan smoothing (Empirical Bayes, BYM/BYM2).
Contoh Numerik Mini (m = 8 lokasi) Kita buat data sintetis agar langkah-langkah transparan.
set.seed(123)
m <- 8
N <- round(runif(m, 8000, 25000)) # populasi area
# Bangun nilai "benar" risiko relatif (beberapa hotspot)
theta_true <- c(1.8, 1.2, 1.0, 0.9, 1.4, 0.8, 1.0, 1.6)
# Tentukan crude rate global (misal 0.6 per 1.000)
rbar <- 0.6/1000
E <- rbar * N
# Simulasi O_i ~ Poisson(E_i * theta_i)
O <- rpois(m, lambda = E * theta_true)
df <- data.frame(
area = paste0("A", 1:m),
N = N, E = E, O = O
)
df$SMR <- with(df, O/E)
df
## area N E O SMR
## 1 A1 12889 7.7334 14 1.8103292
## 2 A2 21401 12.8406 22 1.7133156
## 3 A3 14953 8.9718 10 1.1146035
## 4 A4 23011 13.8066 13 0.9415787
## 5 A5 23988 14.3928 25 1.7369796
## 6 A6 8774 5.2644 1 0.1899552
## 7 A7 16978 10.1868 8 0.7853300
## 8 A8 23171 13.9026 24 1.7262958
Hitung CI 95% eksak untuk masing-masing area:
smr_ci <- function(O, E, alpha = 0.05){
lower <- if (O > 0) qchisq(alpha/2, 2*O) / (2*E) else 0
upper <- qchisq(1 - alpha/2, 2*(O + 1)) / (2*E)
c(lower = lower, upper = upper)
}
ci_mat <- t(mapply(smr_ci, O = df$O, E = df$E))
df$SMR_L <- ci_mat[, "lower"]
df$SMR_U <- ci_mat[, "upper"]
df
## area N E O SMR SMR_L SMR_U
## 1 A1 12889 7.7334 14 1.8103292 0.989723831 3.037425
## 2 A2 21401 12.8406 22 1.7133156 1.073725751 2.593980
## 3 A3 14953 8.9718 10 1.1146035 0.534495719 2.049796
## 4 A4 23011 13.8066 13 0.9415787 0.501350984 1.610128
## 5 A5 23988 14.3928 25 1.7369796 1.124081614 2.564125
## 6 A6 8774 5.2644 1 0.1899552 0.004809249 1.058362
## 7 A7 16978 10.1868 8 0.7853300 0.339049768 1.547413
## 8 A8 23171 13.9026 24 1.7262958 1.106070293 2.568591
Visualisasi sederhana SMR + CI per area (tanpa peta geospasial):
op <- par(mar = c(5, 5, 1, 1))
ord <- order(df$SMR, decreasing = TRUE)
y <- df$SMR[ord]
yl <- df$SMR_L[ord]
yu <- df$SMR_U[ord]
labs <- df$area[ord]
plot(seq_along(y), y, ylim = range(c(yl, yu)), xaxt = "n",
xlab = "", ylab = "SMR (O_i / E_i)",
pch = 19)
arrows(x0 = seq_along(y), y0 = yl, x1 = seq_along(y), y1 = yu, angle = 90, code = 3, length = 0.04)
axis(1, at = seq_along(y), labels = labs, las = 2)
abline(h = 1, lty = 2, col = "gray50")
par(op)
Flagging hotspot/coldspot (berdasarkan CI vs 1):
df$flag <- ifelse(df$SMR_L > 1, "Hotspot (signif. > 1)",
ifelse(df$SMR_U < 1, "Coldspot (signif. < 1)",
"Tidak signifikan"))
df[, c("area","O","E","SMR","SMR_L","SMR_U","flag")]
## area O E SMR SMR_L SMR_U flag
## 1 A1 14 7.7334 1.8103292 0.989723831 3.037425 Tidak signifikan
## 2 A2 22 12.8406 1.7133156 1.073725751 2.593980 Hotspot (signif. > 1)
## 3 A3 10 8.9718 1.1146035 0.534495719 2.049796 Tidak signifikan
## 4 A4 13 13.8066 0.9415787 0.501350984 1.610128 Tidak signifikan
## 5 A5 25 14.3928 1.7369796 1.124081614 2.564125 Hotspot (signif. > 1)
## 6 A6 1 5.2644 0.1899552 0.004809249 1.058362 Tidak signifikan
## 7 A7 8 10.1868 0.7853300 0.339049768 1.547413 Tidak signifikan
## 8 A8 24 13.9026 1.7262958 1.106070293 2.568591 Hotspot (signif. > 1)
Untuk menstabilkan estimasi pada area kecil, pendekatan EB (naive, global) sering dipakai. Intuisinya: shrink \(SMR_i\) ke 1 dengan bobot tergantung “kekuatan data” (besar \(E_i\)). Skema sederhana berikut hanya ilustrasi (bukan turunan rigor mendalam).
# Skema EB global sederhana (ilustrasi):
# Prior Gamma(a, b) untuk theta; posterior ~ Gamma(a + O_i, b + E_i).
# Estimasi Bayes (mean posterior): (a + O_i) / (b + E_i).
# Pilih (a,b) agar mean prior = 1 dan var moderat (misal a=1, b=1) -> prior mean = 1.
a <- 1; b <- 1
df$theta_EB <- (a + df$O) / (b + df$E)
# Bandingkan raw SMR vs EB
head(df[, c("area","SMR","theta_EB")])
## area SMR theta_EB
## 1 A1 1.8103292 1.7175441
## 2 A2 1.7133156 1.6617777
## 3 A3 1.1146035 1.1031108
## 4 A4 0.9415787 0.9455243
## 5 A5 1.7369796 1.6891014
## 6 A6 0.1899552 0.3192644
Visual perbandingan SMR vs EB:
op <- par(mfrow = c(1,2), mar = c(4,4,2,1))
plot(df$SMR, df$theta_EB, pch = 19, xlab = "SMR (raw)", ylab = "EB (posterior mean)")
abline(0,1,lty=2,col="gray50")
plot(df$E, abs(df$theta_EB - df$SMR), pch = 19, xlab = "E_i", ylab = "|EB - SMR|")
par(op)
Praktik lapangan biasanya memakai pendekatan EB lokal atau model Bayesian spasial (BYM/BYM2) yang menggabungkan spatial smoothing melalui conditional autoregressive (CAR) priors—membiarkan area bertetangga saling “berbagi informasi” agar peta risiko lebih halus.
Ringkasan Praktis
Lampiran: Fungsi Bantuan
smr_table <- function(O, N, k = 1000, alpha = 0.05){
stopifnot(length(O) == length(N))
rbar <- sum(O) / sum(N)
E <- rbar * N
SMR <- O / E
lower <- ifelse(O > 0, qchisq(alpha/2, 2*O)/(2*E), 0)
upper <- qchisq(1-alpha/2, 2*(O+1))/(2*E)
data.frame(O = O, N = N, E = E, SMR = SMR, L = lower, U = upper)
}
# Contoh pakai fungsi:
# smr_table(O = df$O, N = df$N)
Dalam pemetaan penyakit (disease mapping), kita sering ingin memodelkan jumlah kasus penyakit pada area ke-\(i\), misalnya \(O_i\), dengan mempertimbangkan variasi spasial dan faktor risiko. Model yang paling dasar adalah model Poisson, yaitu:
\[ O_i \mid \lambda_i \sim \text{Poisson}(E_i \lambda_i) \]
di mana:
Namun, data penyakit sering menunjukkan overdispersion — varians lebih besar dari mean — yang tidak dapat dijelaskan oleh model Poisson murni. Untuk mengakomodasi variasi ini, digunakan model Poisson-Gamma, di mana parameter risiko \(\lambda_i\) dianggap acak dan mengikuti distribusi Gamma:
\[ \lambda_i \sim \text{Gamma}(a, b) \]
Sehingga model hierarkisnya adalah:
\[ O_i \mid \lambda_i \sim \text{Poisson}(E_i \lambda_i), \quad \lambda_i \sim \text{Gamma}(a, b) \]
Kita dapat mengintegrasikan \(\lambda_i\) dari model hierarkis di atas untuk mendapatkan distribusi marginal dari \(O_i\).
Distribusi gabungan:
\[ P(O_i = o_i, \lambda_i) = \frac{(E_i \lambda_i)^{o_i} e^{-E_i \lambda_i}}{o_i!} \cdot \frac{b^a}{\Gamma(a)} \lambda_i^{a-1} e^{-b\lambda_i} \]
Marginalisasi terhadap \(\lambda_i\) menghasilkan:
\[ P(O_i = o_i) = \int_0^\infty P(O_i = o_i, \lambda_i) \, d\lambda_i \]
Setelah diselesaikan secara analitik, hasilnya adalah distribusi Negative Binomial (NB):
\[ O_i \sim \text{NegBin}(r = a, p = \frac{b}{b + E_i}) \]
Artinya, model Poisson-Gamma ekuivalen dengan model Negative Binomial dengan:
\[ \mathbb{E}[O_i] = E_i \cdot \frac{a}{b}, \quad \text{Var}(O_i) = E_i \cdot \frac{a}{b} \left(1 + \frac{E_i}{b}\right) \]
Inilah yang membuat model Poisson-Gamma sangat berguna untuk menangani overdispersion: variasi tambahan berasal dari variasi acak pada \(\lambda_i\).
Kita ingin memperkirakan risiko relatif \(\lambda_i\) setelah mengamati data \(O_i\). Dengan teorema Bayes:
\[ P(\lambda_i \mid O_i) \propto P(O_i \mid \lambda_i) P(\lambda_i) \]
Substitusi:
\[ P(O_i \mid \lambda_i) \propto (E_i \lambda_i)^{O_i} e^{-E_i \lambda_i} \] \[ P(\lambda_i) \propto \lambda_i^{a-1} e^{-b\lambda_i} \]
Maka posteriornya adalah:
\[ \lambda_i \mid O_i \sim \text{Gamma}(a + O_i, b + E_i) \]
Nilai harapan posterior (posterior mean):
\[ \mathbb{E}[\lambda_i \mid O_i] = \frac{a + O_i}{b + E_i} \]
Ini dapat diinterpretasikan sebagai penyusutan Bayes (Bayesian shrinkage): risiko relatif \(\lambda_i\) “ditarik” ke arah rata-rata global \(a/b\), tergantung pada kekuatan informasi data \(O_i\).
Misalkan kita memiliki data jumlah kasus penyakit di 10 wilayah berikut:
set.seed(123)
area <- 1:10
E <- rep(20, 10) # expected cases
a <- 2; b <- 1 # prior parameters (mean = 2)
# True lambda (unknown risks)
lambda_true <- rgamma(10, a, b)
O <- rpois(10, E * lambda_true)
# Posterior estimation
lambda_post_mean <- (a + O) / (b + E)
data.frame(area, O, E, lambda_true, lambda_post_mean)
## area O E lambda_true lambda_post_mean
## 1 1 19 20 0.8920936 1.0000000
## 2 2 67 20 3.3118474 3.2857143
## 3 3 2 20 0.1443750 0.1904762
## 4 4 27 20 1.6625233 1.3809524
## 5 5 91 20 4.3358790 4.4285714
## 6 6 29 20 2.1176157 1.4761905
## 7 7 5 20 0.3507177 0.3333333
## 8 8 2 20 0.1304001 0.1904762
## 9 9 61 20 3.3737820 3.0000000
## 10 10 38 20 1.9730466 1.9047619
Interpretasi: nilai lambda_post_mean memberikan estimasi
risiko relatif posterior untuk tiap area. Area dengan sedikit kasus akan
memiliki estimasi yang “menyusut” ke rata-rata global, sementara area
dengan banyak kasus memiliki nilai lebih tinggi.
Model Poisson-Gamma ini dapat diperluas secara spasial dengan menambahkan struktur dependensi antar area, misalnya dengan prior Conditional Autoregressive (CAR) atau Besag-York-Mollié (BYM). Dalam hal ini, log-risiko dapat dimodelkan sebagai:
\[ \log(\lambda_i) = \alpha + u_i + v_i \]
di mana:
Model ini dapat diestimasi dengan INLA (Integrated Nested Laplace Approximation) untuk efisiensi komputasi.
Model Poisson-Gamma menyediakan jembatan alami antara Poisson dan Negative Binomial, menjelaskan overdispersion melalui ketidakpastian pada parameter risiko. Posterior dari risiko relatif memiliki bentuk Gamma yang mudah diturunkan secara analitik, menjadikannya model sederhana namun kuat untuk disease mapping.
Untuk kasus spasial yang kompleks, model ini menjadi dasar bagi model hierarkis spasial seperti BYM dan BYM2 yang lebih realistis.
Referensi:
Catatan: contoh kode bersifat self-contained (simulasi data + adjacency), sehingga bisa dijalankan tanpa data eksternal.
Pada disease mapping, kita sering memodelkan jumlah kasus \(Y_i\) pada area \(i\) (mis. kecamatan/kabupaten) sebagai \[ Y_i \sim \text{Poisson}(E_i \, \theta_i), \] di mana \(E_i\) adalah ekspektasi kasus jika tidak ada variasi spasial (mis. indirect standardization berdasarkan struktur umur/jenis kelamin) dan \(\theta_i\) adalah relative risk (RR) yang ingin dipetakan.
Pendekatan Bayesian memadukan likelihood dengan prior untuk menghasilkan posterior: \[ p(\boldsymbol{\theta} \mid \mathbf{Y}) \propto p(\mathbf{Y} \mid \boldsymbol{\theta}) \, p(\boldsymbol{\theta}). \] Pada konteks spasial, kita memilih prior yang mendorong kedekatan nilai di area bertetangga (mis. CAR/BYM), sehingga informasi “dipinjam” dari tetangga (borrowing strength) untuk menstabilkan estimasi pada area dengan populasi kecil.
Smoothing adalah proses menstabilkan estimasi risiko/angka kejadian yang bervariasi liar ketika jumlah populasi atau kasus kecil. Raw rate atau SMR \(\mathrm{SMR}_i = Y_i/E_i\) sering tidak stabil—area dengan \(E_i\) kecil akan memiliki nilai ekstrem karena fluktuasi acak. Bayesian smoothing (atau shrinkage) menarik estimasi menuju rata-rata global maupun rata-rata tetangga (spasial). Hasilnya: - Peta risiko lebih halus, - Pola lebih mudah ditangkap, - Interval kredibel lebih realistis, khususnya untuk area kecil.
Empirical Bayes (EB):
Full Bayes:
Struktur umum (untuk area \(i=1,\dots,n\)): \[ \begin{aligned} Y_i &\sim \text{Poisson}(E_i \,\theta_i),\\ \log \theta_i &= \alpha + \beta^\top x_i + u_i + v_i, \end{aligned} \]
dengan:
Pemetaan penyakit (disease mapping) adalah salah satu aplikasi paling populer dari pemodelan spasial dalam epidemiologi. Tujuan utamanya adalah:
Dalam pendekatan klasik, disease mapping sering dilakukan dengan membandingkan angka insiden kasar (crude rates) antar wilayah. Namun, angka tersebut bisa sangat tidak stabil jika jumlah penduduk kecil atau kasus jarang. Pendekatan Bayesian mengatasi masalah ini dengan:
Di antara metode komputasi Bayesian, pendekatan klasik adalah MCMC (Markov Chain Monte Carlo). Namun, untuk model spasial yang kompleks dan dataset besar, MCMC bisa sangat lambat. Di sinilah Integrated Nested Laplace Approximation (INLA) menjadi alternatif yang sangat efisien untuk kelas Latent Gaussian Models (LGM), termasuk sebagian besar model disease mapping.
Dokumen ini membahas:
Tujuan akhirnya adalah menyediakan materi yang cukup lengkap (konseptual, matematis, dan praktis) untuk pembelajaran maupun pengajaran.
Misalkan kita memiliki \(n\) wilayah (area) yang saling bersebelahan. Untuk setiap wilayah \(i = 1, \dots, n\):
Model disease mapping standar (Poisson log-linear) adalah:
\[ y_i \mid \lambda_i \sim \text{Poisson}(E_i \lambda_i), \]
dengan
\[ \log(\lambda_i) = \eta_i = \alpha + \mathbf{x}_i^\top \boldsymbol{\beta} + u_i + v_i. \]
Di sini:
Model ini dikenal juga sebagai model BYM (Besag–York–Mollié) ketika \(u_i\) dimodelkan sebagai ICAR (Intrinsic Conditional AutoRegressive) dan \(v_i\) sebagai IID Gaussian.
Dalam bentuk log-mean:
\[ \mu_i = \log(E_i) + \eta_i, \]
sehingga offset \(\log(E_i)\) bisa dimasukkan langsung dalam formulasi model.
Model Bayesian biasanya ditulis dalam tiga tahap:
Data model (likelihood)
\[
y_i \mid \eta_i \sim \text{Poisson}(\exp(\log(E_i) + \eta_i)).
\]
Latent field (model untuk \(\eta_i\))
\[
\eta_i = \alpha + \mathbf{x}_i^\top\boldsymbol{\beta} + u_i + v_i.
\]
Hyperparameters (model untuk parameter varians,
precision, dan parameter lain)
Misalnya: \[
\alpha \sim \mathrm{N}(0, \sigma_{\alpha}^2), \quad
\boldsymbol{\beta} \sim \mathrm{N}(\mathbf{0}, \Sigma_{\beta}),
\] dan prior untuk parameter presisi \(\tau_u, \tau_v\) yang mengontrol
variabilitas \(u_i\) dan \(v_i\).
INLA memanfaatkan fakta bahwa model ini adalah Latent Gaussian Model: latent field \((\eta, u, v, \alpha, \beta)\) berdistribusi Gaussian (conditional pada hyperparameters), sementara likelihood dapat berupa salah satu distribusi dari keluarga yang luas (termasuk Poisson).
Efek non-struktural (IID) dimodelkan sebagai:
\[ v_i \mid \tau_v \sim \mathrm{N}(0, \tau_v^{-1}), \quad i = 1, \dots, n, \]
dengan:
Efek ini tidak mengandung struktur spasial—setiap wilayah bisa memiliki deviasi sendiri tanpa memperhatikan tetangga.
Model ICAR adalah pondasi untuk banyak model spasial area-level. Asumsinya:
Model ICAR mendefinisikan distribusi bersama \(u = (u_1, \dots, u_n)\) melalui full conditional distributions:
\[ u_i \mid u_{-i}, \tau_u \sim \mathrm{N}\left( \frac{1}{m_i}\sum_{j \sim i} u_j,\; \frac{1}{\tau_u m_i} \right), \]
di mana \(j \sim i\) berarti \(w_{ij} = 1\) (tetangga).
Dalam bentuk matriks, prior ICAR dapat ditulis sebagai:
\[ \pi(\mathbf{u} \mid \tau_u) \propto \tau_u^{(n-1)/2} \exp\left( -\frac{\tau_u}{2} \mathbf{u}^\top Q \mathbf{u} \right), \]
dengan matriks presisi:
\[ Q_{ij} = \begin{cases} m_i, & \text{jika } i = j, \\ -1, & \text{jika } i \text{ dan } j \text{ bertetangga}, \\ 0, & \text{lainnya}. \end{cases} \]
Matriks ini singular (rank-deficient) karena model hanya mengidentifikasi perbedaan relatif antar area, bukan level absolut. Oleh karena itu prior ICAR disebut intrinsic.
Untuk melakukan inferensi, perlu diberi constraint, misalnya:
\[ \sum_{i=1}^n u_i = 0. \]
INLA menangani hal ini secara otomatis ketika kita menentukan
model = "icar" atau model = "besag"
(tergantung versi paket).
Model BYM menggabungkan:
\[ u_i \sim \text{ICAR}(\tau_u), \quad v_i \sim \mathrm{N}(0, \tau_v^{-1}), \]
sehingga:
\[ \eta_i = \alpha + \mathbf{x}_i^\top\boldsymbol{\beta} + u_i + v_i. \]
Model ini melakukan smoothing risiko relatif baik secara spasial maupun non-spasial.
Masalah klasik BYM:
Untuk mengatasi hal ini, dikembangkan reparameterization seperti BYM2.
Model BYM2 menulis efek total sebagai kombinasi terstandar:
\[ b_i = \frac{1}{\sqrt{\tau}} \left( \sqrt{1 - \phi}\, \theta_i + \sqrt{\phi}\, s_i \right), \]
dengan:
Interpretasi:
Keuntungan BYM2:
Dalam INLA, model BYM2 diakses dengan
model = "bym2".
Model Leroux adalah model CAR yang memadukan IID dan CAR dalam satu parameter smoothing \(\rho\):
\[ \mathbf{u} \mid \tau, \rho \sim \mathrm{N}\left(\mathbf{0}, \tau^{-1} \left[ (1-\rho)I + \rho R \right]^{-1} \right), \]
di mana \(R\) adalah matriks presisi ICAR (biasanya \(R = D - W\)).
Parameter \(\rho \in [0,1]\):
Model ini memberikan cara lain untuk mengontrol kekuatan smoothing spasial.
INLA dirancang untuk model dengan struktur:
\[ y_i \mid \eta_i, \boldsymbol{\theta} \sim \pi(y_i \mid \eta_i, \boldsymbol{\theta}), \quad i = 1,\dots,n. \]
\[ \eta_i = \mu_i + \sum_k z_{ik} x_k, \]
di mana \(x_k\) adalah komponen dari latent field \(\mathbf{x}\) (koefisien regresi, random effects, dsb).
\[ \mathbf{x} \mid \boldsymbol{\theta} \sim \mathrm{N}(\mathbf{0}, Q(\boldsymbol{\theta})^{-1}), \]
dengan \(Q(\boldsymbol{\theta})\) matriks presisi (jarang/sparse).
\[ \boldsymbol{\theta} \sim \pi(\boldsymbol{\theta}). \]
Tujuan: menghitung posterior marginal:
INLA menggunakan kombinasi pendekatan:
Skema kasar:
\[ \tilde{\pi}(\boldsymbol{\theta} \mid \mathbf{y}) \propto \frac{ \pi(\mathbf{x}, \boldsymbol{\theta}, \mathbf{y}) \big|_{\mathbf{x} = \mathbf{x}^*(\boldsymbol{\theta})} }{ \tilde{\pi}_G(\mathbf{x} \mid \boldsymbol{\theta}, \mathbf{y}) \big|_{\mathbf{x} = \mathbf{x}^*(\boldsymbol{\theta})} }, \]
di mana \(\mathbf{x}^*(\boldsymbol{\theta})\) adalah modus conditional \(\pi(\mathbf{x} \mid \boldsymbol{\theta}, \mathbf{y})\) dan \(\tilde{\pi}_G\) adalah pendekatan Gaussian.
\[ \pi(x_k \mid \mathbf{y}) = \int \pi(x_k \mid \boldsymbol{\theta}, \mathbf{y}) \pi(\boldsymbol{\theta} \mid \mathbf{y}) \, d\boldsymbol{\theta} \approx \sum_{m} \tilde{\pi}(x_k \mid \boldsymbol{\theta}_m, \mathbf{y}) \tilde{\pi}(\boldsymbol{\theta}_m \mid \mathbf{y}) \Delta_m. \]
INLA menyediakan tiga tingkat aproksimasi untuk \(\pi(x_k \mid \boldsymbol{\theta}, \mathbf{y})\):
Pemilihan prior untuk model disease mapping sangat penting. Prior yang kurang tepat dapat menghasilkan smoothing berlebihan atau under-smoothing.
Pada model klasik, precision \(\tau\) sering diberikan prior Gamma:
\[ \tau \sim \text{Gamma}(a, b). \]
Namun, interpretasi langsung dari parameter \(a, b\) terhadap varians sering tidak intuitif, dan prior ini bisa terlalu informatif pada skala log-varians.
PC priors didesain berdasarkan prinsip bahwa:
Untuk varians atau precision, PC prior sering dinyatakan dalam bentuk:
\[ \Pr(\sigma > U) = \alpha, \]
untuk \(U\) dan \(\alpha\) yang ditentukan pengguna. Ini kemudian diubah menjadi prior untuk \(\tau = 1/\sigma^2\).
Contoh: jika kita ingin menyatakan bahwa a priori ada probabilitas 0.01 bahwa standard deviation melebihi 1, maka:
\[ \Pr(\sigma > 1) = 0.01. \]
INLA menyediakan PC priors untuk banyak komponen, termasuk:
theta1).Pada BYM2:
\[ b_i = \frac{1}{\sqrt{\tau}} \left( \sqrt{1 - \phi}\, \theta_i + \sqrt{\phi}\, s_i \right), \]
dengan hyperparameters:
Misalnya, kita dapat mengatur:
Di R-INLA, ini dilakukan melalui argumen hyper di
f(..., model = "bym2", hyper = list(...)).
Sebelum memulai, pastikan paket INLA sudah terinstal.
Biasanya, INLA dipasang dari repositori khusus.
# install.packages("INLA", repos=c(getOption("repos"),
# INLA="https://inla.r-inla-download.org/R/stable"), dep=TRUE)
library(readxl)
library(INLA)
library(dplyr)
library(ggplot2)
library(sf) # untuk data spasial vector
library(spdep) # untuk adjacency dan nb2INLA
library(MASS)
Catatan: Sesuaikan instalasi INLA dengan dokumentasi versi terbaru.
Kita akan mensimulasikan data pada peta grid sederhana (misalnya \(5 \times 5\) = 25 area) dengan struktur ketetanggaan rook (atas–bawah–kiri–kanan).
set.seed(123)
# Buat grid 5x5
nrow <- 5
ncol <- 5
n <- nrow * ncol
grid <- expand.grid(
col = 1:ncol,
row = 1:nrow
) %>%
mutate(id = 1:n)
# Buat objek sf titik (hanya untuk contoh)
coords <- cbind(grid$col, grid$row)
grid_sf <- st_as_sf(data.frame(grid), coords = c("col", "row"), crs = 4326)
# Buat neighbor list rook (4-neighbors)
nb <- spdep::cell2nb(nrow, ncol, type = "rook")
W <- spdep::nb2mat(nb, style = "B") # matriks ketetanggaan
# Fungsi sederhana untuk generate ICAR dari Q
sim_icar <- function(Q, tau = 1) {
Q_eps <- Q + diag(1e-05, nrow(Q)) # tambahkan ridge
Sigma <- solve(Q_eps)
z <- MASS::mvrnorm(1, mu = rep(0, nrow(Q)), Sigma = (1 / tau) * Sigma)
as.numeric(z - mean(z)) # pusatkan ke mean 0
}
# Matriks presisi ICAR Q = D - W
D <- diag(rowSums(W))
Q <- D - W
tau_u <- 3 # precision efek spasial
tau_v <- 10 # precision efek iid
u_true <- sim_icar(Q, tau = tau_u)
v_true <- rnorm(n, 0, sd = 1 / sqrt(tau_v))
x <- rnorm(n, mean = 0, sd = 1)
alpha_true <- 1.0
beta_true <- 0.4
eta_true <- alpha_true + beta_true * x + u_true + v_true
# Eksposur (expected counts)
E <- runif(n, min = 50, max = 500)
lambda_true <- exp(eta_true)
mu_true <- E * lambda_true
# Simulasi kasus
y <- rpois(n, lambda = mu_true)
dat <- data.frame(
id = 1:n,
x = x,
E = E,
y = y,
u_true = u_true,
v_true = v_true
)
head(dat)
## id x E y u_true v_true
## 1 1 0.25331851 431.35392 369 -0.73764676 -0.53337926
## 2 2 -0.02854676 273.88727 935 -0.02755701 0.26493153
## 3 3 -0.04287046 224.55906 556 -0.13238557 0.04850084
## 4 4 1.36860228 160.90205 468 -0.09587661 -0.35991050
## 5 5 -0.22577099 99.99341 405 0.08289763 0.39649109
## 6 6 1.51647060 225.49750 886 -0.35134275 0.13485983
# Indeks area untuk INLA
dat$idx_area <- dat$id
# Buat file adjacency untuk INLA
graph_basename <- tempfile(pattern = "grid_adj")
file_adj <- paste0(graph_basename, ".graph")
nb2INLA(file_adj, nb) # pastikan nama file lengkap termasuk .graph
# Pengecekan isi file
first_lines <- readLines(file_adj, n = 5)
print(first_lines)
## [1] "25" "1 2 2 6" "2 3 1 3 7" "3 3 2 4 8" "4 3 3 5 9"
# Baca graph
g <- inla.read.graph(file_adj)
# Cek hasil
print(g)
## $n
## [1] 25
##
## $nnbs
## [1] 2 3 3 3 2 3 4 4 4 3 3 4 4 4 3 3 4 4 4 3 2 3 3 3 2
##
## $nbs
## $nbs[[1]]
## [1] 2 6
##
## $nbs[[2]]
## [1] 1 3 7
##
## $nbs[[3]]
## [1] 2 4 8
##
## $nbs[[4]]
## [1] 3 5 9
##
## $nbs[[5]]
## [1] 4 10
##
## $nbs[[6]]
## [1] 1 7 11
##
## $nbs[[7]]
## [1] 2 6 8 12
##
## $nbs[[8]]
## [1] 3 7 9 13
##
## $nbs[[9]]
## [1] 4 8 10 14
##
## $nbs[[10]]
## [1] 5 9 15
##
## $nbs[[11]]
## [1] 6 12 16
##
## $nbs[[12]]
## [1] 7 11 13 17
##
## $nbs[[13]]
## [1] 8 12 14 18
##
## $nbs[[14]]
## [1] 9 13 15 19
##
## $nbs[[15]]
## [1] 10 14 20
##
## $nbs[[16]]
## [1] 11 17 21
##
## $nbs[[17]]
## [1] 12 16 18 22
##
## $nbs[[18]]
## [1] 13 17 19 23
##
## $nbs[[19]]
## [1] 14 18 20 24
##
## $nbs[[20]]
## [1] 15 19 25
##
## $nbs[[21]]
## [1] 16 22
##
## $nbs[[22]]
## [1] 17 21 23
##
## $nbs[[23]]
## [1] 18 22 24
##
## $nbs[[24]]
## [1] 19 23 25
##
## $nbs[[25]]
## [1] 20 24
##
##
## $cc
## $cc$id
## [1] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
##
## $cc$n
## [1] 1
##
## $cc$nodes
## $cc$nodes[[1]]
## [1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
##
##
## $cc$mean
## [1] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## Levels: 1
##
##
## attr(,"class")
## [1] "inla.graph"
formula_bym <- y ~ 1 + x +
f(idx_area, model = "bym",
graph = g,
scale.model = TRUE,
hyper = list(
prec.unstruct = list(prior = "pc.prec", param = c(0.5, 0.01)),
prec.spatial = list(prior = "pc.prec", param = c(0.5, 0.01))
))
res_bym <- inla(
formula_bym,
family = "poisson",
data = dat,
E = E,
control.predictor = list(compute = TRUE),
control.compute = list(dic = TRUE, waic = TRUE, cpo = TRUE)
)
summary(res_bym)
## Time used:
## Pre = 1.2, Running = 2.11, Post = 0.0213, Total = 3.33
## Fixed effects:
## mean sd 0.025quant 0.5quant 0.975quant mode kld
## (Intercept) 1.039 0.037 0.962 1.039 1.116 1.039 388.332
## x 0.455 0.075 0.306 0.455 0.604 0.455 52.568
##
## Random effects:
## Name Model
## idx_area BYM model
##
## Model hyperparameters:
## mean sd 0.025quant 0.5quant
## Precision for idx_area (iid component) 7864.32 1.46e+05 4.06 152.45
## Precision for idx_area (spatial component) 5.25 2.53e+00 1.58 4.81
## 0.975quant mode
## Precision for idx_area (iid component) 35662.48 6.73
## Precision for idx_area (spatial component) 11.24 3.88
##
## Deviance Information Criterion (DIC) ...............: 258.56
## Deviance Information Criterion (DIC, saturated) ....: 49.74
## Effective number of parameters .....................: 24.70
##
## Watanabe-Akaike information criterion (WAIC) ...: 251.32
## Effective number of parameters .................: 12.58
##
## Marginal log-Likelihood: -168.27
## CPO, PIT is computed
## Posterior summaries for the linear predictor and the fitted values are computed
## (Posterior marginals needs also 'control.compute=list(return.marginals.predictor=TRUE)')
formula_bym2 <- y ~ 1 + x +
f(idx_area, model = "bym2",
graph = g,
scale.model = TRUE,
hyper = list(
prec = list(prior = "pc.prec", param = c(0.5, 0.01)),
phi = list(prior = "pc", param = c(0.5, 0.5))
))
res_bym2 <- inla(
formula_bym2,
family = "poisson",
data = dat,
E = E,
control.predictor = list(compute = TRUE),
control.compute = list(
dic = TRUE,
waic = TRUE,
cpo = TRUE,
return.marginals.predictor = TRUE # **ini tambahan penting**
)
)
summary(res_bym2)
## Time used:
## Pre = 8.26, Running = 0.227, Post = 0.016, Total = 8.5
## Fixed effects:
## mean sd 0.025quant 0.5quant 0.975quant mode kld
## (Intercept) 1.039 0.054 0.925 1.039 1.152 1.039 177.255
## x 0.453 0.080 0.296 0.454 0.610 0.454 47.076
##
## Random effects:
## Name Model
## idx_area BYM2 model
##
## Model hyperparameters:
## mean sd 0.025quant 0.5quant 0.975quant mode
## Precision for idx_area 5.602 1.55 3.103 5.420 9.144 5.091
## Phi for idx_area 0.617 0.22 0.174 0.641 0.955 0.814
##
## Deviance Information Criterion (DIC) ...............: 258.57
## Deviance Information Criterion (DIC, saturated) ....: 49.75
## Effective number of parameters .....................: 24.72
##
## Watanabe-Akaike information criterion (WAIC) ...: 251.30
## Effective number of parameters .................: 12.57
##
## Marginal log-Likelihood: -170.02
## CPO, PIT is computed
## Posterior summaries for the linear predictor and the fitted values are computed
## (Posterior marginals needs also 'control.compute=list(return.marginals.predictor=TRUE)')
phi_marg <- res_bym2$marginals.hyperpar[["Phi for idx_area"]]
# Cetak beberapa elemen untuk melihat struktur
str(phi_marg)
## num [1:43, 1:2] 0.0191 0.033 0.0612 0.1256 0.1736 ...
## - attr(*, "hyperid")= chr "11002|idx_area"
## - attr(*, "dimnames")=List of 2
## ..$ : NULL
## ..$ : chr [1:2] "x" "y"
head(phi_marg, 3)
## x y
## [1,] 0.01910488 0.002186451
## [2,] 0.03301109 0.011683780
## [3,] 0.06120761 0.056509568
phi_df <- data.frame(
phi = phi_marg[, 1],
dens = phi_marg[, 2]
)
ggplot(phi_df, aes(phi, dens)) +
geom_line() +
labs(x = expression(phi), y = "Posterior density",
title = "Posterior untuk proporsi variasi spasial (phi)")
Pada contoh simulasi grid, kita belum punya polygon area, hanya titik. Secara praktis, untuk analisis nyata, kita akan memakai shapefile polygon. Namun, kita bisa tetap mem-plot nilai RR (Relative Risk) sebagai heatmap titik.
eta_mean <- res_bym2$summary.linear.predictor$mean
lambda_mean <- exp(eta_mean)
dat$lambda_mean <- lambda_mean
# Gabungkan dengan koordinat grid
grid_plot <- cbind(grid, dat["lambda_mean"])
ggplot(grid_plot, aes(x = col, y = row, fill = lambda_mean)) +
geom_tile(color = "grey50") +
scale_fill_viridis_c(option = "C") +
coord_equal() +
labs(fill = "RR (mean)",
title = "Posterior mean Relative Risk (BYM2) pada Grid 5x5") +
theme_minimal()
# Setelah fitting res_bym2
# Pastikan marginals tersedia:
# control.compute = list(..., return.marginals.fitted.values = TRUE) was set.
# Atau jika tidak, bisa gunakan marginals.linear.predictor dan transformasi.
# Misalkan threshold
c_threshold <- 1.2
# Ambil marginals untuk fitted values (λ)
marg_list <- res_bym2$marginals.fitted.values
if (is.null(marg_list)) {
# Jika marginals.fitted.values NULL, gunakan marginals.linear.predictor dan transformasi
marg_lp <- res_bym2$marginals.linear.predictor
marg_list <- lapply(marg_lp,
function(m) inla.tmarginal(fun = exp, m = m))
}
# Hitung exceedance probability
exceed_prob <- sapply(marg_list, function(marg) {
1 - inla.pmarginal(q = c_threshold, marginal = marg)
})
# Tambahkan ke dataset
dat$exceed_prob <- exceed_prob
# Gabungkan ke grid untuk plotting
grid_plot2 <- cbind(grid, dat["exceed_prob"])
library(ggplot2)
ggplot(grid_plot2, aes(x = col, y = row, fill = exceed_prob)) +
geom_tile(color = "grey50") +
scale_fill_viridis_c(option = "C", labels = scales::percent_format(accuracy = 1)) +
coord_equal() +
labs(fill = paste0("Prob(λ > ", c_threshold, ")"),
title = paste0("Peta Probabilitas Melebihi RR > ", c_threshold)) +
theme_minimal()
Bagian ini sketsa konseptual untuk data nyata; .
library(sf)
library(dplyr)
library(spdep)
library(INLA)
library(ggplot2)
# Jika file ada di folder & nama misalnya "Bandung.shp"
setwd("/Users/mindra/mybook/Epidemiologi/BANDUNG")
Bandung_sf <- st_read("Bandung.shp")
## Reading layer `BANDUNG' from data source
## `/Users/mindra/mybook/Epidemiologi/BANDUNG/BANDUNG.shp' using driver `ESRI Shapefile'
## Simple feature collection with 30 features and 12 fields
## Geometry type: POLYGON
## Dimension: XY
## Bounding box: xmin: 107.5453 ymin: -6.969748 xmax: 107.7395 ymax: -6.840746
## Geodetic CRS: WGS84 Lat/Long's, Degrees, -180 ==> +180
Bandung_sf$id<-c(1:30)
Bandung_sf<-st_as_sf(Bandung_sf) # Pastikan kolom geometry ada
df_case<-read_xlsx("DataDBDRainfall.xlsx")
dat2 <- Bandung_sf %>%
st_drop_geometry() %>%
left_join(df_case, by = "id")
# 2. Buat neighbor list & graph untuk INLA
nb2 <- poly2nb(Bandung_sf) # opsi ‘row.names’ agar matching lebih aman
# Cek jumlah area
n_area <- length(nb2)
graph_basename2 <- tempfile(pattern = "kec_adj")
file_graph2 <- paste0(graph_basename2, ".graph")
# Buat graph file secara eksplisit dengan ekstensi
spdep::nb2INLA(file = file_graph2, nb = nb2)
# Cek file
if (!file.exists(file_graph2)) {
stop("File .graph tidak ditemukan: ", file_graph2)
}
# Tampilkan beberapa baris awal dari file, untuk verifikasi
first_lines <- readLines(file_graph2, n = 5)
print(first_lines)
## [1] "30" "1 4 3 25 29 30" "2 5 3 21 22 25 30"
## [4] "3 4 1 2 25 30" "4 2 5 6"
# Baca graph
g2 <- INLA::inla.read.graph(file_graph2)
# Verifikasi objek graph
print(g2$n) # harus = number of areas
## [1] 30
str(g2)
## List of 4
## $ n : int 30
## $ nnbs: num [1:30] 4 5 4 2 7 4 4 4 3 5 ...
## $ nbs :List of 30
## ..$ : int [1:4] 3 25 29 30
## ..$ : int [1:5] 3 21 22 25 30
## ..$ : int [1:4] 1 2 25 30
## ..$ : int [1:2] 5 6
## ..$ : int [1:7] 4 6 7 11 13 16 18
## ..$ : int [1:4] 4 5 7 23
## ..$ : int [1:4] 5 6 18 23
## ..$ : int [1:4] 9 10 11 28
## ..$ : int [1:3] 8 10 28
## ..$ : int [1:5] 8 9 11 12 24
## ..$ : int [1:5] 5 8 10 12 13
## ..$ : int [1:6] 10 11 13 14 15 24
## ..$ : int [1:7] 5 11 12 14 16 18 27
## ..$ : int [1:7] 12 13 15 17 19 20 27
## ..$ : int [1:5] 12 14 19 21 24
## ..$ : int [1:5] 5 13 17 18 27
## ..$ : int [1:6] 14 16 18 20 23 27
## ..$ : int [1:6] 5 7 13 16 17 23
## ..$ : int [1:5] 14 15 20 21 26
## ..$ : int [1:6] 14 17 19 26 29 30
## ..$ : int [1:5] 2 15 19 26 30
## ..$ : int [1:2] 2 25
## ..$ : int [1:4] 6 7 17 18
## ..$ : int [1:3] 10 12 15
## ..$ : int [1:4] 1 2 3 22
## ..$ : int [1:4] 19 20 21 30
## ..$ : int [1:4] 13 14 16 17
## ..$ : int [1:2] 8 9
## ..$ : int [1:3] 1 20 30
## ..$ : int [1:7] 1 2 3 20 21 26 29
## $ cc :List of 4
## ..$ id : int [1:30] 1 1 1 1 1 1 1 1 1 1 ...
## ..$ n : int 1
## ..$ nodes:List of 1
## .. ..$ : int [1:30] 1 2 3 4 5 6 7 8 9 10 ...
## ..$ mean : Factor w/ 1 level "1": 1 1 1 1 1 1 1 1 1 1 ...
## - attr(*, "class")= chr "inla.graph"
# 3. Tambahkan index area untuk random effect
dat2 <- dat2 %>%
mutate(
idx_area = as.integer(factor(id)) # 1…n_area
)
stopifnot(max(dat2$idx_area) == n_area)
# 4. Definisikan formula dengan BYM2 + kovariat
formula_kec <- Cases ~ 1 + Rainfall +
f(idx_area, model = "bym2",
graph = g2,
scale.model = TRUE,
constr = TRUE,
hyper = list(
prec = list(prior = "pc.prec", param = c(0.5, 0.01)),
phi = list(prior = "pc", param = c(0.5, 0.5))
))
# 5. Jalankan INLA
res_kec <- inla(
formula = formula_kec,
family = "poisson",
data = dat2,
E = dat2$E,
control.predictor = list(compute = TRUE),
control.compute = list(dic = TRUE, waic = TRUE, cpo = TRUE)
)
# 6. Ekstrak RR
dat2 <- dat2 %>%
mutate(
eta_mean = res_kec$summary.linear.predictor$mean,
RR_mean = exp(eta_mean)
)
# 7. Gabungkan kembali ke sf untuk plot
library(dplyr)
Bandung_res <- Bandung_sf %>%
left_join(dat2, by = "id")
Bandung_res_2024<-Bandung_res[Bandung_res$Year==2024,]
ggplot(Bandung_res_2024) +
geom_sf(aes(fill = RR_mean)) +
facet_wrap(~Month)+
scale_fill_viridis_c(option = "C") +
labs(
title = "Posterior Mean Relative Risk per Bandungan",
fill = "RR"
) +
theme_minimal()
# —————
# Perbandingan Model: IID‐only vs BYM2
# Pastikan data model IID vs BYM2 menggunakan *data yang sama (dat2)*
formula_iid <- Cases ~ 1 +Rainfall +
f(idx_area, model = "iid",
hyper = list(
prec = list(prior = "pc.prec", param = c(0.5, 0.01))
))
res_iid <- inla(
formula = formula_iid,
family = "poisson",
data = dat2,
E = dat2$E,
control.predictor = list(compute = TRUE),
control.compute = list(dic = TRUE, waic = TRUE, cpo = TRUE)
)
# Ambil DIC/WAIC
model_comp <- tibble(
model = c("IID_only", "BYM2"),
DIC = c(res_iid$dic$dic, res_kec$dic$dic),
WAIC = c(res_iid$waic$waic, res_kec$waic$waic)
)
print(model_comp)
## # A tibble: 2 × 3
## model DIC WAIC
## <chr> <dbl> <dbl>
## 1 IID_only 26575. 26267.
## 2 BYM2 26575. 26267.
dat2 <- dat2 %>%
mutate(
RR_iid = exp(res_iid$summary.linear.predictor$mean),
RR_bym2 = RR_mean
)
rr_long <- dat2 %>%
dplyr::select(id, RR_iid, RR_bym2) %>%
tidyr::pivot_longer(
cols = starts_with("RR_"),
names_to = "model",
values_to= "RR"
)
library(ggplot2)
ggplot(dat2, aes(x = RR_iid, y = RR_bym2)) +
geom_point(alpha = 0.6) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "grey30") +
labs(
x = "RR (IID-only)",
y = "RR (BYM2)",
title = "Perbandingan Relative Risk: IID-only vs BYM2"
) +
theme_minimal()
# Misal kita punya data panel area-time:
# data_st: id_area, time, y, E, x, ...
data_st <- df_case %>%
mutate(
area_idx = as.integer(factor(id)),
time_idx = as.integer(factor(Month)),
area_time_idx = as.integer(interaction(area_idx, time_idx, drop = TRUE))
)
formula_st <- Cases ~ 1 + Rainfall +
f(area_idx, model = "bym2", graph = g2, scale.model = TRUE, constr = TRUE,
hyper = list(
prec = list(prior = "pc.prec", param = c(0.5, 0.01)),
phi = list(prior = "pc", param = c(0.5, 0.5))
)) +
f(time_idx, model = "rw1",
hyper = list(
prec = list(prior = "pc.prec", param = c(0.5, 0.01))
)) +
f(area_time_idx, model = "iid",
hyper = list(
prec = list(prior = "pc.prec", param = c(0.5, 0.01))
))
res_st <- inla(
formula = formula_st,
family = "poisson",
data = data_st,
E = data_st$E,
control.predictor = list(compute = TRUE),
control.compute = list(dic = TRUE, waic = TRUE, cpo = TRUE)
)
Penutup
Materi ini telah mengulas:
Dengan R Markdown ini, Anda dapat:
Selanjutnya, Anda bisa mengintegrasikan materi ini dengan topik lain seperti:
Analisis survival (juga disebut time-to-event analysis) mempelajari waktu sampai terjadinya suatu kejadian (event).
Contoh event: - Kematian pasien - Kekambuhan penyakit - Kegagalan mesin - Drop-out dari program
Misalkan: - \(T\) = peubah acak waktu sampai event terjadi, dengan \(T \ge 0\) - \(t\) = realisasi waktu (angka tertentu, misal 12 bulan)
Fungsi-fungsi utama dalam analisis survival:
Fungsi distribusi kumulatif: \[ F(t) = P(T \le t) \] Probabilitas event sudah terjadi paling lambat pada waktu \(t\).
Fungsi survival: \[ S(t) = P(T > t) = 1 - F(t) \] Probabilitas subjek masih bertahan (belum mengalami event) sampai lewat waktu \(t\).
Fungsi densitas peluang (jika kontinu): \[ f(t) = rac{d}{dt}F(t) \]
Fungsi hazard: \[ h(t) = \lim_{\Delta t o 0} rac{P(t \le T < t + \Delta t \mid T \ge t)}{\Delta t} \]
Secara intuitif, \(h(t)\) adalah laju risiko sesaat di waktu \(t\), diberikan bahwa subjek masih hidup/bertahan sampai tepat sebelum \(t\).
Hubungan penting: \[ h(t) = rac{f(t)}{S(t)} \]
Definisikan hazard kumulatif: \[ H(t) = \int_0^t h(u)\,du \]
Hubungan antara \(S(t)\) dan \(H(t)\): \[ S(t) = \exp\{-H(t)\} \]
Ini sering dipakai dalam teori dan estimasi model survival.
Keunikan analisis survival adalah adanya censoring:
Kasus paling umum dalam praktik: right-censored.
Representasi data: - \(T_i = \min(T_i^st,
C_i)\)
\(T_i^st\) = waktu event
sebenarnya,
\(C_i\) = waktu censoring (misal akhir
studi), - \(\delta_i = 1\) jika event
teramati, \(\delta_i = 0\) jika
tersensor.
Kaplan–Meier digunakan untuk mengestimasi fungsi survival \(S(t)\) tanpa asumsi bentuk distribusi.
Misalkan: - Waktu event terurut: \(t_{(1)}
< t_{(2)} < \dots < t_{(k)}\)
(hanya waktu di mana terjadi event, bukan censoring) - \(d_j\) = jumlah event pada waktu \(t_{(j)}\) - \(n_j\) = jumlah individu yang masih
berisiko tepat sebelum \(t_{(j)}\)
Estimator Kaplan–Meier dituliskan sebagai:
\[ \hat{S}(t) = \prod_{t_{(j)} \le t} \left( 1 - \frac{d_j}{n_j} \right) \]
dengan:
Contoh ini menggunakan data 10 pasien kanker yang dipantau selama
beberapa bulan.
Status: - 1 = meninggal (event) - 0 =
tersensor (misal hilang follow up / studi berakhir)
kaplan_data <- data.frame(
id = 1:10,
waktu = c(3, 4, 5, 6, 6, 10, 15, 15, 16, 28),
status = c(1, 0, 1, 1, 1, 0, 1, 1, 1, 1)
)
kaplan_data
## id waktu status
## 1 1 3 1
## 2 2 4 0
## 3 3 5 1
## 4 4 6 1
## 5 5 6 1
## 6 6 10 0
## 7 7 15 1
## 8 8 15 1
## 9 9 16 1
## 10 10 28 1
Total ada 10 pasien pada awal studi.
Untuk Kaplan–Meier, kita fokus pada waktu event
unik:
\[
3, 5, 6, 15, 16, 28
\]
Definisi: - \(n_j\): banyaknya individu yang masih berisiko tepat sebelum \(t_{(j)}\) - \(d_j\): banyaknya event pada waktu \(t_{(j)}\)
Secara manual, dapat diringkas sebagai berikut:
| Waktu | \(n_j\) | \(d_j\) | Keterangan singkat |
|---|---|---|---|
| 3 | 10 | 1 | 10 berisiko, 1 meninggal |
| 5 | 8 | 1 | 1 dicensor di 4, tersisa 8 berisiko, 1 meninggal di 5 |
| 6 | 7 | 2 | 7 berisiko, 2 meninggal |
| 15 | 4 | 2 | 1 dicensor di 10, 4 berisiko, 2 meninggal |
| 16 | 2 | 1 | 2 berisiko, 1 meninggal |
| 28 | 1 | 1 | 1 berisiko, 1 meninggal |
\[ \begin{aligned} t_{(1)} = 3&: && \frac{d_1}{n_1} = \frac{1}{10} = 0.10, && 1 - \frac{d_1}{n_1} = 0.90 \\ t_{(2)} = 5&: && \frac{d_2}{n_2} = \frac{1}{8} = 0.125, && 1 - \frac{d_2}{n_2} = 0.875 \\ t_{(3)} = 6&: && \frac{d_3}{n_3} = \frac{2}{7} \approx 0.28571, && 1 - \frac{d_3}{n_3} \approx 0.71429 \\ t_{(4)} = 15&: && \frac{d_4}{n_4} = \frac{2}{4} = 0.50, && 1 - \frac{d_4}{n_4} = 0.50 \\ t_{(5)} = 16&: && \frac{d_5}{n_5} = \frac{1}{2} = 0.50, && 1 - \frac{d_5}{n_5} = 0.50 \\ t_{(6)} = 28&: && \frac{d_6}{n_6} = \frac{1}{1} = 1.00, && 1 - \frac{d_6}{n_6} = 0 \end{aligned} \]
Rumus Kaplan–Meier:
\[ \hat{S}(t) = \prod_{t_{(j)} \le t} \left(1 - \frac{d_j}{n_j}\right) \]
Secara bertahap:
\[ \begin{aligned} \hat{S}(0) &= 1 \\ \hat{S}(3) &= 1 \times 0.90 = 0.90000 \\ \hat{S}(5) &= 0.90000 \times 0.875 = 0.78750 \\ \hat{S}(6) &= 0.78750 \times 0.71429 \approx 0.56250 \\ \hat{S}(15) &= 0.56250 \times 0.50 = 0.28125 \\ \hat{S}(16) &= 0.28125 \times 0.50 = 0.14063 \\ \hat{S}(28) &= 0.14063 \times 0 = 0 \end{aligned} \]
Ringkasan:
| Waktu | \(n_j\) | \(d_j\) | Hazard \(d_j/n_j\) | \(1 - d_j/n_j\) | \(\hat{S}(t)\) |
|---|---|---|---|---|---|
| 0 | 10 | 0 | 0.00000 | 1.00000 | 1.00000 |
| 3 | 10 | 1 | 0.10000 | 0.90000 | 0.90000 |
| 5 | 8 | 1 | 0.12500 | 0.87500 | 0.78750 |
| 6 | 7 | 2 | 0.28571 | 0.71429 | 0.56250 |
| 15 | 4 | 2 | 0.50000 | 0.50000 | 0.28125 |
| 16 | 2 | 1 | 0.50000 | 0.50000 | 0.14063 |
| 28 | 1 | 1 | 1.00000 | 0.00000 | 0.00000 |
Censoring pada waktu 4 dan 10 tidak muncul sebagai baris event, tetapi memengaruhi nilai \(n_j\) berikutnya.
survfit di Rkm_fit <- survfit(Surv(waktu, status) ~ 1, data = kaplan_data)
summary(km_fit)
## Call: survfit(formula = Surv(waktu, status) ~ 1, data = kaplan_data)
##
## time n.risk n.event survival std.err lower 95% CI upper 95% CI
## 3 10 1 0.900 0.0949 0.7320 1.000
## 5 8 1 0.787 0.1340 0.5641 1.000
## 6 7 2 0.562 0.1651 0.3165 1.000
## 15 4 2 0.281 0.1631 0.0903 0.876
## 16 2 1 0.141 0.1286 0.0234 0.844
## 28 1 1 0.000 NaN NA NA
Perhatikan kolom time dan surv pada output
summary(km_fit) – nilai surv seharusnya sama
(atau sangat dekat) dengan nilai \(\hat{S}(t)\) hasil perhitungan manual.
ggsurvplot(
km_fit,
conf.int = TRUE,
risk.table = TRUE,
xlab = "Waktu (bulan)",
ylab = "Probabilitas survival",
surv.median.line = "hv"
)
Kurva survival Kaplan–Meier akan berbentuk tangga, dengan penurunan di setiap waktu event, dan risk table menunjukkan berapa banyak subjek yang masih berisiko pada berbagai waktu.
Nelson–Aalen mengestimasi fungsi hazard kumulatif \(H(t)\) dengan:
\[ \hat{H}(t) = \sum_{t_{(j)} \le t} \frac{d_j}{n_j} \]
Lalu fungsi survival dapat didekati dengan:
\[ \hat{S}(t) \approx \exp\{-\hat{H}(t)\} \] —
Dokumen ini menjelaskan perhitungan manual uji
log-rank (perbandingan dua kurva Kaplan–Meier) menggunakan
contoh data ovarian dari paket
survival di R.
Variabel utama:
futime = waktu follow-up (misal dalam hari)fustat = status kejadian
1 = event terjadi (misal: meninggal)0 = tersensor (masih hidup / lost follow-up)rx = kelompok perlakuan
1 = treatment 12 = treatment 2Uji log-rank digunakan untuk menguji:
H_0: tidak ada perbedaan fungsi survival antara dua kelompok
H_1: ada perbedaan fungsi survival antara minimal dua kelompok
Sekarang kita masuk ke rumus log-rank seperti yang muncul di gambar Excel.
Misal ada dua kelompok:
rx = 1rx = 2Untuk setiap waktu event \(t_j\) (j = 1, 2, …, k), didefinisikan:
Dalam tabel Excel:
Di bawah hipotesis nol (H_0) “tidak ada perbedaan survival antar kelompok”, probabilitas gagal (event) pada \(t_j\) dianggap sama untuk semua subjek di risk set.
Probabilitas gagal untuk satu subjek (tanpa membedakan kelompok) pada \(t_j\) diperkirakan oleh:
\[ \widehat{p}_j = \frac{d_j}{Y_j} \]
Sehingga expected number of failures di kelompok 1 pada waktu \(t_j\) adalah:
\[ E_{1j} = Y_{1j} \widehat{p}_j = \frac{Y_{1j}}{Y_j} d_j \]
Begitu pula untuk kelompok 2:
\[ E_{2j} = Y_{2j} \widehat{p}_j = \frac{Y_{2j}}{Y_j} d_j \]
Jelas bahwa:
\[ E_{1j} + E_{2j} = d_j \]
Total expected event untuk kelompok 1 sepanjang semua waktu event:
\[ E_1 = \sum_{j=1}^k E_{1j} \]
Total observed event di kelompok 1:
\[ O_1 = \sum_{j=1}^k d_{1j} \]
Jika kita menganggap \(d_j\) mengikuti distribusi hiper-geometrik (atau binomial aproksimasi) dengan alokasi ke dua kelompok secara acak menurut ukuran risk set, maka varians dari selisih \(d_{1j} - E_{1j}\) adalah:
\[ V_j = \frac{Y_{1j} Y_{2j} d_j (Y_j - d_j)}{Y_j^2 (Y_j - 1)} \]
Intuisi:
Total varians untuk perbandingan dua kelompok:
\[ V = \sum_{j=1}^k V_j \]
Statistik uji log-rank fokus pada selisih kumulatif antara observed dan expected di satu kelompok (misalnya kelompok 1):
\[ Z = \frac{O_1 - E_1}{\sqrt{V}} \]
Di bawah H_0, secara asimtotik:
\[ Z \sim N(0, 1) \]
Sehingga kita dapat juga menggunakan bentuk chi-square:
\[ \chi^2 = Z^2 = \frac{(O_1 - E_1)^2}{V} \]
Dengan derajat bebas 1. p-value dihitung sebagai:
\[ p\text{-value} = P\big(\chi^2\_{(1)} \ge \chi^2\_{\text{hitung}}\big) \]
Dalam R:
p_value <- 1 - pchisq(chisq_manual, df = 1)
Untuk memudahkan pengajaran, berikut ringkasan seluruh rumus dalam bentuk tabel:
\[ \begin{aligned} Y_{1j} &= \text{jumlah at risk di grup 1 sebelum } t_j \\ Y_{2j} &= \text{jumlah at risk di grup 2 sebelum } t_j \\ Y_j &= Y_{1j} + Y_{2j} \\ d_{1j} &= \text{jumlah event di grup 1 pada } t_j \\ d_{2j} &= \text{jumlah event di grup 2 pada } t_j \\ d_j &= d_{1j} + d_{2j} \end{aligned} \]
\[ E_{1j} = \frac{Y_{1j}}{Y_j} d_j, \qquad V_j = \frac{Y_{1j} Y_{2j} d_j (Y_j - d_j)}{Y_j^2 (Y_j - 1)} \]
\[ O_1 = \sum_j d_{1j}, \quad E_1 = \sum_j E_{1j}, \quad V = \sum_j V_j \]
\[ \chi^2 = \frac{(O_1 - E_1)^2}{V} \sim \chi^2\_{(1)} \quad \text{di bawah } H_0. \]
data(ovarian)
ovarian <- ovarian
# Lihat struktur data
str(ovarian)
## 'data.frame': 26 obs. of 6 variables:
## $ futime : num 59 115 156 421 431 448 464 475 477 563 ...
## $ fustat : num 1 1 1 0 1 0 1 1 0 1 ...
## $ age : num 72.3 74.5 66.5 53.4 50.3 ...
## $ resid.ds: num 2 2 2 2 2 1 2 2 2 1 ...
## $ rx : num 1 1 1 2 1 1 2 2 1 2 ...
## $ ecog.ps : num 1 1 2 1 1 2 2 2 1 2 ...
# Ringkas per kelompok rx
ovarian %>%
group_by(rx) %>%
summarise(
n = n(),
event = sum(fustat),
censored = n() - sum(fustat)
)
## # A tibble: 2 × 4
## rx n event censored
## <dbl> <int> <dbl> <dbl>
## 1 1 13 7 6
## 2 2 13 5 8
Uji log-rank hanya menggunakan waktu ketika event benar-benar terjadi, bukan waktu censoring.
event_times <- ovarian %>%
filter(fustat == 1) %>% # hanya yang mengalami event
arrange(futime) %>%
distinct(futime) %>% # ambil waktu event unik
pull(futime)
event_times
## [1] 59 115 156 268 329 353 365 431 464 475 563 638
make_logrank_table <- function(dat) {
times <- dat %>%
filter(fustat == 1) %>%
arrange(futime) %>%
distinct(futime) %>%
pull(futime)
out <- lapply(times, function(tj) {
# siapa saja yang masih at risk tepat sebelum tj?
at_risk <- dat %>%
filter(futime >= tj) # belum event / belum censor sebelum tj
# risk set per grup
n1j <- sum(at_risk$rx == 1)
n2j <- sum(at_risk$rx == 2)
# event pada tj per grup
d1j <- dat %>% filter(futime == tj, fustat == 1, rx == 1) %>% nrow()
d2j <- dat %>% filter(futime == tj, fustat == 1, rx == 2) %>% nrow()
data.frame(
time = tj,
Y1j = n1j,
Y2j = n2j,
d1j = d1j,
d2j = d2j
)
})
out <- bind_rows(out)
out <- out %>%
mutate(
Yj = Y1j + Y2j, # total at risk
dj = d1j + d2j # total event
)
out
}
logrank_tab <- make_logrank_table(ovarian)
kable(logrank_tab, caption = "Tabel Risk Set dan Event per Waktu") %>%
kable_styling(full_width = FALSE)
| time | Y1j | Y2j | d1j | d2j | Yj | dj |
|---|---|---|---|---|---|---|
| 59 | 13 | 13 | 1 | 0 | 26 | 1 |
| 115 | 12 | 13 | 1 | 0 | 25 | 1 |
| 156 | 11 | 13 | 1 | 0 | 24 | 1 |
| 268 | 10 | 13 | 1 | 0 | 23 | 1 |
| 329 | 9 | 13 | 1 | 0 | 22 | 1 |
| 353 | 8 | 13 | 0 | 1 | 21 | 1 |
| 365 | 8 | 12 | 0 | 1 | 20 | 1 |
| 431 | 8 | 9 | 1 | 0 | 17 | 1 |
| 464 | 6 | 9 | 0 | 1 | 15 | 1 |
| 475 | 6 | 8 | 0 | 1 | 14 | 1 |
| 563 | 5 | 7 | 0 | 1 | 12 | 1 |
| 638 | 5 | 6 | 1 | 0 | 11 | 1 |
logrank_tab <- logrank_tab %>%
mutate(
# Expected event untuk grup 1 dan 2
E1j = (Y1j / Yj) * dj,
E2j = (Y2j / Yj) * dj,
# Komponen varians untuk grup 1
Vj = (Y1j * Y2j * dj * (Yj - dj)) / (Yj^2 * (Yj - 1))
)
# pastikan menggunakan dplyr::select
logrank_clean <- logrank_tab %>%
dplyr::select(time, Y1j, Y2j, d1j, d2j, Yj, dj, E1j, E2j, Vj)
kable(
logrank_clean,
digits = 4,
caption = "Perhitungan Manual: Observed, Expected, dan Komponen Varians"
) %>%
kable_styling(full_width = FALSE)
| time | Y1j | Y2j | d1j | d2j | Yj | dj | E1j | E2j | Vj |
|---|---|---|---|---|---|---|---|---|---|
| 59 | 13 | 13 | 1 | 0 | 26 | 1 | 0.5000 | 0.5000 | 0.2500 |
| 115 | 12 | 13 | 1 | 0 | 25 | 1 | 0.4800 | 0.5200 | 0.2496 |
| 156 | 11 | 13 | 1 | 0 | 24 | 1 | 0.4583 | 0.5417 | 0.2483 |
| 268 | 10 | 13 | 1 | 0 | 23 | 1 | 0.4348 | 0.5652 | 0.2457 |
| 329 | 9 | 13 | 1 | 0 | 22 | 1 | 0.4091 | 0.5909 | 0.2417 |
| 353 | 8 | 13 | 0 | 1 | 21 | 1 | 0.3810 | 0.6190 | 0.2358 |
| 365 | 8 | 12 | 0 | 1 | 20 | 1 | 0.4000 | 0.6000 | 0.2400 |
| 431 | 8 | 9 | 1 | 0 | 17 | 1 | 0.4706 | 0.5294 | 0.2491 |
| 464 | 6 | 9 | 0 | 1 | 15 | 1 | 0.4000 | 0.6000 | 0.2400 |
| 475 | 6 | 8 | 0 | 1 | 14 | 1 | 0.4286 | 0.5714 | 0.2449 |
| 563 | 5 | 7 | 0 | 1 | 12 | 1 | 0.4167 | 0.5833 | 0.2431 |
| 638 | 5 | 6 | 1 | 0 | 11 | 1 | 0.4545 | 0.5455 | 0.2479 |
O1 <- sum(logrank_tab$d1j) # total observed event di grup 1
E1 <- sum(logrank_tab$E1j) # total expected event di grup 1
V <- sum(logrank_tab$Vj) # total variance
O1; E1; V
## [1] 7
## [1] 5.233531
## [1] 2.936196
chisq_manual <- (O1 - E1)^2 / V
chisq_manual
## [1] 1.06274
p_value <- 1 - pchisq(chisq_manual, df = 1)
p_value
## [1] 0.3025911
survdiff()fit_logrank <- survdiff(Surv(futime, fustat) ~ rx, data = ovarian)
fit_logrank
## Call:
## survdiff(formula = Surv(futime, fustat) ~ rx, data = ovarian)
##
## N Observed Expected (O-E)^2/E (O-E)^2/V
## rx=1 13 7 5.23 0.596 1.06
## rx=2 13 5 6.77 0.461 1.06
##
## Chisq= 1.1 on 1 degrees of freedom, p= 0.3
fit_logrank$chisq
## [1] 1.06274
Hasil fit_logrank$chisq harus sama (atau sangat dekat)
dengan chisq_manual yang kita hitung menggunakan rumus:
\[ \chi^2 = \frac{(O_1 - E_1)^2}{V}. \]
Untuk menyambungkan ke gambar Excel, kita bentuk tabel berikut.
excel_like <- logrank_tab %>%
transmute(
time,
Observed_rx1 = d1j,
NotObs_rx1 = Y1j - d1j,
MSum_rx1 = Y1j,
Observed_rx2 = d2j,
NotObs_rx2 = Y2j - d2j,
MSum_rx2 = Y2j,
Total_at_risk = Yj,
E_rx1 = E1j,
E_rx2 = E2j,
Vj = Vj,
Obs_minus_E_rx1 = d1j - E1j
)
kable(excel_like, digits = 4,
caption = "Tabel Gaya Excel: Observed, Not Observed, M.Sum, Expected, dan Variance") %>%
kable_styling(full_width = FALSE)
| time | Observed_rx1 | NotObs_rx1 | MSum_rx1 | Observed_rx2 | NotObs_rx2 | MSum_rx2 | Total_at_risk | E_rx1 | E_rx2 | Vj | Obs_minus_E_rx1 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 59 | 1 | 12 | 13 | 0 | 13 | 13 | 26 | 0.5000 | 0.5000 | 0.2500 | 0.5000 |
| 115 | 1 | 11 | 12 | 0 | 13 | 13 | 25 | 0.4800 | 0.5200 | 0.2496 | 0.5200 |
| 156 | 1 | 10 | 11 | 0 | 13 | 13 | 24 | 0.4583 | 0.5417 | 0.2483 | 0.5417 |
| 268 | 1 | 9 | 10 | 0 | 13 | 13 | 23 | 0.4348 | 0.5652 | 0.2457 | 0.5652 |
| 329 | 1 | 8 | 9 | 0 | 13 | 13 | 22 | 0.4091 | 0.5909 | 0.2417 | 0.5909 |
| 353 | 0 | 8 | 8 | 1 | 12 | 13 | 21 | 0.3810 | 0.6190 | 0.2358 | -0.3810 |
| 365 | 0 | 8 | 8 | 1 | 11 | 12 | 20 | 0.4000 | 0.6000 | 0.2400 | -0.4000 |
| 431 | 1 | 7 | 8 | 0 | 9 | 9 | 17 | 0.4706 | 0.5294 | 0.2491 | 0.5294 |
| 464 | 0 | 6 | 6 | 1 | 8 | 9 | 15 | 0.4000 | 0.6000 | 0.2400 | -0.4000 |
| 475 | 0 | 6 | 6 | 1 | 7 | 8 | 14 | 0.4286 | 0.5714 | 0.2449 | -0.4286 |
| 563 | 0 | 5 | 5 | 1 | 6 | 7 | 12 | 0.4167 | 0.5833 | 0.2431 | -0.4167 |
| 638 | 1 | 4 | 5 | 0 | 6 | 6 | 11 | 0.4545 | 0.5455 | 0.2479 | 0.5455 |
Interpretasi satu baris pada waktu event \(t_j\):
Observed_rx1 = \(d_{1j}\): jumlah event di grup 1.NotObs_rx1 = \(Y_{1j} -
d_{1j}\): subjek grup 1 yang masih hidup / belum event pada \(t_j\).MSum_rx1 = \(Y_{1j}\):
total at risk di grup 1.Demikian pula untuk grup 2.
Bagian penting untuk dibawa ke slide/materi kuliah:
Kaplan–Meier:
\[
\widehat{S}(t)
= \prod_{t_{(j)} \le t} \left(1 - \frac{d_j}{Y_j}\right)
\]
Log-rank (2 kelompok)
Anda bisa memotong bagian-bagian rumus ini untuk dimasukkan ke PPT, e-book, atau handout Analisis Survival.
Pendekatan parametrik mengasumsikan bentuk distribusi spesifik untuk \(T\). Keuntungannya: - Model lebih ringkas - Bisa mengekstrapolasi di luar rentang data (dengan hati-hati)
Asumsi: hazard konstan \[ h(t) = \lambda, \quad \lambda > 0 \]
Maka: \[ S(t) = \exp(-\lambda t) \] \[ f(t) = \lambda \exp(-\lambda t) \]
Likelihood untuk data dengan censoring:
Misalkan: - \(t_i\) = waktu observasi -
\(\delta_i\) = indikator event (1) /
censored (0)
Untuk distribusi eksponensial: \[ L(\lambda) = \prod_{i=1}^n [f(t_i)]^{\delta_i} [S(t_i)]^{1-\delta_i} = \prod_{i=1}^n [\lambda e^{-\lambda t_i}]^{\delta_i} [e^{-\lambda t_i}]^{1-\delta_i} \]
\[ \ln L(\lambda) = \sum_{i=1}^n \delta_i \ln \lambda - \lambda \sum_{i=1}^n t_i \]
Turunan terhadap \(\lambda\) dan set ke nol: \[ \frac{d}{d\lambda} \ln L(\lambda) = \frac{\sum_{i=1}^n \delta_i}{\lambda} - \sum_{i=1}^n t_i = 0 \Rightarrow \hat{\lambda} = \frac{\sum_{i=1}^n \delta_i}{\sum_{i=1}^n t_i} \] Jadi estimasi MLE parameter eksponensial sangat sederhana.
Model Weibull lebih fleksibel: - Hazard bisa meningkat atau menurun terhadap waktu.
Fungsi hazard: \[ h(t) = \lambda p t^{p-1}, \quad \lambda > 0,\, p > 0 \]
Fungsi survival: \[ S(t) = \exp(-\lambda t^p) \]
Interpretasi: - Jika \(p = 1\): sama dengan eksponensial - Jika \(p > 1\): hazard meningkat seiring waktu - Jika \(p < 1\): hazard menurun seiring waktu
Parameter biasanya diestimasi via maximum likelihood dengan optim numerik (software).
Model ini sangat populer karena: - Tidak perlu menspesifikkan bentuk baseline hazard \(h_0(t)\), - Tetap memungkinkan efek kovariat.
Misalkan \(X_i\) adalah vektor kovariat untuk individu ke-\(i\). Model Cox ditulis sebagai:
\[ h(t \mid X_i) = h_0(t)\,\exp(\eta^\top X_i) \]
dengan:
Cox mengusulkan partial likelihood sebagai berikut.
Misalkan terdapat event pada waktu \(t_{(1)} < t_{(2)} < \dots < t_{(k)}\), dan individu yang mengalami event ke-\(j\) adalah \(i_j\).
Set risk set \(R_j\) didefinisikan sebagai himpunan individu yang masih berisiko tepat sebelum waktu \(t_{(j)}\).
Partial likelihood ditulis sebagai:
\[ L_p(\eta) = \prod_{j=1}^k \frac{\exp(\eta^\top X_{i_j})}{\sum_{l \in R_j} \exp(\eta^\top X_l)} \]
Log partial likelihood:
\[ \ell_p(\eta) = \sum_{j=1}^k \left[ \eta^\top X_{i_j} - \log \left( \sum_{l \in R_j} \exp(\eta^\top X_l) \right) \right] \]
Estimasi \(\hat{\eta}\) diperoleh dengan memaksimumkan \(\ell_p(\eta)\).
Jika model dituliskan sebagai:
\[ h(t \mid X) = h_0(t)\,\exp(\eta_1 X_1 + \dots + \eta_p X_p) \]
maka hazard ratio untuk kovariat \(X_j\) adalah:
\[ \text{HR}_j = \exp(\eta_j) \]
Interpretasi:
Kita gunakan data lung dari paket survival.
Ini adalah data pasien kanker paru.
data("lung", package = "survival")
?lung
## Help on topic 'lung' was found in the following packages:
##
## Package Library
## KMsurv /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
## survival /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
##
##
## Using the first match ...
head(lung)
## inst time status age sex ph.ecog ph.karno pat.karno meal.cal wt.loss
## 1 3 306 2 74 1 1 90 100 1175 NA
## 2 3 455 2 68 1 0 90 90 1225 15
## 3 3 1010 1 56 1 0 90 90 NA 15
## 4 5 210 2 57 1 1 90 60 1150 11
## 5 1 883 2 60 1 0 100 90 NA 0
## 6 12 1022 1 74 1 1 50 80 513 0
summary(lung)
## inst time status age
## Min. : 1.00 Min. : 5.0 Min. :1.000 Min. :39.00
## 1st Qu.: 3.00 1st Qu.: 166.8 1st Qu.:1.000 1st Qu.:56.00
## Median :11.00 Median : 255.5 Median :2.000 Median :63.00
## Mean :11.09 Mean : 305.2 Mean :1.724 Mean :62.45
## 3rd Qu.:16.00 3rd Qu.: 396.5 3rd Qu.:2.000 3rd Qu.:69.00
## Max. :33.00 Max. :1022.0 Max. :2.000 Max. :82.00
## NA's :1
## sex ph.ecog ph.karno pat.karno
## Min. :1.000 Min. :0.0000 Min. : 50.00 Min. : 30.00
## 1st Qu.:1.000 1st Qu.:0.0000 1st Qu.: 75.00 1st Qu.: 70.00
## Median :1.000 Median :1.0000 Median : 80.00 Median : 80.00
## Mean :1.395 Mean :0.9515 Mean : 81.94 Mean : 79.96
## 3rd Qu.:2.000 3rd Qu.:1.0000 3rd Qu.: 90.00 3rd Qu.: 90.00
## Max. :2.000 Max. :3.0000 Max. :100.00 Max. :100.00
## NA's :1 NA's :1 NA's :3
## meal.cal wt.loss
## Min. : 96.0 Min. :-24.000
## 1st Qu.: 635.0 1st Qu.: 0.000
## Median : 975.0 Median : 7.000
## Mean : 928.8 Mean : 9.832
## 3rd Qu.:1150.0 3rd Qu.: 15.750
## Max. :2600.0 Max. : 68.000
## NA's :47 NA's :14
Penjelasan variabel (singkat): - time: waktu follow-up
(hari) - status: 1 = censored, 2 = death (perlu di-recode)
- age: usia - sex: 1 = male, 2 = female -
ph.ecog: ECOG performance score (semakin tinggi = kondisi
lebih buruk), dsb.
Dalam paket survival, kita pakai fungsi
Surv().
lung2 <- lung %>%
mutate(
status = ifelse(status == 2, 1, 0) # 1 = event (death), 0 = censored
)
surv_obj <- with(lung2, Surv(time = time, event = status))
head(surv_obj)
## [1] 306 455 1010+ 210 883 1022+
Objek Surv menyimpan informasi waktu dan status
censoring.
fit_km <- survfit(surv_obj ~ 1, data = lung2)
fit_km
## Call: survfit(formula = surv_obj ~ 1, data = lung2)
##
## n events median 0.95LCL 0.95UCL
## [1,] 228 165 310 285 363
Menampilkan ringkasan survival:
summary(fit_km)$table
## records n.max n.start events rmean se(rmean) median 0.95LCL
## 228.00000 228.00000 228.00000 165.00000 376.27475 19.70779 310.00000 285.00000
## 0.95UCL
## 363.00000
Plot kurva survival:
ggsurvplot(
fit_km,
data = lung2,
risk.table = TRUE,
conf.int = TRUE,
xlab = "Waktu (hari)",
ylab = "Probabilitas Survival",
title = "Kurva Kaplan–Meier (Overall)",
surv.median.line = "hv"
)
Interpretasi: - Kurva survival menunjukkan probabilitas bertahan hidup seiring waktu. - Garis horizontal/vertikal median menunjukkan waktu ketika survival = 0.5 (median survival). - Risk table menunjukkan berapa banyak pasien yang masih diikuti (at risk) pada tiap titik waktu.
Mari kita bangun model Cox dengan kovariat: - sex_factor
(laki vs perempuan) - age (usia) - ph.ecog
(kondisi klinis)
cox_fit <- coxph(
Surv(time, status) ~ sex_factor + age + ph.ecog,
data = lung2
)
summary(cox_fit)
## Call:
## coxph(formula = Surv(time, status) ~ sex_factor + age + ph.ecog,
## data = lung2)
##
## n= 227, number of events= 164
## (1 observation deleted due to missingness)
##
## coef exp(coef) se(coef) z Pr(>|z|)
## sex_factorPerempuan -0.552612 0.575445 0.167739 -3.294 0.000986 ***
## age 0.011067 1.011128 0.009267 1.194 0.232416
## ph.ecog 0.463728 1.589991 0.113577 4.083 4.45e-05 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## exp(coef) exp(-coef) lower .95 upper .95
## sex_factorPerempuan 0.5754 1.7378 0.4142 0.7994
## age 1.0111 0.9890 0.9929 1.0297
## ph.ecog 1.5900 0.6289 1.2727 1.9864
##
## Concordance= 0.637 (se = 0.025 )
## Likelihood ratio test= 30.5 on 3 df, p=1e-06
## Wald test = 29.93 on 3 df, p=1e-06
## Score (logrank) test = 30.5 on 3 df, p=1e-06
Output utama: - coef = \(\hat{eta}\) - exp(coef) =
hazard ratio - se(coef), z,
Pr(>|z|) = uji signifikansi - Concordance,
Likelihood ratio test, dsb.
Misalkan (contoh pola interpretasi; angka aktual lihat output):
sex_factorPerempuan dengan HR < 1:
Berarti perempuan memiliki hazard (risiko kematian) yang lebih
rendah dibanding laki-laki.
Misal HR = 0.7 → hazard 30% lebih rendah.
age dengan HR sedikit > 1:
Misal HR = 1.02 → setiap kenaikan 1 tahun usia, hazard meningkat
2%.
ph.ecog dengan HR > 1, misal HR = 1.5: Setiap
peningkatan satu poin ECOG (kondisi lebih buruk), hazard meningkat
50%.
# Jika paket broom belum terpasang, jalankan dulu:
# install.packages("broom")
broom::tidy(cox_fit, exponentiate = TRUE, conf.int = TRUE)
Kita bisa menggambar kurva survival yang diprediksi dari model Cox untuk berbagai kombinasi kovariat.
Contoh: prediksi survival untuk laki-laki vs perempuan pada usia dan
ph.ecog tertentu.
# Data baru untuk prediksi
newdata <- data.frame(
sex_factor = factor(c("Laki-laki", "Perempuan"),
levels = levels(lung2$sex_factor)),
age = c(60, 60),
ph.ecog = c(1, 1)
)
cox_fit
## Call:
## coxph(formula = Surv(time, status) ~ sex_factor + age + ph.ecog,
## data = lung2)
##
## coef exp(coef) se(coef) z p
## sex_factorPerempuan -0.552612 0.575445 0.167739 -3.294 0.000986
## age 0.011067 1.011128 0.009267 1.194 0.232416
## ph.ecog 0.463728 1.589991 0.113577 4.083 4.45e-05
##
## Likelihood ratio test=30.5 on 3 df, p=1.083e-06
## n= 227, number of events= 164
## (1 observation deleted due to missingness)
fit_surv_cox <- survfit(cox_fit, newdata = newdata)
ggsurvplot(
fit_surv_cox,
data = newdata,
conf.int = TRUE,
legend.labs = c("Laki-laki, 60 th, ECOG 1", "Perempuan, 60 th, ECOG 1"),
legend.title = "Profil Pasien",
xlab = "Waktu (hari)",
ylab = "Probabilitas Survival",
title = "Kurva Survival Diprediksi dari Model Cox"
)
Interpretasi: - Kurva memperlihatkan survival yang diharapkan untuk profil pasien tertentu. - Selisih antara kurva mencerminkan perbedaan hazard ratio antar kovariat.
Asumsi utama model Cox: - Hazard ratio konstan terhadap waktu (proportional hazards).
Kita dapat mengecek dengan uji Schoenfeld residuals:
ph_test <- cox.zph(cox_fit)
ph_test
## chisq df p
## sex_factor 2.305 1 0.13
## age 0.188 1 0.66
## ph.ecog 2.054 1 0.15
## GLOBAL 4.464 3 0.22
plot(ph_test)
survregSebagai tambahan, kita bisa memodelkan survival secara parametrik
dengan survreg().
Contoh: model Weibull.
weibull_fit <- survreg(
Surv(time, status) ~ sex_factor + age + ph.ecog,
data = lung2,
dist = "weibull"
)
summary(weibull_fit)
##
## Call:
## survreg(formula = Surv(time, status) ~ sex_factor + age + ph.ecog,
## data = lung2, dist = "weibull")
## Value Std. Error z p
## (Intercept) 6.67453 0.42740 15.62 < 2e-16
## sex_factorPerempuan 0.40109 0.12373 3.24 0.0012
## age -0.00748 0.00676 -1.11 0.2690
## ph.ecog -0.33964 0.08348 -4.07 4.7e-05
## Log(scale) -0.31319 0.06135 -5.11 3.3e-07
##
## Scale= 0.731
##
## Weibull distribution
## Loglik(model)= -1132.4 Loglik(intercept only)= -1147.4
## Chisq= 29.98 on 3 degrees of freedom, p= 1.4e-06
## Number of Newton-Raphson Iterations: 5
## n=227 (1 observation deleted due to missingness)
Catatan: - Parametrisasi survreg menggunakan bentuk
accelerated failure time (AFT). - Interpretasi koefisien: efek
pada log-waktu survival, bukan langsung hazard ratio. - Namun kita bisa
transform untuk mendapatkan parameter hazard/dekadence tertentu.
\[ \left(1 - \frac{d_j}{n_j}\right) \]
Sebagai ilustrasi tambahan, berikut contoh simulasi data survival eksponensial dengan satu kovariat biner.
set.seed(123)
n <- 300
x <- rbinom(n, 1, 0.5) # 0 atau 1
lambda0 <- 0.01 # baseline hazard
beta <- log(2) # hazard ratio = 2 jika x = 1
# Hazard individu: lambda_i = lambda0 * exp(beta * x)
lambda_i <- lambda0 * exp(beta * x)
# Generate waktu event dari exponential: T ~ Exp(lambda_i)
# Jika U ~ Uniform(0,1), maka T = -log(U)/lambda_i
u <- runif(n)
T_true <- -log(u) / lambda_i
# Tambahkan censoring acak
C <- rexp(n, rate = 0.005) # censoring times
time_obs <- pmin(T_true, C)
status_obs <- as.integer(T_true <= C)
sim_data <- data.frame(
time = time_obs,
status = status_obs,
x = factor(x, labels = c("Grup 0", "Grup 1"))
)
head(sim_data)
## time status x
## 1 24.26128 1 Grup 0
## 2 193.42511 0 Grup 1
## 3 24.96597 1 Grup 0
## 4 15.77729 1 Grup 1
## 5 23.09131 1 Grup 1
## 6 73.20734 1 Grup 0
table(sim_data$status)
##
## 0 1
## 74 226
Fitting Kaplan–Meier dan Cox:
# KM by group
fit_km_sim <- survfit(Surv(time, status) ~ x, data = sim_data)
ggsurvplot(
fit_km_sim,
data = sim_data,
risk.table = TRUE,
pval = TRUE,
title = "Kurva Kaplan–Meier (Data Simulasi)",
xlab = "Waktu",
ylab = "Probabilitas Survival"
)
# Model Cox
cox_sim <- coxph(Surv(time, status) ~ x, data = sim_data)
summary(cox_sim)
## Call:
## coxph(formula = Surv(time, status) ~ x, data = sim_data)
##
## n= 300, number of events= 226
##
## coef exp(coef) se(coef) z Pr(>|z|)
## xGrup 1 0.4405 1.5535 0.1341 3.286 0.00102 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## exp(coef) exp(-coef) lower .95 upper .95
## xGrup 1 1.554 0.6437 1.195 2.02
##
## Concordance= 0.565 (se = 0.019 )
## Likelihood ratio test= 10.73 on 1 df, p=0.001
## Wald test = 10.8 on 1 df, p=0.001
## Score (logrank) test = 10.97 on 1 df, p=9e-04
Perhatikan: - Hazard ratio hasil model Cox seharusnya mendekati nilai “benar” yaitu 2 (karena kita set \(eta = \log 2\)).
Dalam R Markdown ini kita telah membahas:
lung dan simulasi data
eksponensial.Silakan modifikasi, tambahkan penjelasan teoretis yang lebih dalam (misalnya derivasi MLE lengkap, link ke teori likelihood, atau perluasan ke model competing risks) sesuai kebutuhan materi kuliah atau modul pelatihan.
Pada analisis Kaplan–Meier dan uji log-rank, kita hanya
membandingkan kurva survival antar kelompok (misal obat
A vs B) satu per satu.
Jika ingin memasukkan banyak kovariat sekaligus (usia,
jenis kelamin, status merokok, jenis obat, dll.), kita membutuhkan
model regresi untuk waktu survival, yaitu:
Cox Proportional Hazards Regression (Cox PH).
Slide kuliah menggambarkan bahwa:
Kita ingin memodelkan hazard (laju kejadian sesaat) sebagai fungsi dari beberapa faktor risiko, sehingga kita dapat memahami bagaimana kovariat memengaruhi risiko kejadian pada setiap titik waktu.
Dokumen ini menyusun:
ovarian untuk model sederhana.coxph() di
R.Misalkan \(T\) adalah peubah acak waktu sampai kejadian (event), dengan \(T \ge 0\).
Fungsi hazard (instantaneous failure rate) didefinisikan sebagai:
\[ h(t) = \lim_{\Delta t \to 0} \frac{P(t \le T < t + \Delta t \mid T \ge t)}{\Delta t} \]
Jika \(T\) kontinu, hubungan penting:
\[ h(t) = \frac{f(t)}{S(t)} \]
dan
\[ S(t) = \exp\left(-\int_0^t h(u)\,du\right) \]
Misalkan kita punya vektor kovariat \(\mathbf{x} = (x_1,\dots,x_p)'\).
Model Cox PH menyatakan:
\[ h(t \mid \mathbf{x}) = h_0(t)\,\exp(\beta_1 x_1 + \beta_2 x_2 + \cdots + \beta_p x_p) \]
dengan:
Model ini semi-parametrik:
Ambil log di kedua sisi:
\[ \log h(t \mid \mathbf{x}) = \log h_0(t) + \beta_1 x_1 + \cdots + \beta_p x_p \]
Untuk satu kovariat \(x_k\):
\[ \text{HR}_k = \exp(\beta_k) \]
Jika \(\beta_k > 0\): HR > 1 →
hazard meningkat → survival memburuk.
Jika \(\beta_k < 0\): HR < 1 →
hazard menurun → survival membaik.
Slide memberikan contoh:
\[ \log \lambda(t) = \log \lambda_0(t) -5\,\text{gender} + 2\,\text{smokingStatus} + 0.01\,\text{Age} \]
smokingStatus → HR ≈ \(\exp(2) \approx 7.39\)Untuk dua subjek dengan kovariat \(\mathbf{x}_A\) dan \(\mathbf{x}_B\), hazard ratio:
\[ \frac{h(t \mid \mathbf{x}_A)}{h(t \mid \mathbf{x}_B)} = \frac{h_0(t)\exp(\beta'\mathbf{x}_A)}{h_0(t)\exp(\beta'\mathbf{x}_B)} = \exp\big(\beta'(\mathbf{x}_A - \mathbf{x}_B)\big) \]
Catatan penting:
Karena \(h_0(t)\) tidak dispesifikasikan, Cox mengusulkan partial likelihood yang:
Anggap:
Definisikan:
Probabilitas bahwa subjek \(i(k)\) yang mengalami event di antara semua anggota risk set \(R_k\) (dengan asumsi model Cox benar):
\[ P(\text{subjek } i(k) \text{ yang gagal pada } t_{(k)} \mid \text{seseorang gagal di } R_k) = \frac{h(t_{(k)} \mid \mathbf{x}_{i(k)})}{\sum_{l \in R_k} h(t_{(k)} \mid \mathbf{x}_l)} \]
Karena \(h(t \mid \mathbf{x}) = h_0(t)\exp(\beta'\mathbf{x})\), maka:
\[ \frac{h_0(t_{(k)})\exp(\beta'\mathbf{x}_{i(k)})} {\sum_{l \in R_k} h_0(t_{(k)})\exp(\beta'\mathbf{x}_l)} = \frac{\exp(\beta'\mathbf{x}_{i(k)})} {\sum_{l \in R_k} \exp(\beta'\mathbf{x}_l)} \]
Baseline hazard \(h_0(t)\) hilang dari rasio → inilah trik partial likelihood.
Dengan mengalikan probabilitas untuk semua waktu event:
\[ L_p(\beta) = \prod_{k=1}^K \frac{\exp(\beta'\mathbf{x}_{i(k)})} {\sum_{l \in R_k} \exp(\beta'\mathbf{x}_l)} \]
Log-partial likelihood:
\[ \ell_p(\beta) = \sum_{k=1}^K \left[ \beta'\mathbf{x}_{i(k)} - \log\left( \sum_{l \in R_k} \exp(\beta'\mathbf{x}_l) \right) \right] \]
Estimasi \(\hat{\beta}\) diperoleh dengan memaksimalkan \(\ell_p(\beta)\).
Turunan pertama (score vector):
\[ U(\beta) = \frac{\partial \ell_p(\beta)}{\partial \beta} = \sum_{k=1}^K \left[ \mathbf{x}_{i(k)} - \frac{\sum_{l \in R_k} \mathbf{x}_l \exp(\beta'\mathbf{x}_l)}{\sum_{l \in R_k} \exp(\beta'\mathbf{x}_l)} \right] \]
Istilah kedua adalah rata-rata kovariat berbobot pada risk set.
Turunan kedua (observed information matrix):
\[ I(\beta) = -\frac{\partial^2 \ell_p(\beta)}{\partial \beta \partial \beta'} = \sum_{k=1}^K \left[ \frac{\sum_{l \in R_k} \mathbf{x}_l \mathbf{x}_l' \exp(\beta'\mathbf{x}_l)}{\sum_{l \in R_k} \exp(\beta'\mathbf{x}_l)} - \left( \frac{\sum_{l \in R_k} \mathbf{x}_l \exp(\beta'\mathbf{x}_l)}{\sum_{l \in R_k} \exp(\beta'\mathbf{x}_l)} \right) \left( \frac{\sum_{l \in R_k} \mathbf{x}_l \exp(\beta'\mathbf{x}_l)}{\sum_{l \in R_k} \exp(\beta'\mathbf{x}_l)} \right)' \right] \]
Estimator Cox PH:
\[ \hat{\beta} = \arg\max_\beta \ell_p(\beta) \]
dan varian asimtotik:
\[ \widehat{\text{Var}}(\hat{\beta}) = I(\hat{\beta})^{-1} \]
ovarianSlide kuliah menggunakan data ovarian untuk ilustrasi
Kaplan–Meier dan log-rank. fileciteturn0file0
Kita juga akan memakainya untuk Cox PH (satu kovariat:
rx).
data(ovarian)
ovarian <- ovarian
head(ovarian)
## futime fustat age resid.ds rx ecog.ps
## 1 59 1 72.3315 2 1 1
## 2 115 1 74.4932 2 1 1
## 3 156 1 66.4658 2 1 2
## 4 421 0 53.3644 2 2 1
## 5 431 1 50.3397 2 1 1
## 6 448 0 56.4301 1 1 2
summary(ovarian$rx)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 1.0 1.0 1.5 1.5 2.0 2.0
# Ringkas jumlah event dan censoring per grup
ovarian %>%
group_by(rx) %>%
summarise(
n = n(),
event = sum(fustat),
censored = n() - sum(fustat)
)
## # A tibble: 2 × 4
## rx n event censored
## <dbl> <int> <dbl> <dbl>
## 1 1 13 7 6
## 2 2 13 5 8
Kita pasang model:
\[ h(t \mid \text{rx}) = h_0(t)\,\exp(\beta\,\text{rx}) \]
dengan:
rx = 1 → kelompok referensi (misalnya obat A)rx = 2 → obat Bcoxph()fit_cox <- coxph(Surv(futime, fustat) ~ rx, data = ovarian)
summary(fit_cox)
## Call:
## coxph(formula = Surv(futime, fustat) ~ rx, data = ovarian)
##
## n= 26, number of events= 12
##
## coef exp(coef) se(coef) z Pr(>|z|)
## rx -0.5964 0.5508 0.5870 -1.016 0.31
##
## exp(coef) exp(-coef) lower .95 upper .95
## rx 0.5508 1.816 0.1743 1.74
##
## Concordance= 0.608 (se = 0.07 )
## Likelihood ratio test= 1.05 on 1 df, p=0.3
## Wald test = 1.03 on 1 df, p=0.3
## Score (logrank) test = 1.06 on 1 df, p=0.3
Output penting:
rx.exp(coef) = hazard ratio (HR).se(coef) = standar error.z dan Pr(>|z|) = uji Wald.Kita simpan angka-angka tersebut:
beta_hat <- coef(fit_cox)["rx"]
se_beta <- sqrt(vcov(fit_cox)["rx", "rx"])
beta_hat
## rx
## -0.59638
se_beta
## [1] 0.5869895
HR <- exp(beta_hat)
CI_HR <- exp(beta_hat + c(-1, 1) * 1.96 * se_beta)
HR
## rx
## 0.5508019
CI_HR
## [1] 0.174317 1.740408
Di sini kita menunjukkan langkah manual sesuai formulasi partial likelihood.
library(survival)
library(dplyr)
data(ovarian)
# Buat data sederhana berisi waktu, status, dan rx
dat <- ovarian %>%
dplyr::select(futime, fustat, rx) %>%
arrange(futime)
head(dat, 10)
## futime fustat rx
## 1 59 1 1
## 2 115 1 1
## 3 156 1 1
## 22 268 1 1
## 23 329 1 1
## 24 353 1 2
## 25 365 1 2
## 26 377 0 2
## 4 421 0 2
## 5 431 1 1
Kita hanya menggunakan baris dengan event untuk partial likelihood; baris sensored hanya mempengaruhi risk set.
event_idx <- which(dat$fustat == 1)
event_times <- dat$futime[event_idx]
event_times
## [1] 59 115 156 268 329 353 365 431 464 475 563 638
length(event_times) # jumlah event K
## [1] 12
Untuk setiap event ke-\(k\) pada waktu \(t_k\):
rx untuk subjek yang event
(numerator).Untuk model 1 kovariat, \(\beta\) skalar.
# Fungsi untuk membangun tabel partial likelihood pada suatu nilai beta
build_partial_table <- function(beta, dat){
# dat harus sudah terurut naik berdasarkan futime
out_list <- list()
ev_rows <- which(dat$fustat == 1)
for(k in seq_along(ev_rows)){
i_event <- ev_rows[k]
t_k <- dat$futime[i_event]
# risk set: semua dengan futime >= t_k
R_k <- which(dat$futime >= t_k)
# kovariat untuk event dan untuk risk set
x_event <- dat$rx[i_event]
x_R <- dat$rx[R_k]
num <- exp(beta * x_event)
den <- sum(exp(beta * x_R))
out_list[[k]] <- data.frame(
k = k,
t_k = t_k,
i_event = i_event,
x_event = x_event,
R_size = length(R_k),
num = num,
den = den,
term = num / den # kontribusi L_p di waktu ke-k
)
}
do.call(rbind, out_list)
}
# Coba dengan beta = 0 (tidak ada efek rx)
partial_beta0 <- build_partial_table(beta = 0, dat = dat)
kable(head(partial_beta0, 10), digits = 4,
caption = "Kontribusi Partial Likelihood untuk \\(\\beta = 0\\) (10 event pertama)") %>%
kable_styling(full_width = FALSE)
| k | t_k | i_event | x_event | R_size | num | den | term |
|---|---|---|---|---|---|---|---|
| 1 | 59 | 1 | 1 | 26 | 1 | 26 | 0.0385 |
| 2 | 115 | 2 | 1 | 25 | 1 | 25 | 0.0400 |
| 3 | 156 | 3 | 1 | 24 | 1 | 24 | 0.0417 |
| 4 | 268 | 4 | 1 | 23 | 1 | 23 | 0.0435 |
| 5 | 329 | 5 | 1 | 22 | 1 | 22 | 0.0455 |
| 6 | 353 | 6 | 2 | 21 | 1 | 21 | 0.0476 |
| 7 | 365 | 7 | 2 | 20 | 1 | 20 | 0.0500 |
| 8 | 431 | 10 | 1 | 17 | 1 | 17 | 0.0588 |
| 9 | 464 | 12 | 2 | 15 | 1 | 15 | 0.0667 |
| 10 | 475 | 13 | 2 | 14 | 1 | 14 | 0.0714 |
Untuk \(\beta = 0\):
num = 1, den = ukuran risk set.
Partial likelihood \(L_p(\beta)\) dan log-partial likelihood:
Lp_beta0 <- prod(partial_beta0$term)
logLp_beta0 <- sum(log(partial_beta0$term))
Lp_beta0
## [1] 6.400788e-16
logLp_beta0
## [1] -34.98494
Sekarang kita gunakan \(\hat{\beta}\) dari coxph()
untuk melihat berapa nilai log-partial likelihood.
partial_betahat <- build_partial_table(beta = beta_hat, dat = dat)
kable(head(partial_betahat, 10), digits = 4,
caption = "Kontribusi Partial Likelihood untuk \\(\\beta = \\hat\\beta\\) (10 event pertama)") %>%
kable_styling(full_width = FALSE)
| k | t_k | i_event | x_event | R_size | num | den | term | |
|---|---|---|---|---|---|---|---|---|
| rx | 1 | 59 | 1 | 1 | 26 | 0.5508 | 11.1044 | 0.0496 |
| rx1 | 2 | 115 | 2 | 1 | 25 | 0.5508 | 10.5536 | 0.0522 |
| rx2 | 3 | 156 | 3 | 1 | 24 | 0.5508 | 10.0028 | 0.0551 |
| rx3 | 4 | 268 | 4 | 1 | 23 | 0.5508 | 9.4520 | 0.0583 |
| rx4 | 5 | 329 | 5 | 1 | 22 | 0.5508 | 8.9012 | 0.0619 |
| rx5 | 6 | 353 | 6 | 2 | 21 | 0.3034 | 8.3504 | 0.0363 |
| rx6 | 7 | 365 | 7 | 2 | 20 | 0.3034 | 8.0470 | 0.0377 |
| rx7 | 8 | 431 | 10 | 1 | 17 | 0.5508 | 7.1369 | 0.0772 |
| rx8 | 9 | 464 | 12 | 2 | 15 | 0.3034 | 6.0353 | 0.0503 |
| rx9 | 10 | 475 | 13 | 2 | 14 | 0.3034 | 5.7319 | 0.0529 |
Lp_betahat <- prod(partial_betahat$term)
logLp_betahat <- sum(log(partial_betahat$term))
Lp_betahat
## [1] 1.082813e-15
logLp_betahat
## [1] -34.45921
Bandingkan log-partial likelihood:
c(
logL_beta0 = logLp_beta0,
logL_betahat = logLp_betahat,
logL_coxph = fit_cox$loglik[2] # from coxph: loglik at beta_hat
)
## logL_beta0 logL_betahat logL_coxph
## -34.98494 -34.45921 -34.45921
Perbedaan kecil bisa muncul karena rounding atau penanganan ties (Cox PH default menggunakan metode Efron/Breslow jika ada ties).
Untuk parameter tunggal \(\beta\):
p-value:
\[ p = 2 \left[ 1 - \Phi\left( |Z_{\text{Wald}}| \right) \right] \]
Z_wald <- beta_hat / se_beta
p_wald <- 2 * (1 - pnorm(abs(Z_wald)))
Z_wald
## rx
## -1.015998
p_wald
## rx
## 0.3096304
95% Confidence Interval (CI) untuk \(\beta\):
\[ \hat{\beta} \pm z_{0.975} \,\text{SE}(\hat{\beta}) \]
CI untuk HR:
\[ \text{CI}_{\text{HR}} = \left[ \exp(\hat{\beta} - 1.96\,\text{SE}),\ \exp(\hat{\beta} + 1.96\,\text{SE}) \right] \]
CI_beta <- beta_hat + c(-1, 1) * 1.96 * se_beta
CI_beta
## [1] -1.7468795 0.5541194
exp(CI_beta)
## [1] 0.174317 1.740408
Hasilnya harus sama dengan CI yang ditampilkan oleh
summary(fit_cox).
rx →
log-likelihood \(\ell_1\).Statistik uji:
\[ \text{LRT} = -2(\ell_0 - \ell_1) \sim \chi^2_{(df)} \]
dengan \(df =\) banyaknya parameter kovariat (di sini 1).
loglik_null <- fit_cox$loglik[1]
loglik_full <- fit_cox$loglik[2]
LRT <- -2 * (loglik_null - loglik_full)
p_LRT <- 1 - pchisq(LRT, df = 1)
c(LRT = LRT, p_LRT = p_LRT)
## LRT p_LRT
## 1.0514531 0.3051727
Score test pada \(\beta = 0\) untuk
model 1 kovariat setara dengan uji log-rank.
R menghitungnya secara otomatis, tetapi di sini fokus kita adalah:
Dari output:
HR
## rx
## 0.5508019
CI_HR
## [1] 0.174317 1.740408
rx = 2 memiliki hazard lebih
tinggi dibanding rx = 1.rx = 2.Slide memberi contoh interpretasi:
“Hazard ratio 0.6 berarti risiko pasien dengan treatment A meninggal 40%
lebih kecil dibanding treatment B.” fileciteturn0file0
Ringkas dari slide: fileciteturn0file0
Poin-poin yang bisa langsung dipindah ke slide/handout:
Model Cox PH: \[ h(t \mid \mathbf{x}) = h_0(t)\,\exp(\beta_1 x_1 + \cdots + \beta_p x_p) \]
Hazard ratio: \[ \text{HR} = \exp(\beta_k) \] untuk kenaikan satu unit kovariat \(x_k\).
Partial likelihood: \[ L_p(\beta) = \prod_{k=1}^K \frac{\exp(\beta'\mathbf{x}_{i(k)})} {\sum_{l \in R_k} \exp(\beta'\mathbf{x}_l)} \]
Log-partial likelihood: \[ \ell_p(\beta) = \sum_{k=1}^K \left[ \beta'\mathbf{x}_{i(k)} - \log\left( \sum_{l \in R_k} \exp(\beta'\mathbf{x}_l) \right) \right] \]
Varian dan uji Wald: \[ \widehat{\text{Var}}(\hat{\beta}) = I(\hat{\beta})^{-1}, \quad Z_{\text{Wald}} = \frac{\hat{\beta}}{\text{SE}(\hat{\beta})} \]
CI 95% untuk HR: \[ \text{CI}_{\text{HR}} = \left[\exp(\hat{\beta} - 1.96\,\text{SE}),\ \exp(\hat{\beta} + 1.96\,\text{SE})\right] \]
Dengan R Markdown ini, Anda bisa:
coxph() sehingga
mahasiswa melihat jembatan antara rumus dan output software.