ANALISIS EPIDEMIOLOGIS DAN PERAMALAN KASUS HEPATITIS A DI PROVINSI DKI JAKARTA

Ditujukan Guna Memenuhi Ujian Akhir Semester Mata Kuliah Epidemiologi

Disusun oleh:

Muhammad Al Fauzan (140610230011)


Dosen Pengampu:
I Gede Nyoman Mindra Jaya, Ph.D




Abstrak

Penelitian ini bertujuan untuk menganalisis pola epidemiologis dan melakukan peramalan kasus Hepatitis A di Provinsi DKI Jakarta berdasarkan pendekatan spasial dan temporal. Data yang digunakan berasal dari Sistem Surveilans Penyakit Dinas Kesehatan Provinsi DKI Jakarta dan Badan Pusat Statistik (BPS) Provinsi DKI Jakarta periode 2017–2025. Analisis epidemiologis dilakukan melalui perhitungan incidence rate (IR) per 100.000 penduduk, analisis spasial antar kabupaten/kota, serta uji autokorelasi spasial menggunakan Moran’s I, sedangkan analisis temporal dilakukan dengan metode peramalan Holt, Exponential Smoothing (ETS), dan Autoregressive Integrated Moving Average (ARIMA). Hasil penelitian menunjukkan bahwa kasus Hepatitis A di Provinsi DKI Jakarta mengalami fluktuasi yang signifikan dengan puncak kasus pada tahun 2019 dan penurunan tajam pada tahun 2021, serta distribusi IR yang tidak merata antarwilayah pada tahun 2025 tanpa adanya autokorelasi spasial yang signifikan. Evaluasi peramalan menunjukkan bahwa metode Holt memberikan kinerja terbaik dibandingkan ETS dan ARIMA, sementara proyeksi 12 bulan ke depan mengindikasikan jumlah kasus relatif rendah hingga menengah dengan tingkat ketidakpastian yang cukup tinggi. Temuan ini diharapkan dapat menjadi dasar pendukung dalam perencanaan surveilans dan pengendalian Hepatitis A berbasis data di Provinsi DKI Jakarta.

Kata kunci: Hepatitis A, epidemiologi, incidence rate, analisis spasial, peramalan, DKI Jakarta.

Abstract

This study aims to analyze epidemiological patterns and forecast Hepatitis A cases in DKI Jakarta Province based on spatial and temporal approaches. The data used are from the Disease Surveillance System of the DKI Jakarta Provincial Health Office and the Central Statistics Agency (BPS) of DKI Jakarta Province for the period 2017–2025. Epidemiological analysis was conducted through calculating the incidence rate (IR) per 100,000 population, spatial analysis between districts/cities, and spatial autocorrelation tests using Moran’s I, while temporal analysis was conducted using the Holt forecasting method, Exponential Smoothing (ETS), and Autoregressive Integrated Moving Average (ARIMA). The results of the study indicate that Hepatitis A cases in DKI Jakarta Province experienced significant fluctuations with a peak in 2019 and a sharp decline in 2021, as well as an uneven distribution of IR between regions in 2025 without significant spatial autocorrelation. Forecasting evaluations showed that the Holt method performed better than ETS and ARIMA, while 12-month projections indicated a relatively low to medium number of cases with a high degree of uncertainty. These findings are expected to provide a basis for data-driven Hepatitis A surveillance and control planning in Jakarta Province.

Keywords: Hepatitis A, epidemiology, incidence rate, spatial analysis, forecasting, DKI Jakarta.



DAFTAR ISI
ABSTRAK
1
DAFTAR ISI
2
BAB I PENDAHULUAN
3
1.1 Latar Belakang
3
1.2 Rumusan Masalah
3
1.3 Tujuan Penelitian
4
BAB II TINJAUAN PUSTAKA
5
2.1 Konsep Dasar Epidemiologi
5
2.2 Ukuran Epidemiologi Incidence Rate (IR)
7
2.3 Analisis Spasial dalam Epidemiologi
8
2.4 Analisis Temporal dan Peramalan Deret Waktu
8
BAB III METODOLOGI PENELITIAN
10
3.1 Desain dan Pendekatan Penelitian
10
3.2 Sumber dan Jenis Data
10
3.3 Variabel Penelitian
10
3.4 Metode Analisis Data
11
3.5 Alur Kerja Penelitian
12
BAB IV HASIL DAN PEMBAHASAN
13
4.1 Data Penelitian dan Implikasi Epidemiologis
13
4.2 Statistik Deskriptif dan Interpretasi Epidemiologis
13
4.3 Analisis Spasial Incidence Rate
15
4.4 Analisis Temporal
17
4.5 Evaluasi Forecast
18
4.6 Evaluasi 12 Bulan ke Depan
19
4.7 Sintesis Temuan Utama dan Implikasi Kebijakan
22
BAB V PENUTUP
23
5.1 Kesimpulan
23
5.2 Saran
24
5.3 Ucapan Terima Kasih
24
DAFTAR PUSTAKA
26
LAMPIRAN
27



BAB I
PENDAHULUAN
1.1 Latar Belakang

Hepatitis A merupakan penyakit infeksi akut pada hati yang disebabkan oleh virus Hepatitis A (HAV) dan umumnya ditularkan melalui jalur fekal–oral, terutama melalui konsumsi makanan dan minuman yang terkontaminasi [1]. Penyakit ini masih menjadi masalah kesehatan masyarakat di berbagai negara berkembang, termasuk Indonesia, terutama pada wilayah dengan kepadatan penduduk tinggi dan sanitasi lingkungan yang belum optimal [2].

Di Indonesia, Hepatitis A secara berkala menimbulkan kejadian luar biasa (KLB), khususnya di lingkungan sekolah, asrama, dan permukiman padat penduduk [3]. Provinsi DKI Jakarta sebagai pusat aktivitas ekonomi dan pemerintahan memiliki karakteristik epidemiologis yang unik, yaitu kepadatan penduduk yang sangat tinggi, mobilitas penduduk yang intens, serta heterogenitas kondisi sanitasi dan perilaku hidup bersih dan sehat antarwilayah administratif [4]. Kondisi tersebut berpotensi meningkatkan risiko penularan penyakit berbasis lingkungan, termasuk Hepatitis A.

Pendekatan epidemiologi memegang peranan penting dalam memahami pola kejadian Hepatitis A melalui analisis distribusi orang, tempat, dan waktu [5]. Salah satu indikator utama yang digunakan adalah incidence rate (IR), yang menggambarkan tingkat kejadian kasus baru dalam suatu populasi pada periode tertentu [6]. Analisis IR secara spasial antar kabupaten/kota memungkinkan identifikasi wilayah dengan risiko relatif lebih tinggi, sehingga dapat mendukung penentuan prioritas intervensi kesehatan masyarakat [7].

Selain analisis deskriptif dan spasial, pemanfaatan analisis deret waktu (time series) menjadi semakin penting dalam epidemiologi modern. Analisis temporal memungkinkan peneliti untuk memahami tren, fluktuasi musiman, serta lonjakan kasus yang dapat mengindikasikan terjadinya wabah [8]. Lebih lanjut, peramalan (forecasting) kasus penyakit menular menggunakan metode statistik seperti Holt, Exponential Smoothing (ETS), dan Autoregressive Integrated Moving Average (ARIMA) telah banyak digunakan untuk mendukung sistem peringatan dini dan perencanaan sumber daya kesehatan [9][10].

Meskipun data Hepatitis A di Provinsi DKI Jakarta telah tersedia melalui Sistem Surveilans Penyakit Dinas Kesehatan Provinsi DKI Jakarta dan data penduduk dari Badan Pusat Statistik (BPS), pemanfaatan data tersebut meliputi analisis epidemiologis, spasial, dan peramalan masih relatif terbatas dalam kajian akademik. Oleh karena itu, diperlukan suatu penelitian yang mengintegrasikan pendekatan epidemiologi deskriptif, analisis spasial, dan peramalan deret waktu untuk memberikan gambaran yang komprehensif mengenai dinamika Hepatitis A di Provinsi DKI Jakarta. Penelitian ini yang diharapkan dapat memberikan kontribusi ilmiah serta menjadi masukan bagi pengambilan kebijakan kesehatan masyarakat berbasis data.

1.2 Rumusan Masalah

Berdasarkan latar belakang yang telah diuraikan, rumusan masalah dalam penelitian ini adalah sebagai berikut:

  1. Bagaimana perkembangan jumlah kasus dan incidence rate (IR) Hepatitis A di Provinsi DKI Jakarta selama periode 2017–2025?
  2. Bagaimana distribusi spasial incidence rate Hepatitis A antar kabupaten/kota di Provinsi DKI Jakarta?
  3. Apakah terdapat indikasi autokorelasi spasial pada distribusi incidence rate Hepatitis A antarwilayah?
  4. Bagaimana pola temporal kasus Hepatitis A bulanan di Provinsi DKI Jakarta?
  5. Metode peramalan manakah (Holt, ETS, atau ARIMA) yang memberikan kinerja terbaik dalam memprediksi kasus Hepatitis A?
  6. Bagaimana proyeksi jumlah kasus Hepatitis A di Provinsi DKI Jakarta untuk 12 bulan ke depan?
1.3 Tujuan Penelitian

Tujuan umum penelitian ini adalah untuk menganalisis pola epidemiologis dan melakukan peramalan kasus Hepatitis A di Provinsi DKI Jakarta berdasarkan data surveilans dan data kependudukan resmi. Secara khusus, penelitian ini bertujuan untuk:

  1. Menghitung dan menganalisis incidence rate (IR) Hepatitis A di Provinsi DKI Jakarta.
  2. Mengkaji distribusi spasial IR Hepatitis A antar kabupaten/kota.
  3. Menguji adanya autokorelasi spasial menggunakan pendekatan statistik spasial.
  4. Menganalisis pola temporal kasus Hepatitis A bulanan.
  5. Membandingkan kinerja metode peramalan Holt, ETS, dan ARIMA.
  6. Menyusun proyeksi kasus Hepatitis A untuk mendukung perencanaan kesehatan masyarakat.


BAB II
TINJAUAN PUSTAKA
2.1 Konsep Dasar Epidemiologi

Epidemiologi didefinisikan sebagai ilmu yang mempelajari distribusi dan determinan kejadian penyakit dalam populasi serta penerapannya untuk pengendalian masalah kesehatan [5]. Pendekatan epidemiologi menekankan analisis berdasarkan tiga dimensi utama, yaitu orang (person), tempat (place), dan waktu (time) [6].

2.1.1 Model Agent–Host–Environment

Model agent–host–environment merupakan kerangka klasik dalam epidemiologi penyakit menular [11].

Gambar 1. Agent-Host-Evirontment Penyakit Hepatitis A
Gambar 1. Agent-Host-Evirontment Penyakit Hepatitis A

1. Agent (Agen Penyebab)
Agent pada kasus Hepatitis A adalah Virus Hepatitis A (Hepatitis A Virus/HAV), yaitu virus RNA dari famili Picornaviridae dan genus Hepatovirus. Virus ini bersifat tahan terhadap kondisi lingkungan, mampu bertahan hidup dalam air dan makanan yang terkontaminasi, serta resisten terhadap suhu rendah dan kondisi asam ringan. HAV ditularkan terutama melalui jalur fekal–oral, baik melalui konsumsi makanan dan minuman yang terkontaminasi tinja penderita, maupun melalui kontak langsung antarindividu. Masa inkubasi yang relatif panjang, berkisar 15–50 hari, menyebabkan infeksi sering tidak terdeteksi sejak awal, sehingga memungkinkan terjadinya penularan lanjutan sebelum gejala klinis muncul. Selain itu, infeksi HAV pada anak sering bersifat asimtomatik atau ringan, yang semakin meningkatkan potensi penyebaran virus di masyarakat.

2. Host (Inang)
Host pada Hepatitis A adalah manusia yang rentan terhadap infeksi HAV, dengan tingkat kerentanan yang dipengaruhi oleh faktor biologis, perilaku, dan imunitas. Kelompok host yang berisiko tinggi meliputi anak-anak usia sekolah, remaja, serta individu dewasa yang belum memiliki kekebalan terhadap HAV, baik melalui infeksi sebelumnya maupun vaksinasi.

Perilaku host memegang peranan penting dalam penularan penyakit, terutama kebiasaan higiene personal yang kurang baik, seperti tidak mencuci tangan sebelum makan atau setelah buang air besar. Di wilayah perkotaan padat seperti DKI Jakarta, mobilitas penduduk yang tinggi serta interaksi sosial yang intens, misalnya di sekolah, tempat kerja, dan pusat kuliner, meningkatkan peluang kontak antara individu rentan dengan sumber infeksi. Tingkat keparahan gejala juga cenderung lebih berat pada usia dewasa dibandingkan anak-anak, sehingga beban penyakit pada kelompok usia produktif menjadi lebih signifikan.

3. Environment (Lingkungan)
Environment atau lingkungan berperan sebagai faktor pendukung utama dalam transmisi Hepatitis A. Lingkungan dengan sanitasi yang tidak memadai, akses air bersih yang terbatas, serta pengelolaan limbah yang kurang baik menciptakan kondisi ideal bagi penyebaran HAV. Kontaminasi sumber air dan makanan oleh tinja yang mengandung virus menjadi jalur penularan yang dominan.

Kondisi kepadatan penduduk yang tinggi di DKI Jakarta memperbesar risiko terjadinya klaster kasus, terutama di kawasan permukiman padat, sekolah, dan pusat aktivitas ekonomi. Selain itu, keberadaan pedagang makanan dengan standar higiene yang tidak konsisten dapat menjadi sumber penularan tidak langsung. Faktor lingkungan ini menjelaskan mengapa distribusi kasus Hepatitis A sering kali tidak merata secara spasial antarwilayah, sebagaimana terlihat pada variasi incidence rate (IR) antar kabupaten/kota.

Gambar 2. Diagram Alir Proses Agent-Host-Evirontment pada Penyakit Hepatitis A
Gambar 2. Diagram Alir Proses Agent-Host-Evirontment pada Penyakit Hepatitis A

Terjadinya kasus Hepatitis A merupakan hasil interaksi dinamis antara virus HAV yang mampu bertahan di lingkungan, host yang rentan akibat kurangnya imunitas dan perilaku berisiko, serta lingkungan yang mendukung transmisi melalui sanitasi dan kepadatan penduduk. Oleh karena itu, pengendalian Hepatitis A tidak dapat hanya berfokus pada satu komponen saja, melainkan memerlukan pendekatan terpadu melalui peningkatan higiene dan sanitasi lingkungan, perlindungan host melalui edukasi dan vaksinasi, serta penguatan sistem surveilans untuk mendeteksi dan merespons penularan sejak dini. Hepatitis A adalah penyakit hati akut yang umumnya bersifat self-limiting, namun dapat menyebabkan beban kesehatan yang signifikan ketika terjadi wabah [1]. Penularan terutama terjadi melalui konsumsi makanan atau air yang terkontaminasi, serta kontak dekat antarindividu [12]. Studi sebelumnya menunjukkan bahwa wilayah perkotaan padat penduduk memiliki risiko lebih tinggi terhadap wabah Hepatitis A [4][13].

2.2 Ukuran Epidemiologi Incidence Rate (IR)

Incidence rate merupakan ukuran frekuensi kejadian kasus baru dalam suatu populasi pada periode tertentu [6]. Ukuran ini banyak digunakan dalam studi epidemiologi penyakit menular untuk membandingkan risiko antarwilayah dan antarperiode waktu [7][14].

2.3 Analisis Spasial dalam Epidemiologi

Analisis spasial digunakan untuk mengkaji pola geografis kejadian penyakit dan mengidentifikasi area berisiko tinggi [15]. Salah satu metode yang umum digunakan adalah Moran’s I, yang mengukur derajat autokorelasi spasial global [16]. Nilai Moran’s I yang signifikan menunjukkan adanya pengelompokan (clustering) spasial, sedangkan nilai yang tidak signifikan mengindikasikan distribusi yang relatif acak.

2.4 Analisis Temporal dan Peramalan Deret Waktu

Analisis temporal dalam epidemiologi bertujuan untuk memahami pola kejadian penyakit berdasarkan dimensi waktu, termasuk tren jangka panjang, fluktuasi musiman, dan perubahan mendadak akibat kejadian luar biasa seperti kejadian luar biasa atau intervensi kesehatan masyarakat [8]. Pendekatan ini penting untuk mengidentifikasi periode peningkatan risiko, mengevaluasi efektivitas kebijakan pengendalian penyakit, serta mendukung pengambilan keputusan berbasis bukti.

Dalam konteks penyakit menular seperti Hepatitis A, analisis temporal memiliki peran strategis karena penyakit ini sering menunjukkan fluktuasi kasus yang dipengaruhi oleh faktor lingkungan, perilaku masyarakat, serta kondisi sanitasi. Oleh karena itu, peramalan deret waktu (time series forecasting) digunakan untuk memperkirakan jumlah kasus di masa mendatang berdasarkan pola historis, sehingga dapat dimanfaatkan sebagai bagian dari sistem peringatan dini (early warning system) dan perencanaan intervensi kesehatan yang lebih proaktif [9].

Berbagai metode peramalan deret waktu telah dikembangkan, mulai dari metode smoothing hingga model berbasis stokastik. Dalam penelitian ini, digunakan tiga metode utama yang umum diaplikasikan dalam epidemiologi, yaitu metode Holt, Exponential Smoothing (ETS), dan Autoregressive Integrated Moving Average (ARIMA).

2.4.1 Metode Holt

Metode Holt merupakan pengembangan dari simple exponential smoothing yang dirancang untuk menangani data deret waktu yang memiliki pola tren, tetapi tidak mengandung komponen musiman [10]. Metode ini memperhitungkan dua komponen utama, yaitu level (tingkat rata-rata) dan tren (arah perubahan data), yang diperbarui secara eksponensial pada setiap periode waktu. Keunggulan metode Holt terletak pada kesederhanaan model dan kemampuannya dalam menangkap perubahan tren secara adaptif. Dalam konteks epidemiologi, metode Holt sering digunakan untuk memodelkan penyakit dengan pola tren jangka panjang yang relatif stabil, tanpa fluktuasi musiman yang kuat. Selain itu, metode ini tidak memerlukan proses transformasi atau differencing data, sehingga relatif mudah diterapkan dan diinterpretasikan [11]. Namun demikian, metode Holt memiliki keterbatasan ketika data menunjukkan pola musiman yang kompleks atau perubahan struktural yang tajam. Oleh karena itu, evaluasi kinerja model tetap diperlukan untuk memastikan bahwa metode ini sesuai dengan karakteristik data kasus Hepatitis A yang dianalisis.

2.4.2 Exponential Smoothing (ETS)

Exponential Smoothing atau ETS merupakan keluarga model peramalan yang lebih fleksibel dibandingkan metode Holt, karena mampu mengakomodasi berbagai kombinasi komponen level, tren, dan musiman secara otomatis [9]. Model ETS dinyatakan dalam notasi (Error, Trend, Seasonal), yang memungkinkan penyesuaian bentuk error (aditif atau multiplikatif), tren, serta pola musiman sesuai dengan karakteristik data. Dalam aplikasi epidemiologi, ETS banyak digunakan karena kemampuannya untuk menangkap pola kompleks pada data penyakit menular, termasuk fluktuasi musiman yang sering terjadi akibat perubahan cuaca, mobilitas penduduk, atau kebiasaan sosial [12]. Pemilihan struktur model ETS biasanya dilakukan secara otomatis menggunakan kriteria informasi, seperti Akaike Information Criterion (AIC), sehingga model yang dihasilkan bersifat data-driven. Kelebihan utama ETS adalah kemampuannya menghasilkan interval prediksi yang informatif, yang sangat berguna dalam konteks kesehatan masyarakat untuk menggambarkan ketidakpastian peramalan. Meskipun demikian, ETS tetap bersifat univariat dan tidak secara eksplisit memodelkan hubungan kausal dengan faktor eksternal, sehingga interpretasi hasil harus dilakukan dengan hati-hati.

2.4.3 Autoregressive Integrated Moving Average (ARIMA)

Model Autoregressive Integrated Moving Average (ARIMA) merupakan salah satu model deret waktu yang paling banyak digunakan dalam analisis epidemiologi [17]. Model ini mengombinasikan tiga komponen utama, yaitu autoregressive (AR), integrated (I), dan moving average (MA), yang masing-masing berfungsi untuk menangkap ketergantungan data terhadap nilai masa lalu, mengatasi ketidakstasioneran melalui differencing, serta memodelkan error acak. Keunggulan ARIMA terletak pada kerangka teoritis yang kuat dan kemampuannya dalam menangani data deret waktu yang tidak stasioner, yang sering dijumpai pada data kasus penyakit menular. Dalam praktik epidemiologi, ARIMA banyak digunakan untuk peramalan penyakit seperti influenza, dengue, dan hepatitis, baik dalam skala regional maupun nasional [18]. Proses pemodelan ARIMA melibatkan beberapa tahap penting, yaitu identifikasi orde model, pengujian stasioneritas, estimasi parameter, serta evaluasi diagnostik residu. Meskipun model ini relatif kompleks dibandingkan metode smoothing, ARIMA sering memberikan hasil peramalan yang kompetitif, terutama ketika data menunjukkan pola autocorrelation yang kuat. Dalam penelitian ini, model ARIMA digunakan sebagai pembanding terhadap metode smoothing untuk mengevaluasi metode peramalan yang paling sesuai dengan karakteristik temporal kasus Hepatitis A di Provinsi DKI Jakarta.



BAB III
METODOLOGI PENELITIAN
3.1 Desain dan Pendekatan Penelitian

Penelitian ini menggunakan desain penelitian deskriptif kuantitatif dengan pendekatan cross-sectional dan time series. Pendekatan cross-sectional digunakan untuk menggambarkan distribusi dan tingkat kejadian kasus Hepatitis A antarwilayah administrasi kabupaten/kota di Provinsi DKI Jakarta pada periode tertentu, sedangkan pendekatan time series digunakan untuk menganalisis pola temporal serta melakukan peramalan jumlah kasus Hepatitis A berdasarkan data runtun waktu bulanan.

Pendekatan deskriptif dipilih karena penelitian ini bertujuan untuk memotret kondisi epidemiologis yang terjadi, bukan untuk menguji hubungan kausal antarvariabel. Selain itu, kombinasi analisis epidemiologis dan peramalan deret waktu memungkinkan penelitian ini tidak hanya memberikan gambaran beban penyakit saat ini, tetapi juga memproyeksikan kecenderungan kasus di masa mendatang sebagai bagian dari sistem kewaspadaan dini penyakit menular.

3.2 Sumber dan Jenis Data

Penelitian ini menggunakan data sekunder yang diperoleh dari instansi resmi pemerintah, sehingga memiliki tingkat validitas dan reliabilitas yang tinggi. Adapun sumber data yang digunakan adalah sebagai berikut:

3.2.1 Data Jumlah Penduduk

Data jumlah penduduk diperoleh dari Badan Pusat Statistik (BPS) Provinsi DKI Jakarta, yang memuat informasi jumlah penduduk menurut kabupaten/kota dan tahun pengamatan. Data ini digunakan sebagai populasi berisiko (denominator) dalam perhitungan ukuran epidemiologis incidence rate Hepatitis A.

3.2.2 Data Kasus Hepatitis A

Data kasus Hepatitis A diperoleh dari Sistem Surveilans Penyakit Dinas Kesehatan Provinsi DKI Jakarta, yang mencatat jumlah kasus Hepatitis A secara berkala berdasarkan wilayah administrasi dan waktu kejadian. Data ini digunakan sebagai numerator dalam analisis epidemiologis serta sebagai data utama dalam analisis deret waktu dan peramalan.

3.3 Variabel Penelitian

Variabel yang digunakan dalam penelitian ini terdiri atas variabel utama dan variabel turunan, yang dijelaskan sebagai berikut:

3.3.1 Jumlah Kasus Hepatitis A

Jumlah kasus Hepatitis A yang dilaporkan pada setiap wilayah kabupaten/kota dan periode waktu tertentu. Variabel ini merupakan variabel utama dalam analisis epidemiologis dan peramalan.

3.3.2 Jumlah Penduduk

Jumlah penduduk pada masing-masing wilayah kabupaten/kota di Provinsi DKI Jakarta. Variabel ini digunakan sebagai populasi berisiko dalam perhitungan incidence rate.

3.3.3 Incidence Rate (IR)

Variabel turunan yang menggambarkan tingkat kejadian kasus baru Hepatitis A pada populasi berisiko dalam periode waktu tertentu. IR digunakan untuk membandingkan risiko kejadian Hepatitis A antarwilayah dengan ukuran populasi yang berbeda.

3.3.4 Waktu (Bulan dan Tahun)

Variabel waktu yang digunakan dalam analisis deret waktu untuk mengidentifikasi pola tren, fluktuasi, dan melakukan peramalan jumlah kasus Hepatitis A.

3.4 Metode Analisis Data

Analisis data dalam penelitian ini dilakukan melalui beberapa tahapan analisis epidemiologis dan statistika sebagai berikut:

3.4.1 Perhitungan Incidence Rate (IR)

Incidence Rate digunakan untuk menggambarkan tingkat kejadian Hepatitis A dengan mempertimbangkan ukuran populasi pada masing-masing wilayah. Perhitungan incidence rate dilakukan menggunakan rumus:

\[IR = \frac{Jumlah\ Kasus\ Hepatitis\ A}{Jumlah\ Penduduk} \times 100.000\]

Nilai incidence rate dinyatakan per 100.000 penduduk agar mudah diinterpretasikan dan memungkinkan perbandingan risiko kejadian Hepatitis A antar kabupaten/kota di Provinsi DKI Jakarta secara objektif.

3.4.2 Analisis Deret Waktu Kasus Hepatitis A

Analisis deret waktu dilakukan terhadap data jumlah kasus Hepatitis A bulanan untuk mengidentifikasi pola tren dan fluktuasi kasus dari waktu ke waktu. Tahapan analisis deret waktu meliputi:

  1. Penyusunan data dalam format runtun waktu bulanan.
  2. Identifikasi pola tren dan musiman secara visual.
  3. Pemilihan metode peramalan yang sesuai dengan karakteristik data.
3.4.3 Metode Peramalan

Untuk memprediksi jumlah kasus Hepatitis A pada periode mendatang, penelitian ini menggunakan tiga metode peramalan deret waktu, yaitu:

  1. Autoregressive Integrated Moving Average (ARIMA)
    Metode ARIMA digunakan untuk memodelkan data runtun waktu berdasarkan komponen autoregressive, differencing, dan moving average. Pemilihan model dilakukan dengan mempertimbangkan stasioneritas data serta kriteria informasi seperti Akaike Information Criterion (AIC).
  1. Exponential Smoothing (ETS)
    Metode ETS digunakan untuk menangkap komponen level, tren, dan musiman pada data runtun waktu melalui pendekatan pemulusan eksponensial. Metode ini bersifat adaptif terhadap perubahan pola data dan banyak digunakan dalam peramalan kasus penyakit menular.
  1. Holt
    Metode Holt merupakan pengembangan dari exponential smoothing yang secara khusus menangani data dengan komponen tren. Metode ini digunakan sebagai pembanding untuk mengevaluasi kestabilan dan sensitivitas hasil peramalan.

Hasil peramalan dari ketiga metode dibandingkan untuk menilai kecenderungan jumlah kasus Hepatitis A serta mengevaluasi metode yang memberikan performa terbaik.

3.5 Alur Kerja Penelitian

Alur kerja penelitian ini disusun secara sistematis agar proses analisis dapat dilakukan secara terstruktur, dengan tahapan sebagai berikut:

  1. Pengumpulan data, Mengumpulkan data jumlah penduduk dari BPS Provinsi DKI Jakarta dan data kasus Hepatitis A dari sistem surveilans Dinas Kesehatan Provinsi DKI Jakarta.
  2. Pembersihan dan pengolahan data, Meliputi pemeriksaan kelengkapan data, konsistensi logis data, serta penyusunan data dalam format cross-sectional dan time series.
  3. Perhitungan incidence rate, Menghitung incidence rate Hepatitis A per 100.000 penduduk pada masing-masing wilayah kabupaten/kota.
  4. Analisis deret waktu dan peramalan, Melakukan pemodelan deret waktu dan peramalan jumlah kasus Hepatitis A menggunakan metode ARIMA, ETS, dan Holt.
  5. Interpretasi dan Pelaporan Hasil, Menginterpretasikan hasil analisis epidemiologis dan peramalan dalam bentuk tabel, grafik, dan narasi untuk mendukung pengambilan keputusan berbasis data.


BAB IV
HASIL DAN PEMBAHASAN
4.1 Data Penelitian dan Implikasi Epidemiologis
4.1.1 Struktur Data dan Ukuran Epidemiologis

Data penelitian ini mencakup jumlah kasus (Cases), jumlah penduduk (Population), dan Incidence Rate (IR) per 100.000 penduduk. Ukuran epidemiologis tersebut dihitung menggunakan formula:

\[IR = \frac{Jumlah\ Kasus\ Hepatitis\ A}{Jumlah\ Penduduk} \times 100.000\]

IR merupakan ukuran risiko kejadian baru yang dinormalisasi oleh populasi sehingga lebih tepat untuk membandingkan antarwilayah (kab/kota) maupun antarwaktu, terutama ketika jumlah penduduk berbeda besar. Jika hanya melihat jumlah kasus, wilayah berpenduduk besar cenderung tampak “lebih buruk”, padahal risikonya bisa lebih kecil. Karena itu, IR penting sebagai dasar prioritas intervensi (misalnya pengetatan higiene pangan, penyelidikan epidemiologi, dan edukasi perilaku hidup bersih).

4.1.2 Data Cross-Section dan Variasi Risiko

Contoh tahun 2017 menunjukkan variasi IR antarwilayah (misalnya Jakarta Selatan 4,89 vs Jakarta Utara 0,618; Kepulauan Seribu 0). Variasi IR menunjukkan perbedaan risiko yang mungkin dipengaruhi oleh kepadatan, mobilitas, perilaku hygiene, serta paparan melalui makanan/air. Nilai IR = 0 (misalnya Kepulauan Seribu pada tahun tertentu) dapat bermakna tidak ada kasus terlapor, namun secara surveilans perlu diingat bahwa nol laporan tidak selalu berarti nol kejadian (potensi under-reporting, akses layanan, atau perubahan definisi/ketepatan pelaporan).

4.2 Statistik Deskriptif dan Interpretasi Epidemiologis
4.2.1 Ringkasan Tahunan Provinsi (Agregasi Kabupaten/Kota)

Berikut adalah ringkasan perkembangan kasus Hepatitis A di Provinsi DKI Jakarta selama periode 2017–2025:

Tabel 4.1. Ringkasan Tahunan Kasus, Penduduk, dan IR Hepatitis A di Provinsi DKI Jakarta (2017-2025)
Tahun Kasus Penduduk IR per 100.000
2017 319 10.374.235 3,07
2018 453 10.467.629 4,33
2019 685 10.557.810 6,49
2020 303 10.562.088 2,87
2021 74 10.605.437 0,70
2022 139 10.640.007 1,31
2023 147 10.672.100 1,38
2024 146 10.685.000 1,37
2025 220 10.677.975 2,06

Puncak 2019 (IR 6,49) mengindikasikan periode peningkatan risiko yang kuat dan secara epidemiologi konsisten dengan potensi terjadinya klaster (misalnya paparan bersama melalui makanan/air atau transmisi pada setting komunal seperti sekolah/asrama/lingkungan padat). Penurunan tajam hingga 2021 (IR 0,70) dapat mengindikasikan penurunan transmisi, tetapi juga perlu dipertimbangkan faktor surveilans (perubahan perilaku mencari layanan, perubahan pelaporan, atau dinamika kegiatan masyarakat). Kenaikan kembali pada 2025 (IR 2,06) menunjukkan risiko yang tidak kembali setinggi 2019, namun cukup untuk menandakan perlunya penguatan surveilans dan pencegahan, karena Hepatitis A mudah membentuk klaster ketika kondisi sanitasi/higiene menurun.

4.2.2 Statistik Deskriptif Provinsi (2017-2025)
Tabel 4.2. Statistik Deskriptif Provinsi
Indikator Kasus IR per 100.000
Minimum 74 0,698
Maksimum 685 6,49
Mean 276 2,62
Median 220 2,06
Range 611 5,79

Range IR yang besar (5,79) menunjukkan volatilitas risiko antar tahun; ini penting karena penyakit berbasis fekal–oral sangat sensitif terhadap perubahan sanitasi, kejadian kontaminasi, dan peristiwa klaster. Median IR (2,06) lebih rendah dari maksimum jauh menandakan adanya tahun “outlier” yang sangat tinggi (2019). Dalam epidemiologi, outlier seperti ini sering terkait kejadian terfokus (KLB) alih-alih peningkatan gradual.

4.2.3 Statistik Deskriptif Kabupaten/Kota (Akumulasi 2017–2025)
Tabel 4.2. Statistik Deskriptif Provinsi
Indikator Kasus IR per 100.000
Minimum 0 0
Maksimum 635 3,82
Mean 414 2,28
Median 520 2,44
Range 635 3,82

Perbedaan IR antar wilayah menunjukkan heterogenitas risiko: beberapa wilayah mungkin lebih sering mengalami paparan (misalnya rantai makanan, kepadatan hunian, sanitasi lingkungan). Nilai IR maksimum akumulatif 3,82 (selama 9 tahun) menandakan wilayah tersebut secara konsisten menyumbang risiko relatif lebih tinggi sepanjang periode pengamatan, wilayah seperti ini layak menjadi prioritas investigasi faktor risiko lokal dan intervensi higiene-sanitasi yang lebih intensif.

4.3 Analisis Spasial Incidence Rate

Pemetaan dilakukan untuk melihat sebaran risiko pada tahun terbaru (2025).

Gambar 3. Peta Incidence Rate (IR) Hepatitis A per 100.000 Penduduk – DKI Jakarta (2025)
Gambar 3. Peta Incidence Rate (IR) Hepatitis A per 100.000 Penduduk – DKI Jakarta (2025)
4.3.1 Ranking IR Kabupaten/Kota Tahun 2025
Tabel 4.4. Ranking IR Hepatitis A Kabupaten/Kota Tahun 2025
Peringkat Wilayah Kasus Penduduk IR per 100.000
1 Jakarta Barat 118 2.487.199 4,74
2 Jakarta Utara 48 1.819.009 2,64
3 Jakarta Timur 31 3.085.058 1,00
4 Jakarta Pusat 10 1.038.396 0,963
5 Jakarta Selatan 13 2.219.225 0,586
6 Kepulauan Seribu 0 29.088 0

Jakarta Barat (IR 4,74) menjadi wilayah dengan risiko tertinggi pada 2025. Dalam konteks Hepatitis A (fekal–oral), IR tinggi dapat mencerminkan kombinasi: kepadatan, mobilitas, kerentanan higiene pangan, serta potensi paparan pada kelompok komunal. Ini menjadi sinyal prioritas untuk: peningkatan inspeksi higiene pangan, edukasi cuci tangan, dan investigasi klaster (misalnya jika kenaikan terjadi pada waktu yang berdekatan). Jakarta Utara (IR 2,64) sebagai peringkat kedua menunjukkan adanya “kantong” risiko tinggi lainnya. Dalam epidemiologi perkotaan, dua wilayah berisiko tinggi secara bersamaan dapat mengindikasikan adanya faktor struktural yang mirip (misalnya rantai distribusi pangan, kepadatan, atau kondisi sanitasi tertentu). Wilayah dengan IR rendah (Jakarta Selatan 0,586) bukan berarti aman sepenuhnya; IR rendah dapat juga dipengaruhi oleh variasi perilaku pelaporan, akses layanan, atau perbedaan intensitas surveilans. Namun, secara program, wilayah dengan IR rendah dapat dijadikan benchmark untuk menelaah praktik pencegahan yang mungkin lebih baik.

4.3.2 Autokorelasi Spasial Global (Moran’s I)

Uji Moran’s I menghasilkan nilai \(-0,1249\) dengan p-value \(0,1961\). Tidak signifikannya autokorelasi spasial positif menunjukkan bahwa pada skala kab/kota, peningkatan risiko tidak membentuk pola “mengelompok” yang kuat secara statistik. Secara epidemiologis, ini bisa berarti penularan/kejadian lebih dipengaruhi paparan spesifik (misalnya klaster institusional atau sumber makanan tertentu), bukan penyebaran gradual antar wilayah bertetangga atau unit analisis terlalu besar (kab/kota) sehingga klaster mikro (kelurahan/kecamatan) tidak tertangkap. Mengingat hanya ada 6 unit wilayah, daya uji statistik juga terbatas. Dengan demikian, rekomendasi yang lebih tajam adalah melakukan pemetaan pada unit yang lebih kecil (kecamatan/kelurahan) apabila data tersedia, agar pola klaster lebih terlihat.

4.4 Analisis Temporal

Data deret waktu disusun berdasarkan total kasus bulanan seluruh provinsi.

Gambar 4. Tren Bulanan Kasus Hepatitis A (Total Provinsi DKI Jakarta), 2017-2025
Gambar 4. Tren Bulanan Kasus Hepatitis A (Total Provinsi DKI Jakarta), 2017-2025

Ringkasan deret waktu menunjukkan nilai minimum 2 dan maksimum 142 kasus per bulan. Nilai maksimum bulanan 142 mengindikasikan episode lonjakan yang sangat kuat. Hepatitis A memiliki masa inkubasi relatif panjang (minggu), sehingga puncak kasus bulanan sering mencerminkan paparan yang terjadi beberapa minggu sebelumnya. Ini penting untuk investigasi: puncak tajam biasanya lebih konsisten dengan paparan bersama (misalnya kontaminasi makanan/air) dibanding peningkatan perlahan. Mean yang lebih besar dari median (23,02 vs 15,5) menunjukkan adanya bulan-bulan ekstrem yang “mengangkat” rata-rata, tipikal pada penyakit yang dapat mengalami klaster/KLB.

Uji stasioneritas (ADF p-value 0,1377 dan KPSS p-value 0,01512) menunjukkan Ketidakstasioneran yang mengindikasikan adanya perubahan level/tren sepanjang waktu. Dalam epidemiologi, perubahan tersebut dapat mencerminkan dinamika nyata kejadian penyakit (misalnya KLB, perubahan perilaku higienis, intervensi) maupun perubahan sistem pelaporan. Karena itu, pemodelan ARIMA yang dapat mengakomodasi differencing menjadi relevan untuk menangani pola non-stasioner..

4.5 Evaluasi Forecast

Akurasi model dievaluasi menggunakan data uji (test set) 12 bulan terakhir.

Tabel 4.5. Akurasi Forecast pada Test 12 Bulan Terakhir
Metode RMSE MAE MAPE (%)
Holt 8,530 6,739 64,610
ARIMA 9,128 7,692 61,518
ETS 10,847 9,416 64,612

Holt terbaik pada RMSE/MAE menunjukkan bahwa untuk periode terbaru, pola data lebih cocok ditangkap oleh komponen level-tren sederhana. Ini sering terjadi ketika dinamika terbaru relatif stabil dan tidak menunjukkan musiman kuat. MAPE tinggi (≈61–65%) perlu dibaca hati-hati dalam konteks epidemiologi: ketika kasus bulanan kecil, kesalahan absolut kecil sekalipun dapat menjadi persentase besar. Oleh karena itu, RMSE/MAE sering lebih informatif untuk perencanaan operasional (misalnya estimasi beban layanan), sedangkan MAPE lebih sensitif untuk periode rendah kasus. Dalam aplikasi kesehatan masyarakat, tujuan forecasting bukan hanya “angka tepat”, tetapi juga sinyal kenaikan risiko. Model yang stabil dan responsif terhadap perubahan level sering lebih bermanfaat untuk deteksi dini dibanding mengejar akurasi persentase pada bulan-bulan rendah.

Gambar 5. Perbandingan Forecast TEST 12 Bulan: Holt vs ETS vs ARIMA
Gambar 5. Perbandingan Forecast TEST 12 Bulan: Holt vs ETS vs ARIMA

Perbedaan kurva prediksi menunjukkan perbedaan asumsi model terhadap level/tren dan volatilitas. Model dengan interval lebih lebar merefleksikan ketidakpastian yang lebih tinggi, ini penting untuk komunikasi risiko (bukan sekadar angka titik).

4.6 Forecast 12 Bulan ke Depan

Forecast menggunakan seluruh data untuk memproyeksikan 12 bulan berikutnya (Januari – Desember 2026).

Gambar 6. Forecast 12 Bulan ke Depan (Seluruh Data): ETS + Holt + ARIMA
Gambar 6. Forecast 12 Bulan ke Depan (Seluruh Data): ETS + Holt + ARIMA

Proyeksi mean pada 2026 cenderung berada pada kisaran sekitar 6–25 kasus/bulan tergantung metode dan bulan. Ini mengindikasikan potensi beban kasus tetap ada, meskipun tidak diprediksi setinggi periode puncak historis. Interval prediksi yang lebar (bahkan negatif pada batas bawah) menunjukkan ketidakpastian tinggi akibat volatilitas historis dan adanya lonjakan besar di masa lalu. Dalam epidemiologi, ini menegaskan bahwa sistem harus siap menghadapi skenario lebih buruk (upper bound), terutama karena Hepatitis A dapat melonjak bila ada kejadian kontaminasi/klaster. Secara praktis, nilai batas bawah negatif tidak bermakna secara biologis, untuk interpretasi program dapat dipotong menjadi 0 kasus.

4.6.1 Tabel Forecast 12 Bulan ke Depan

Berikut adalah tabel lengkap hasil peramalan (forecast) yang mencakup nilai rata-rata (mean) serta interval kepercayaan 80% dan 95% untuk ketiga metode (Holt, ETS, dan ARIMA) selama periode Januari hingga Desember 2026:

Tabel 4.6. Forecast Kasus Hepatitis A 12 Bulan ke Depan (2026)
Tanggal Metode Forecast Lo80 Hi80 Lo95 Hi95
2026-01-01 Holt 9,21 -10,65 29,06 -21,16 39,57
2026-02-01 Holt 8,93 -15,59 33,46 -28,57 46,44
2026-03-01 Holt 8,66 -19,78 37,09 -34,83 52,15
2026-04-01 Holt 8,38 -23,49 40,26 -40,36 57,13
2026-05-01 Holt 8,11 -26,86 43,08 -45,38 61,60
2026-06-01 Holt 7,83 -29,99 45,66 -50,01 65,68
2026-07-01 Holt 7,56 -32,91 48,03 -54,34 69,46
2026-08-01 Holt 7,29 -35,67 50,24 -58,41 72,98
2026-09-01 Holt 7,01 -38,30 52,32 -62,28 76,31
2026-10-01 Holt 6,74 -40,81 54,28 -65,98 79,45
2026-11-01 Holt 6,46 -43,22 56,14 -69,52 82,44
2026-12-01 Holt 6,19 -45,54 57,92 -72,93 85,30
2026-01-01 ETS 12,18 3,81 20,55 -0,62 24,97
2026-02-01 ETS 12,11 2,86 21,36 -2,04 26,26
2026-03-01 ETS 14,32 2,33 26,31 -4,01 32,66
2026-04-01 ETS 10,13 0,93 19,32 -3,93 24,19
2026-05-01 ETS 11,89 0,28 23,50 -5,87 29,65
2026-06-01 ETS 11,34 -0,51 23,19 -6,78 29,46
2026-07-01 ETS 9,35 -1,04 19,74 -6,54 25,24
2026-08-01 ETS 9,43 -1,68 20,54 -7,56 26,42
2026-09-01 ETS 11,17 -2,72 25,07 -10,08 32,42
2026-10-01 ETS 13,32 -4,12 30,77 -13,36 40,00
2026-11-01 ETS 25,14 -9,43 59,71 -27,73 78,01
2026-12-01 ETS 14,50 -6,40 35,41 -17,47 46,48
2026-01-01 ARIMA 5,27 -13,16 23,69 -22,92 33,45
2026-02-01 ARIMA 8,23 -14,84 31,30 -27,06 43,51
2026-03-01 ARIMA 15,27 -10,12 40,66 -23,56 54,11
2026-04-01 ARIMA 15,72 -10,97 42,41 -25,10 56,54
2026-05-01 ARIMA 14,28 -13,20 41,75 -27,74 56,29
2026-06-01 ARIMA 12,11 -15,87 40,08 -30,68 54,89
2026-07-01 ARIMA 17,15 -11,16 45,47 -26,15 60,46
2026-08-01 ARIMA 20,40 -8,16 48,96 -23,28 64,08
2026-09-01 ARIMA 18,55 -10,19 47,30 -25,41 62,52
2026-10-01 ARIMA 16,18 -12,71 45,08 -28,01 60,37
2026-11-01 ARIMA 15,84 -13,18 44,86 -28,55 60,22
2026-12-01 ARIMA 11,68 -17,44 40,81 -32,86 56,23

Forecast di atas memberikan skenario baseline (nilai mean), sedangkan interval memberikan gambaran mengenai skenario ketidakpastian atau risiko peningkatan kasus di masa depan[cite: 408]. [cite_start]Dalam konteks manajemen program kesehatan, nilai upper bound (Hi80 dan Hi95) menjadi sangat relevan untuk keperluan kesiapsiagaan, seperti perencanaan sumber daya surveilans, persiapan investigasi klaster, dan intensifikasi edukasi higiene.

Bulan-bulan dengan nilai upper bound yang lebih tinggi, misalnya pada metode ETS untuk bulan November 2026, dapat ditafsirkan sebagai periode dengan tingkat ketidakpastian terbesar menurut model. [cite_start]Meskipun angka tersebut bukan merupakan kepastian kejadian, informasi ini sebaiknya dijadikan dasar penetapan “bulan kewaspadaan” dalam sistem peringatan dini (early warning system) penyakit Hepatitis A di Provinsi DKI Jakarta.

4.7 Sintesis Temuan Utama dan Implikasi Kebijakan
  1. Lonjakan risiko kuat pada 2019 menunjukkan adanya periode dengan potensi KLB/klaster; ini menegaskan pentingnya surveilans yang mampu mendeteksi kenaikan tajam dan cepat melakukan investigasi sumber paparan.
  2. Peta IR 2025 memperlihatkan kantong risiko tertinggi di Jakarta Barat dan Jakarta Utara, sehingga intervensi pencegahan (higiene pangan, sanitasi, edukasi cuci tangan, tracing klaster) paling rasional diprioritaskan pada wilayah ber-IR tinggi.
  3. Tidak signifikannya Moran’s I menunjukkan pola risiko tidak mengelompok kuat pada level kab/kota; secara epidemiologis hal ini mengarah bahwa kejadian bisa dipengaruhi paparan spesifik/klaster mikro yang lebih kecil dari unit kab/kota, sehingga analisis lebih rinci per kecamatan/kelurahan akan lebih informatif bila data tersedia.
  4. Metode Holt paling baik (RMSE terendah) pada test 12 bulan, namun MAPE tinggi menandakan ketidakpastian prediksi relatif besar pada periode kasus rendah. Dalam praktik surveilans, model peramalan sebaiknya dipakai sebagai alat kewaspadaan (mendeteksi deviasi dari baseline) bukan sebagai “angka pasti”.


BAB V
PENUTUP
5.1 Kesimpulan
  1. Dinamika kasus tahunan (2017-2025) menunjukkan fluktuasi yang kuat.
    Total kasus provinsi (hasil agregasi kab/kota) mencapai puncak pada tahun 2019 sebesar 685 kasus dengan IR 6,49 per 100.000 penduduk, kemudian menurun tajam dan mencapai nilai terendah pada tahun 2021 sebesar 74 kasus dengan IR 0,70 per 100.000 penduduk. Pada tahun 2025, jumlah kasus meningkat kembali menjadi 220 kasus dengan IR 2,06 per 100.000 penduduk.
  1. Distribusi spasial risiko (IR) pada tahun 2025 tidak merata antar wilayah kabupaten/kota.
    Wilayah dengan IR tertinggi pada tahun 2025 adalah Jakarta Barat (IR 4,74), diikuti Jakarta Utara (IR 2,64). Wilayah dengan IR terendah adalah Kepulauan Seribu (IR 0) karena tidak terdapat kasus terlapor pada tahun tersebut. Temuan ini menunjukkan adanya prioritas wilayah yang perlu diperhatikan dalam perencanaan intervensi dan penguatan pencegahan.
  1. Tidak terdapat bukti autokorelasi spasial positif yang signifikan pada IR tahun 2025.
    Uji Moran’s I menghasilkan p-value 0,1961, sehingga secara statistik tidak cukup bukti untuk menyatakan bahwa wilayah dengan IR tinggi cenderung berdekatan dan membentuk pengelompokan (cluster) spasial. Dengan demikian, pola risiko tahun 2025 lebih tepat dipahami sebagai perbedaan antarwilayah tanpa pola pengelompokan spasial yang kuat pada tingkat kab/kota.
  1. Deret waktu bulanan total provinsi menunjukkan adanya lonjakan ekstrem dan ketidakstasioneran.
    Nilai maksimum bulanan mencapai 142 kasus, sedangkan minimum 2 kasus, dengan median 15,5 dan rata-rata 23,02 kasus per bulan. Uji ADF (p-value 0,1377) dan KPSS (p-value 0,01512) mengindikasikan deret waktu cenderung tidak stasioner, sehingga pendekatan ARIMA yang mengakomodasi differencing menjadi relevan.
  1. Peramalan 12 bulan terakhir (uji) menunjukkan metode Holt memiliki performa terbaik berdasarkan RMSE.
    Pada evaluasi test 12 bulan terakhir, metode Holt menghasilkan RMSE 8,53 dan MAE 6,74, lebih baik dibanding ETS (RMSE 10,85; MAE 9,42) dan ARIMA (RMSE 9,13; MAE 7,69). Nilai MAPE seluruh metode relatif tinggi (>60%), yang mengindikasikan adanya tantangan peramalan pada data dengan fluktuasi tajam dan periode kasus rendah.
  1. Peramalan 12 bulan ke depan (2026) menunjukkan proyeksi kasus bulanan pada level rendah–menengah dengan ketidakpastian cukup besar.
    Hasil forecast menunjukkan nilai prediksi bulanan umumnya berada pada kisaran belasan kasus, namun interval prediksi (80% dan 95%) cukup lebar pada beberapa bulan, menandakan adanya ketidakpastian yang tinggi akibat dinamika historis dan kemungkinan perubahan pola kasus.
5.2 Saran
5.2.1 Saran untuk Program Kesehatan Masyarakat dan Surveilans
  1. Prioritaskan penguatan pencegahan di wilayah berisiko tinggi.
    Mengacu pada hasil IR tahun 2025, wilayah seperti Jakarta Barat dan Jakarta Utara perlu menjadi fokus peningkatan edukasi higiene, pengawasan sanitasi, keamanan pangan, serta investigasi dini jika terjadi peningkatan kasus.
  1. Perkuat surveilans berbasis waktu (temporal) untuk deteksi lonjakan lebih dini.
    Lonjakan besar di sekitar 2019 menunjukkan pentingnya sistem peringatan dini (early warning) berbasis tren bulanan, sehingga potensi klaster dapat direspons lebih cepat melalui investigasi epidemiologi dan intervensi lapangan.
  1. Tingkatkan kualitas data dan integrasi sumber data.
    Perbedaan pola serta ketidakpastian peramalan dapat ditekan jika tersedia data pendukung yang lebih rinci, misalnya data kasus menurut kelompok umur, sumber paparan (makanan/air), lokasi klaster, serta data lingkungan (sanitasi/air bersih). Integrasi data lintas sumber akan memperkuat interpretasi hasil.
5.2.2 Saran untuk Pengembangan Metode dan Penelitian Selanjutnya
  1. Tambahkan variabel penjelas untuk meningkatkan akurasi model peramalan.
    Model univariat (Holt/ETS/ARIMA) hanya memanfaatkan sejarah kasus. Penelitian selanjutnya dapat mempertimbangkan model dengan kovariat (misalnya ARIMAX, dynamic regression, atau state space model), menggunakan indikator lingkungan dan sosial sebagai prediktor.
  1. Uji pendekatan yang lebih robust terhadap lonjakan ekstrem (outlier) dan perubahan struktur.
    Mengingat terdapat puncak ekstrem (max 142 bulanan), penelitian lanjutan dapat menambahkan prosedur outlier detection, intervention analysis, atau pemodelan yang mengakomodasi structural break agar interval prediksi tidak terlalu lebar dan evaluasi MAPE membaik.
  1. Pertimbangkan analisis spasial yang lebih kaya jika unit wilayah diperluas atau data lebih granular tersedia.
    Pada level kab/kota (6 wilayah), uji spasial seperti Moran’s I memiliki keterbatasan kekuatan statistik. Jika data tersedia pada level kecamatan/kelurahan atau menggunakan data kasus berbasis koordinat, maka analisis hotspot (LISA), pemodelan spasial (CAR/SAR), atau spatio-temporal modeling dapat memberikan insight yang lebih tajam.
5.3 Ucapan Terima Kasih

Penulis mengucapkan terima kasih yang sebesar-besarnya kepada I Gede Nyoman Mindra Jaya, Ph.D. selaku dosen pengampu mata kuliah Epidemiologi, atas bimbingan, arahan, serta penjelasan konseptual yang diberikan selama proses perkuliahan. Arahan tersebut sangat membantu penulis dalam memahami konsep dasar hingga penerapan epidemiologi, khususnya dalam analisis ukuran epidemiologis, interpretasi hasil, serta penyusunan laporan penelitian ini. Penulis juga menyampaikan terima kasih kepada Badan Pusat Statistik (BPS) Provinsi DKI Jakarta atas ketersediaan data kependudukan, serta kepada Dinas Kesehatan Provinsi DKI Jakarta melalui Sistem Surveilans Penyakit, yang telah menyediakan data kasus Hepatitis A sehingga penelitian ini dapat dilaksanakan secara komprehensif dan berbasis data resmi.

Selain itu, penulis mengakui peran teknologi kecerdasan buatan (Artificial Intelligence/AI) sebagai alat bantu dalam proses penyusunan penelitian ini. AI digunakan secara terbatas dan bertanggung jawab untuk:
1. Membantu penulisan serta penyusunan kode Python dalam analisis ukuran epidemiologis dan peramalan kasus Hepatitis A.
2. Membantu penyusunan kode R dalam pengembangan dashboard R Shiny epidemiologis interaktif untuk visualisasi data Hepatitis A Provinsi DKI Jakarta.
3. Membantu pencarian serta penyesuaian referensi ilmiah yang relevan dengan topik epidemiologi dan peramalan penyakit menular.

Penggunaan AI dalam penelitian ini bersifat sebagai alat bantu pendukung, sedangkan seluruh proses analisis, interpretasi hasil, pengambilan keputusan metodologis, dan penulisan akhir laporan tetap berada di bawah tanggung jawab penuh penulis.

Penulis juga mengucapkan terima kasih kepada rekan-rekan mahasiswa yang telah memberikan dukungan, diskusi, dan masukan selama proses pengerjaan tugas ini. Akhir kata, penulis menyadari bahwa laporan ini masih memiliki keterbatasan, sehingga kritik dan saran yang membangun sangat diharapkan sebagai bahan perbaikan di masa mendatang.



DAFTAR PUSTAKA

[1] World Health Organization. (2017).Hepatitis A. Geneva: WHO Press.
[2] Centers for Disease Control and Prevention. (2022). Hepatitis A Epidemiology and Prevention of Vaccine-Preventable Diseases. Atlanta: CDC.
[3] Gordis, L. (2014). Epidemiology (5th ed.). Philadelphia: Elsevier Saunders.
[4] Friis, R. H., & Sellers, T. A. (2021). Epidemiology for Public Health Practice (6th ed.). Burlington: Jones & Bartlett Learning.
[5] Rothman, K. J., Greenland, S., & Lash, T. L. (2008). Modern Epidemiology (3rd ed.). Philadelphia: Lippincott Williams & Wilkins.
[6] Bonita, R., Beaglehole, R., & Kjellström, T. (2006). Basic Epidemiology (2nd ed.). Geneva: World Health Organization.
[7] Lawson, A. B. (2018). Bayesian Disease Mapping: Hierarchical Modeling in Spatial Epidemiology (2nd ed.). Boca Raton: CRC Press.
[8] Brookmeyer, R., & Stroup. D. F. (2004). Monitoring the Health of Populations: Statistical Principles and Methods for Public Health Surveillance. New York: Oxford University Press.
[9] Hyndman, R. J., & Athanasopoulos, G. (2021). Forecasting: Principles and Practice (3rd ed.). Melbourne: OTexts.
[10] Holt, C. C. (1957). Forecasting seasonals and trends by exponentially weighted moving averages. Office of Naval Research Memorandum, 52, 1-10.
[11] Gardner, E. S. (2006). Exponential smoothing: The state of the art Part II. International Journal of Forecasting, 22(4), 637-666.
[12] Taylor, J. W. (2010). Exponential smoothing with a damped multiplicative trend. International Journal of Forecasting, 26(2), 281-293.
[13] Cowpertwait, P. S. P., & Metcalfe, A. V. (2009). Introductory Time Series with R. New York: Springer.
[14] Chatfield, C. (2004). The Analysis of Time Series: An Introduction (6th ed.). Boca Raton: Chapman & Hall/CRC.
[15] Box, G. E. P., Jenkins, G. M., Reinsel, G. C., & Ljung, G. M. (2015). Time Series Analysis: Forecasting and Control (5th ed.). Hoboken: John Wiley & Sons.
[16] Helfenstein, U. (1996). Box-Jenkins modelling of some viral infectious diseases. Statistics in Medicine, 15(23), 2563-2576.
[17] Cowling, B. J., Lau, E. H. Y., Lam, C. L. K., Cheng, C. K. Y., Kovar, J., Chan, K. H., & Peiris, J. S. M. (2008). Effects of school closures, 2008 winter influenza season, Hong Kong. Emerging Infectious Diseases, 14(10), 1660-1662.
[18] Earnest, A., Chen, M. I. C., Ng, D., & Leo, Y. S. (2005). Using autoregressive integrated moving average (ARIMA) models to predict and monitor the number of beds occupied during a SARS outbreak in a tertiary hospital in Singapore. BMC Health Services Research, 5(1), 36.





LAMPIRAN
  1. Dashboard (RShiny)
Dashboard interaktif untuk memvisualisasikan data dan peta secara real-time dapat diakses melalui: https://muhammad-al-fauzan.shinyapps.io/PemetaanForecastingKasusHepatitisADKIJakarta/
  1. Video Pemaparan Projek
Video presentasi dapat dilihat pada tautan berikut: https://youtu.be/fpEswfi4zzw?si=f3i7HVS_WZewnY2O
  1. Source Code untuk Analisis (R)
Berikut adalah kode R yang digunakan untuk melakukan analisis statistik, perhitungan IR, dan pemodelan peramalan:
# ANALISIS HEPATITIS A DKI JAKARTA
rm(list = ls()); gc()

# PACKAGES
pkgs <- c(
  "dplyr","tidyr","readr","stringr","lubridate",
  "ggplot2","scales","sf","forecast","tseries","spdep"
)
to_install <- pkgs[!pkgs %in% installed.packages()[,"Package"]]
if (length(to_install) > 0) install.packages(to_install, dependencies = TRUE)
invisible(lapply(pkgs, library, character.only = TRUE))

options(scipen = 999)
theme_set(theme_minimal(base_size = 13))

# 1) HELPER FUNCTIONS
num_clean <- function(x) {
  x <- as.character(x)
  x <- stringr::str_replace_all(x, "\\.", "")
  x <- stringr::str_replace_all(x, ",", ".")
  suppressWarnings(as.numeric(x))
}

std_wilayah <- function(x) {
  x <- stringr::str_squish(as.character(x))
  x <- stringr::str_replace_all(x, regex("^Kota\\s+", ignore_case = TRUE), "")
  x <- stringr::str_replace_all(x, "Kab\\.?\\s*Kep\\.?\\s*Seribu", "Kepulauan Seribu")
  x <- stringr::str_replace_all(x, "Kabupaten\\s*Kepulauan\\s*Seribu", "Kepulauan Seribu")
  x <- stringr::str_replace_all(x, "Kep\\.?\\s*Seribu", "Kepulauan Seribu")
  x
}

get_years <- function(x) {
  x <- as.character(x)
  y <- stringr::str_extract(x, "\\b(19\\d{2}|20\\d{2})\\b")
  as.integer(y)
}

rmse <- function(actual, pred) sqrt(mean((actual - pred)^2, na.rm = TRUE))
mae  <- function(actual, pred) mean(abs(actual - pred), na.rm = TRUE)
mape_safe <- function(actual, pred) {
  actual <- as.numeric(actual); pred <- as.numeric(pred)
  idx <- which(actual != 0 & !is.na(actual) & !is.na(pred))
  if (length(idx) == 0) return(NA_real_)
  mean(abs((actual[idx] - pred[idx]) / actual[idx])) * 100
}

is_jateng_row <- function(wilayah) {
  stringr::str_detect(stringr::str_to_upper(wilayah), "JAWA\\s*TENGAH")
}

is_provinsi_row <- function(wilayah) {
  stringr::str_detect(stringr::str_to_upper(wilayah), "PROVINSI")
}

guess_name_col <- function(sfobj) {
  cand <- c("nama","NAMOBJ","WADMKK","NAME_2","NAME","KABKOT","KAB_KOTA","KABKOTA","Wilayah")
  hit <- cand[cand %in% names(sfobj)]
  if (length(hit) > 0) return(hit[1])
  chr_cols <- names(sfobj)[sapply(sfobj, is.character)]
  if (length(chr_cols) == 0) stop("Tidak menemukan kolom nama wilayah (kolom karakter) di GeoJSON.")
  chr_cols[1]
}

# 2) LOAD DATA FUNCTIONS
load_crosssection_csv <- function(path) {
  raw <- suppressMessages(readr::read_csv(path, show_col_types = FALSE, col_names = TRUE))
  
  header_row <- raw[1, ]
  dat <- raw[-1, ] %>% as.data.frame()
  
  idx_pop_marker <- which(names(raw) == "Data Jumlah Penduduk")
  if (length(idx_pop_marker) == 0) stop("Kolom marker 'Data Jumlah Penduduk' tidak ditemukan pada crosssection CSV.")
  
  case_cols <- 2:(idx_pop_marker - 1)
  pop_cols  <- (idx_pop_marker):(ncol(raw))
  
  case_years <- get_years(unlist(header_row[case_cols]))
  if (all(is.na(case_years))) case_years <- get_years(names(raw)[case_cols])
  
  pop_years <- get_years(unlist(header_row[pop_cols]))
  if (all(is.na(pop_years))) pop_years <- get_years(names(raw)[pop_cols])
  
  if (all(is.na(case_years))) case_years <- 2017:2025
  if (all(is.na(pop_years)))  pop_years  <- 2017:2025
  
  case_years <- case_years[!is.na(case_years)]
  pop_years  <- pop_years[!is.na(pop_years)]
  
  cases_wide <- dat %>%
    dplyr::select(Wilayah, dplyr::all_of(names(raw)[case_cols])) %>%
    dplyr::mutate(Wilayah = std_wilayah(Wilayah)) %>%
    dplyr::rename_with(~ paste0("Case_", case_years[seq_along(.x)]), .cols = -Wilayah) %>%
    dplyr::mutate(dplyr::across(dplyr::starts_with("Case_"), ~ num_clean(.x)))
  
  pop_wide <- dat %>%
    dplyr::select(Wilayah, dplyr::all_of(names(raw)[pop_cols])) %>%
    dplyr::mutate(Wilayah = std_wilayah(Wilayah))
  
  pop_names <- paste0("Pop_", pop_years[seq_len(ncol(pop_wide) - 1)])
  names(pop_wide) <- c("Wilayah", pop_names)
  pop_wide <- pop_wide %>% dplyr::mutate(dplyr::across(dplyr::starts_with("Pop_"), ~ num_clean(.x)))
  
  cs <- dplyr::left_join(cases_wide, pop_wide, by = "Wilayah") %>%
    tidyr::pivot_longer(cols = -Wilayah, names_to = "key", values_to = "value") %>%
    tidyr::separate(key, into = c("type","year"), sep = "_", convert = TRUE) %>%
    tidyr::pivot_wider(names_from = type, values_from = value) %>%
    dplyr::rename(Cases = Case, Population = Pop) %>%
    dplyr::mutate(
      year = as.integer(year),
      Wilayah = std_wilayah(Wilayah),
      IR_100k = dplyr::if_else(Population > 0, (Cases/Population) * 100000, NA_real_)
    )
  
  cs
}

load_timeseries_csv <- function(path) {
  raw <- suppressMessages(readr::read_csv(path, show_col_types = FALSE))
  
  # posisi kolom Total 
  total_idx <- which(toupper(names(raw)) == "TOTAL")
  if (length(total_idx) == 0) total_idx <- NA_integer_
  
  new_names <- names(raw)
  
  # Rename kolom wilayah berdasarkan baris pertama (header wilayah)
  # - kolom 2..(total_idx-1) saja yang di-rename
  # - rename hanya jika value baris pertama tidak NA/kosong
  start_j <- 2
  end_j <- if (!is.na(total_idx)) (total_idx - 1) else ncol(raw)
  
  for (j in start_j:end_j) {
    cand <- raw[[j]][1]
    cand <- as.character(cand)
    cand <- stringr::str_squish(cand)
    
    if (!is.na(cand) && nzchar(cand)) {
      new_names[j] <- cand
    }
  }
  
  # Pastikan tidak ada nama kolom NA / kosong
  bad <- is.na(new_names) | new_names == ""
  if (any(bad)) {
    new_names[bad] <- paste0("V", which(bad))
  }
  
  # Pastikan unik (hindari duplikat)
  new_names <- make.unique(new_names)
  
  names(raw) <- new_names
  
  # Buang baris pertama (baris header wilayah)
  ts <- raw[-1, ]
  
  # Parse tanggal aman 
  ts <- ts %>%
    dplyr::mutate(
      Tanggal = as.character(Tanggal),
      Tanggal = suppressWarnings(lubridate::dmy(Tanggal)),
      Tanggal = dplyr::if_else(is.na(Tanggal), suppressWarnings(lubridate::ymd(as.character(raw[-1, "Tanggal"]))), Tanggal),
      Tanggal = dplyr::if_else(is.na(Tanggal), suppressWarnings(lubridate::mdy(as.character(raw[-1, "Tanggal"]))), Tanggal)
    )
  
  # Konversi numerik untuk semua kolom selain Tanggal
  ts <- ts %>%
    dplyr::mutate(dplyr::across(-Tanggal, ~ num_clean(.x)))
  
  # Kalau Total belum ada, buat Total dari penjumlahan semua wilayah
  if (!("Total" %in% names(ts))) {
    ts <- ts %>% dplyr::mutate(Total = rowSums(dplyr::across(-Tanggal), na.rm = TRUE))
  }
  
  # Bentuk long untuk analisis kab/kota
  ts_long <- ts %>%
    tidyr::pivot_longer(cols = -c(Tanggal, Total), names_to = "Wilayah", values_to = "Cases") %>%
    dplyr::mutate(
      Wilayah = std_wilayah(Wilayah),
      year = lubridate::year(Tanggal)
    )
  
  list(ts_wide = ts, ts_long = ts_long)
}


# 3) PATH FILE 
BASE_PATH <- "C:/Users/ajayg/Documents/data"

CS_PATH  <- file.path(BASE_PATH, "DATA UAS EPIDEM - DATA Crossection.csv")
TS_PATH  <- file.path(BASE_PATH, "DATA UAS EPIDEM - Data Time Series.csv")
GEO_PATH <- file.path(BASE_PATH, "id-jk.min.geojson")

CS_PATH  <- normalizePath(CS_PATH,  winslash = "/", mustWork = FALSE)
TS_PATH  <- normalizePath(TS_PATH,  winslash = "/", mustWork = FALSE)
GEO_PATH <- normalizePath(GEO_PATH, winslash = "/", mustWork = FALSE)

stopifnot(file.exists(CS_PATH))
stopifnot(file.exists(TS_PATH))
stopifnot(file.exists(GEO_PATH))

cs_raw <- load_crosssection_csv(CS_PATH)

ts_obj  <- load_timeseries_csv(TS_PATH)
ts_wide <- ts_obj$ts_wide
ts_long <- ts_obj$ts_long

# 4) CLEAN khusus: buang baris "PROVINSI JAWA TENGAH"
cs_raw  <- cs_raw  %>% dplyr::filter(!is_jateng_row(Wilayah))
ts_long <- ts_long %>% dplyr::filter(!is_jateng_row(Wilayah))

# Split level
cs_prov <- cs_raw %>% dplyr::filter(is_provinsi_row(Wilayah))
cs_kab  <- cs_raw %>% dplyr::filter(!is_provinsi_row(Wilayah))

# (1) DATA PENELITIAN: tampilkan 6 atas & 6 bawah
cat("\n==================== (1) DATA PENELITIAN (CROSS-SECTION) ====================\n")

cs_view <- cs_raw %>%
  dplyr::arrange(year, Wilayah) %>%
  dplyr::select(Wilayah, year, Cases, Population, IR_100k)

cat("\n--- 6 Baris Teratas (head) ---\n")
print(utils::head(cs_view, 6))

cat("\n--- 6 Baris Terbawah (tail) ---\n")
print(utils::tail(cs_view, 6))

cat("\n==================== DATA TIME SERIES (BULANAN) ====================\n")
ts_view <- ts_long %>%
  dplyr::arrange(Tanggal, Wilayah) %>%
  dplyr::select(Tanggal, Wilayah, Cases)

cat("\n--- 6 Baris Teratas (head) ---\n")
print(utils::head(ts_view, 6))

cat("\n--- 6 Baris Terbawah (tail) ---\n")
print(utils::tail(ts_view, 6))

# (2) STATISTIK DESKRIPTIF: max, min, mean, median, range, dan IR
cat("\n==================== (2) STATISTIK DESKRIPTIF ====================\n")

# Provinsi per tahun (agregasi dari kab/kota supaya konsisten)
prov_year <- cs_kab %>%
  dplyr::group_by(year) %>%
  dplyr::summarise(
    Cases = sum(Cases, na.rm = TRUE),
    Population = sum(Population, na.rm = TRUE),
    IR_100k = dplyr::if_else(Population > 0, (Cases/Population) * 100000, NA_real_),
    .groups = "drop"
  )

desc_prov <- prov_year %>%
  dplyr::summarise(
    min_cases = min(Cases, na.rm = TRUE),
    max_cases = max(Cases, na.rm = TRUE),
    mean_cases = mean(Cases, na.rm = TRUE),
    median_cases = median(Cases, na.rm = TRUE),
    range_cases = max_cases - min_cases,
    min_ir = min(IR_100k, na.rm = TRUE),
    max_ir = max(IR_100k, na.rm = TRUE),
    mean_ir = mean(IR_100k, na.rm = TRUE),
    median_ir = median(IR_100k, na.rm = TRUE),
    range_ir = max_ir - min_ir
  )

cat("\n--- Ringkasan Provinsi (agregasi kab/kota) per tahun ---\n")
print(prov_year)

cat("\n--- Statistik deskriptif (Provinsi) ---\n")
print(desc_prov)

# Kab/kota akumulasi seluruh tahun
kab_all <- cs_kab %>%
  dplyr::group_by(Wilayah) %>%
  dplyr::summarise(
    Cases = sum(Cases, na.rm = TRUE),
    Population = sum(Population, na.rm = TRUE),
    IR_100k = dplyr::if_else(Population > 0, (Cases/Population) * 100000, NA_real_),
    .groups = "drop"
  )

desc_kab <- kab_all %>%
  dplyr::summarise(
    min_cases = min(Cases, na.rm = TRUE),
    max_cases = max(Cases, na.rm = TRUE),
    mean_cases = mean(Cases, na.rm = TRUE),
    median_cases = median(Cases, na.rm = TRUE),
    range_cases = max_cases - min_cases,
    min_ir = min(IR_100k, na.rm = TRUE),
    max_ir = max(IR_100k, na.rm = TRUE),
    mean_ir = mean(IR_100k, na.rm = TRUE),
    median_ir = median(IR_100k, na.rm = TRUE),
    range_ir = max_ir - min_ir
  )

cat("\n--- Statistik deskriptif (Kab/Kota, akumulasi seluruh tahun) ---\n")
print(desc_kab)

# (3) ANALISIS SPASIAL
cat("\n==================== (3) ANALISIS SPASIAL (DKI GEOJSON) ====================\n")

g_dki <- sf::st_read(GEO_PATH, quiet = TRUE) %>% sf::st_make_valid()
name_col <- guess_name_col(g_dki)

g_dki <- g_dki %>%
  dplyr::mutate(Wilayah = std_wilayah(.data[[name_col]]))

year_latest <- max(cs_kab$year, na.rm = TRUE)

map_df <- cs_kab %>%
  dplyr::filter(year == year_latest) %>%
  dplyr::select(Wilayah, year, Cases, Population, IR_100k)

g_map <- g_dki %>%
  dplyr::left_join(map_df, by = "Wilayah")

cat(paste0("\nPeta dibuat untuk tahun: ", year_latest, "\n"))
cat("Cek wilayah yang tidak match (jika ada):\n")
print(
  g_map %>%
    dplyr::filter(is.na(Cases)) %>%
    sf::st_drop_geometry() %>%
    dplyr::select(Wilayah)
)

p_map <- ggplot(g_map) +
  geom_sf(aes(fill = IR_100k), linewidth = 0.2) +
  scale_fill_continuous(
    labels = scales::number_format(accuracy = 0.01),
    na.value = "grey90"
  ) +
  labs(
    title = paste0("Peta Incidence Rate (IR) Hepatitis A per 100.000 Penduduk - DKI Jakarta (", year_latest, ")"),
    subtitle = "IR = kasus / penduduk × 100.000",
    fill = "IR/100k"
  )
print(p_map)

# (Opsional) Moran's I global untuk IR (tahun terbaru)
moran_data <- g_map %>%
  dplyr::filter(!is.na(IR_100k)) %>%
  sf::st_transform(4326)

if (nrow(moran_data) >= 3) {
  nb <- spdep::poly2nb(moran_data, queen = TRUE)
  lw <- spdep::nb2listw(nb, style = "W", zero.policy = TRUE)
  moran_res <- spdep::moran.test(moran_data$IR_100k, lw, zero.policy = TRUE)
  
  cat("\n--- Moran's I (Global) untuk IR_100k (tahun terbaru) ---\n")
  print(moran_res)
} else {
  cat("\n(Moran's I dilewati) Data IR valid terlalu sedikit untuk uji autokorelasi spasial.\n")
}

# ranking IR kab/kota tahun terbaru
rank_ir_latest <- cs_kab %>%
  dplyr::filter(year == year_latest) %>%
  dplyr::arrange(dplyr::desc(IR_100k)) %>%
  dplyr::mutate(rank = dplyr::row_number()) %>%
  dplyr::select(rank, Wilayah, year, Cases, Population, IR_100k)

cat("\n--- Ranking IR Kab/Kota (tahun terbaru) ---\n")
print(rank_ir_latest)

# (4) ANALISIS TEMPORAL + FORECASTING
cat("\n==================== (4) ANALISIS TEMPORAL + FORECASTING ====================\n")

# Total provinsi bulanan = sum kab/kota per tanggal
ts_prov <- ts_long %>%
  dplyr::group_by(Tanggal) %>%
  dplyr::summarise(Cases = sum(Cases, na.rm = TRUE), .groups = "drop") %>%
  dplyr::arrange(Tanggal)

start_y <- lubridate::year(min(ts_prov$Tanggal))
start_m <- lubridate::month(min(ts_prov$Tanggal))
y_ts <- ts(ts_prov$Cases, start = c(start_y, start_m), frequency = 12)

cat("\n--- Ringkasan deret waktu (Total Provinsi) ---\n")
print(summary(y_ts))

plot(y_ts, main = "Tren Bulanan Kasus Hepatitis A (Total Provinsi DKI Jakarta)",
     ylab = "Kasus", xlab = "Tahun")

# Uji stasioneritas (opsional, berguna untuk narasi ARIMA)
cat("\n--- Uji Stasioneritas (opsional) ---\n")
cat("ADF Test (tseries::adf.test):\n")
print(tseries::adf.test(y_ts))

cat("\nKPSS Test (tseries::kpss.test):\n")
print(tseries::kpss.test(y_ts))

H_TEST <- 12
H_FUTURE <- 12

n <- length(y_ts)
if (n <= H_TEST) stop("Data time series lebih pendek dari 12 bulan, tidak cukup untuk test horizon.")

train_ts <- window(y_ts, end = time(y_ts)[n - H_TEST])
test_ts  <- window(y_ts, start = time(y_ts)[n - H_TEST + 1])

# Holt
fit_holt <- forecast::holt(train_ts, h = H_TEST)

# ETS
fit_ets <- forecast::ets(train_ts)
fc_ets  <- forecast::forecast(fit_ets, h = H_TEST)

# ARIMA
fit_arima <- forecast::auto.arima(train_ts, seasonal = TRUE, stepwise = TRUE, approximation = FALSE)
fc_arima  <- forecast::forecast(fit_arima, h = H_TEST)

# Evaluasi TEST
actual <- as.numeric(test_ts)
pred_holt  <- as.numeric(fit_holt$mean)
pred_ets   <- as.numeric(fc_ets$mean)
pred_arima <- as.numeric(fc_arima$mean)

metrics <- data.frame(
  Metode = c("Holt","ETS","ARIMA"),
  RMSE = c(rmse(actual, pred_holt), rmse(actual, pred_ets), rmse(actual, pred_arima)),
  MAE  = c(mae(actual, pred_holt),  mae(actual, pred_ets),  mae(actual, pred_arima)),
  MAPE = c(mape_safe(actual, pred_holt), mape_safe(actual, pred_ets), mape_safe(actual, pred_arima))
) %>% dplyr::arrange(RMSE)

cat("\n--- Akurasi TEST (12 bulan terakhir) ---\n")
print(metrics)

# Plot perbandingan forecast TEST
plot(fc_ets, main = "Perbandingan Forecast TEST 12 bulan: Holt vs ETS vs ARIMA",
     ylab = "Kasus", xlab = "Tahun")
lines(fit_holt$mean, lty = 2)
lines(fc_arima$mean, lty = 3)
legend("topright", legend = c("ETS","Holt","ARIMA"), lty = c(1,2,3), bty = "n")

# Forecast 12 bulan ke depan pakai seluruh data
final_holt  <- forecast::holt(y_ts, h = H_FUTURE)
final_ets   <- forecast::forecast(forecast::ets(y_ts), h = H_FUTURE)
final_arima <- forecast::forecast(forecast::auto.arima(y_ts, seasonal = TRUE), h = H_FUTURE)

plot(final_ets, main = "Forecast 12 Bulan ke Depan (Seluruh Data): ETS + Holt + ARIMA",
     ylab = "Kasus", xlab = "Tahun")
lines(final_holt$mean, lty = 2)
lines(final_arima$mean, lty = 3)
legend("topright",
       legend = c("ETS (base + interval)", "Holt (mean)", "ARIMA (mean)"),
       lty = c(1,2,3), bty = "n")

# Tabel forecast 12 bulan ke depan
last_date <- max(ts_prov$Tanggal, na.rm = TRUE)
future_dates <- seq(last_date %m+% months(1), by = "1 month", length.out = H_FUTURE)

pack_fc <- function(fc, metode) {
  data.frame(
    Tanggal = future_dates,
    Metode = metode,
    Forecast = as.numeric(fc$mean),
    Lo80 = if (!is.null(fc$lower)) as.numeric(fc$lower[,1]) else NA_real_,
    Hi80 = if (!is.null(fc$upper)) as.numeric(fc$upper[,1]) else NA_real_,
    Lo95 = if (!is.null(fc$lower)) as.numeric(fc$lower[,2]) else NA_real_,
    Hi95 = if (!is.null(fc$upper)) as.numeric(fc$upper[,2]) else NA_real_
  )
}

fc_future_tbl <- dplyr::bind_rows(
  pack_fc(final_holt, "Holt"),
  pack_fc(final_ets, "ETS"),
  pack_fc(final_arima, "ARIMA")
) %>%
  dplyr::mutate(dplyr::across(c(Forecast,Lo80,Hi80,Lo95,Hi95), ~ round(.x, 2)))

cat("\n--- Tabel Forecast 12 Bulan ke Depan (3 metode) ---\n")
print(fc_future_tbl)

cat("\n=== SELESAI. Output ada di Console + plot muncul di Plot panel. ===\n")
  1. Source Code RShiny (Dashboard)
Berikut adalah kode R yang digunakan untuk membuat visualisasi menarik:

# ============================================================
# DASHBOARD EPIDEMIOLOGI HEPATITIS A DKI JAKARTA 
# Author: Muhammad Al Fauzan
# ============================================================

# ===============================
# 1) LOAD LIBRARIES
# ===============================
library(shiny)
library(shinydashboard)
library(tidyverse)
library(leaflet)
library(plotly)
library(DT)
library(sf)
library(scales)
library(shinycssloaders)
library(shinyWidgets)
library(shinyjs)
library(lubridate)

# Forecast & tests
library(forecast)
library(tseries)

# Spatial
library(spdep)

# ===============================
# 2) PATH (DEPLOY-SAFE)
# ===============================
BASE_PATH <- "data"

CASE_FILE <- "DATA UAS EPIDEM - Data Kasus HAV.csv"
POP_FILE  <- "DATA UAS EPIDEM - Data Jumlah Penduduk.csv"
TS_FILE   <- "DATAA UAS EPIDEM - Data Deret Waktu.csv"
GEO_FILE  <- "id-jk.min.geojson"

CASE_PATH <- normalizePath(file.path(BASE_PATH, CASE_FILE), winslash = "/", mustWork = FALSE)
POP_PATH  <- normalizePath(file.path(BASE_PATH, POP_FILE),  winslash = "/", mustWork = FALSE)
TS_PATH   <- normalizePath(file.path(BASE_PATH, TS_FILE),   winslash = "/", mustWork = FALSE)
GEO_PATH  <- normalizePath(file.path(BASE_PATH, GEO_FILE),  winslash = "/", mustWork = FALSE)

cat("getwd():", getwd(), "\n")
cat("Files in data/:\n"); print(if (dir.exists(BASE_PATH)) list.files(BASE_PATH) else "")
cat("CASE exists:", file.exists(CASE_PATH), "\n")
cat("POP  exists:", file.exists(POP_PATH),  "\n")
cat("TS   exists:", file.exists(TS_PATH),   "\n")
cat("GEO  exists:", file.exists(GEO_PATH),  "\n")

# ===============================
# 3) HELPERS
# ===============================
read_semicolon <- function(path) {
  readr::read_delim(path, delim = ";", show_col_types = FALSE, trim_ws = TRUE)
}

std_wilayah <- function(x) {
  x %>%
    as.character() %>%
    toupper() %>%
    str_replace_all("\\s+", " ") %>%
    str_trim() %>%
    str_replace_all("^KOTA\\s+", "") %>%
    str_replace_all("^KABUPATEN\\s+", "") %>%
    str_replace_all("^KAB\\.?\\s*", "") %>%
    str_replace_all("ADM\\.?\\s*", "") %>%
    str_replace_all("JAKARTA\\s+KEPULAUAN", "KEPULAUAN") %>%
    str_replace_all("KEP\\.\\s*SERIBU", "KEPULAUAN SERIBU") %>%
    str_replace_all("^KOTA\\s+", "") %>%
    str_replace_all("^KAB\\.?\\s*", "")
}

# buang titik ribuan: 1.056.896 -> 1056896, dukung koma desimal
num_clean_id <- function(x) {
  x <- as.character(x)
  x <- str_replace_all(x, "\\s+", "")
  x <- str_replace_all(x, "\\.", "")
  x <- str_replace_all(x, ",", ".")
  suppressWarnings(as.numeric(x))
}

rmse <- function(actual, pred) sqrt(mean((actual - pred)^2, na.rm = TRUE))
mae  <- function(actual, pred) mean(abs(actual - pred), na.rm = TRUE)
mape_safe <- function(actual, pred) {
  denom <- ifelse(actual == 0, NA_real_, actual)
  mean(abs((actual - pred) / denom), na.rm = TRUE) * 100
}

guess_name_col <- function(g) {
  cand <- c("name","NAME","Name","NAMOBJ","WADMKK","KAB_KOTA","KABKOT","KOTA",
            "wilayah","WILAYAH","KABUPATEN","KABUPATEN_KOTA")
  hit <- cand[cand %in% names(g)]
  if (length(hit) > 0) return(hit[1])
  chr_cols <- names(g)[purrr::map_lgl(g, ~ is.character(.x) || is.factor(.x))]
  if (length(chr_cols) == 0) return(names(g)[1])
  chr_cols[1]
}

# ===============================
# 4) LOAD WIDE (2017..2025) -> LONG (SAFE)
# ===============================
load_wide_years <- function(path, value_name) {
  df <- read_semicolon(path)
  
  # rapikan nama kolom + trim
  names(df) <- names(df) %>%
    stringr::str_replace_all("\ufeff", "") %>%
    stringr::str_trim() %>%
    as.character()
  
  if (!("Wilayah" %in% names(df))) {
    stop("File ", basename(path), " harus punya kolom 'Wilayah'. Kolom terbaca: ",
         paste(names(df), collapse = ", "))
  }
  
  year_cols <- names(df)[stringr::str_detect(names(df), "^\\s*20[0-9]{2}\\s*$")]
  if (length(year_cols) == 0) {
    stop("Tidak menemukan kolom tahun (2017..2025) pada: ", basename(path),
         " | Kolom terbaca: ", paste(names(df), collapse = ", "))
  }
  
  # PAKSA semua kolom tahun jadi character sebelum pivot (FIX error combine type)
  df <- df %>% mutate(across(all_of(year_cols), ~ as.character(.x)))
  
  df %>%
    select(Wilayah, all_of(year_cols)) %>%
    pivot_longer(cols = all_of(year_cols), names_to = "year", values_to = value_name) %>%
    mutate(
      year = as.integer(str_trim(year)),
      Wilayah = as.character(Wilayah),
      Wilayah_std = std_wilayah(Wilayah)
    )
}

# ===============================
# 5) BUILD CROSS SECTION: Cases + Population + IR/100k
# ===============================
build_crosssection <- function(case_path, pop_path) {
  kasus <- load_wide_years(case_path, "Cases") %>%
    mutate(Cases = num_clean_id(Cases))
  
  pop <- load_wide_years(pop_path, "Population") %>%
    mutate(Population = num_clean_id(Population))
  
  left_join(kasus, pop, by = c("Wilayah", "Wilayah_std", "year")) %>%
    mutate(IR_100k = if_else(Population > 0, (Cases / Population) * 100000, NA_real_))
}

# ===============================
# 6) LOAD TIME SERIES (MONTHLY) FROM TS_FILE
#    Format: Tanggal;Jakarta Pusat;...;Total
# ===============================
load_ts_monthly <- function(ts_path) {
  df <- read_semicolon(ts_path)
  
  # rapikan nama kolom (BOM/spasi sering bikin "Tanggal" jadi aneh)
  names(df) <- names(df) %>%
    stringr::str_replace_all("\ufeff", "") %>%
    stringr::str_trim() %>%
    as.character()
  
  nm <- tolower(names(df))
  col_tgl <- names(df)[which(nm %in% c("tanggal","date","waktu"))][1]
  if (is.na(col_tgl)) col_tgl <- names(df)[which(stringr::str_detect(nm, "tanggal|date"))][1]
  
  if (is.na(col_tgl)) {
    stop("TS tidak punya kolom Tanggal/Date. Kolom terbaca: ", paste(names(df), collapse = ", "))
  }
  
  # simpan raw date ke kolom sementara (biar tidak kehapus saat col_tgl == 'Tanggal')
  df <- df %>%
    mutate(Date_raw = .data[[col_tgl]])
  
  # hapus kolom tanggal lama hanya jika BUKAN Date_raw
  df <- df %>% select(-all_of(col_tgl))
  
  # parse tanggal (format kamu dd/mm/yyyy -> dmy)
  df <- df %>%
    mutate(
      Tanggal = suppressWarnings(lubridate::dmy(Date_raw))
    )
  
  # fallback: coba ymd kalau dmy gagal
  if (all(is.na(df$Tanggal))) {
    df <- df %>%
      mutate(Tanggal = suppressWarnings(lubridate::ymd(Date_raw)))
  }
  
  if (all(is.na(df$Tanggal))) {
    stop("Kolom tanggal ada, tapi format tidak terbaca. Contoh valid: 01/01/2017 atau 2017-01-01")
  }
  
  # drop kolom raw
  df <- df %>% select(-Date_raw)
  
  # rapikan numeric semua kolom selain Tanggal
  value_cols <- setdiff(names(df), "Tanggal")
  df <- df %>% mutate(across(all_of(value_cols), num_clean_id))
  
  # jadikan long untuk database (wilayah = semua kolom kecuali Tanggal & Total)
  value_cols <- setdiff(names(df), "Tanggal")
  wilayah_cols <- setdiff(value_cols, "Total")
  
  ts_long <- df %>%
    pivot_longer(cols = all_of(wilayah_cols),
                 names_to = "Wilayah", values_to = "Cases") %>%
    mutate(Wilayah_std = std_wilayah(Wilayah)) %>%
    arrange(Tanggal, Wilayah)
  
  # total provinsi: pakai Total kalau ada, else sum
  if ("Total" %in% value_cols) {
    ts_total <- df %>%
      transmute(Tanggal, Cases = .data[["Total"]]) %>%
      arrange(Tanggal)
  } else {
    ts_total <- ts_long %>%
      group_by(Tanggal) %>%
      summarise(Cases = sum(Cases, na.rm = TRUE), .groups = "drop") %>%
      arrange(Tanggal)
  }
  
  list(ts_long = ts_long, ts_total = ts_total, ts_wide = df)
}


# ===============================
# 7) FORECAST OBJECTS (Holt/ETS/ARIMA)
# ===============================
compute_forecast_objects <- function(ts_total_df) {
  ts_total_df <- ts_total_df %>% arrange(Tanggal) %>% filter(!is.na(Tanggal))
  
  # cek bulanan: minimal 24 bulan
  validate(need(nrow(ts_total_df) >= 24, "Data TS bulanan terlalu pendek (minimal 24 bulan disarankan)."))
  
  start_y <- year(min(ts_total_df$Tanggal))
  start_m <- month(min(ts_total_df$Tanggal))
  y_ts <- ts(ts_total_df$Cases, start = c(start_y, start_m), frequency = 12)
  
  adf  <- tryCatch(tseries::adf.test(y_ts),  error = function(e) NULL)
  kpss <- tryCatch(tseries::kpss.test(y_ts), error = function(e) NULL)
  
  H_TEST <- 12
  n <- length(y_ts)
  validate(need(n > H_TEST, "Deret waktu < 12 bulan, tidak cukup untuk evaluasi TEST."))
  
  train_ts <- window(y_ts, end = time(y_ts)[n - H_TEST])
  test_ts  <- window(y_ts, start = time(y_ts)[n - H_TEST + 1])
  
  fc_holt  <- forecast::holt(train_ts, h = H_TEST)
  fit_ets  <- forecast::ets(train_ts)
  fc_ets   <- forecast::forecast(fit_ets, h = H_TEST)
  fit_arima <- forecast::auto.arima(train_ts, seasonal = TRUE, stepwise = TRUE, approximation = FALSE)
  fc_arima  <- forecast::forecast(fit_arima, h = H_TEST)
  
  actual <- as.numeric(test_ts)
  
  metrics <- tibble(
    Metode = c("Holt","ETS","ARIMA"),
    RMSE = c(rmse(actual, as.numeric(fc_holt$mean)),
             rmse(actual, as.numeric(fc_ets$mean)),
             rmse(actual, as.numeric(fc_arima$mean))),
    MAE  = c(mae(actual, as.numeric(fc_holt$mean)),
             mae(actual, as.numeric(fc_ets$mean)),
             mae(actual, as.numeric(fc_arima$mean))),
    MAPE = c(mape_safe(actual, as.numeric(fc_holt$mean)),
             mape_safe(actual, as.numeric(fc_ets$mean)),
             mape_safe(actual, as.numeric(fc_arima$mean)))
  ) %>% arrange(RMSE)
  
  # forecast 12 bulan ke depan pakai seluruh data
  H_FUTURE <- 12
  final_holt  <- forecast::holt(y_ts, h = H_FUTURE)
  final_ets   <- forecast::forecast(forecast::ets(y_ts), h = H_FUTURE)
  final_arima <- forecast::forecast(forecast::auto.arima(y_ts, seasonal = TRUE), h = H_FUTURE)
  
  last_date <- max(ts_total_df$Tanggal, na.rm = TRUE)
  future_dates <- seq(last_date %m+% months(1), by = "1 month", length.out = H_FUTURE)
  
  pack_fc <- function(fc, metode) {
    tibble(
      Tanggal = future_dates,
      Metode  = metode,
      Forecast = as.numeric(fc$mean),
      Lo80 = as.numeric(fc$lower[,1]),
      Hi80 = as.numeric(fc$upper[,1]),
      Lo95 = as.numeric(fc$lower[,2]),
      Hi95 = as.numeric(fc$upper[,2])
    )
  }
  
  fc_future_tbl <- bind_rows(
    pack_fc(final_holt, "Holt"),
    pack_fc(final_ets, "ETS"),
    pack_fc(final_arima, "ARIMA")
  ) %>% mutate(across(c(Forecast, Lo80, Hi80, Lo95, Hi95), ~ round(.x, 2)))
  
  # test dates (ambil 12 tanggal terakhir dari df)
  test_dates <- tail(ts_total_df$Tanggal, H_TEST)
  df_test <- tibble(
    Tanggal = test_dates,
    Actual  = actual,
    Holt    = as.numeric(fc_holt$mean),
    ETS     = as.numeric(fc_ets$mean),
    ARIMA   = as.numeric(fc_arima$mean)
  )
  
  list(
    ts_total = ts_total_df,
    y_ts = y_ts,
    adf = adf,
    kpss = kpss,
    metrics = metrics,
    df_test = df_test,
    fc_future_tbl = fc_future_tbl
  )
}

# ===============================
# 8) LOAD ALL (ONLY 4 FILES)
# ===============================
cs_raw <- NULL
ts_pack <- NULL
g_dki   <- NULL

if (file.exists(CASE_PATH) && file.exists(POP_PATH)) {
  cs_raw <- tryCatch(build_crosssection(CASE_PATH, POP_PATH),
                     error = function(e) { message("ERROR build_crosssection(): ", e$message); NULL })
}

if (file.exists(TS_PATH)) {
  ts_pack <- tryCatch(load_ts_monthly(TS_PATH),
                      error = function(e) { message("ERROR load TS: ", e$message); NULL })
}

if (file.exists(GEO_PATH)) {
  g_dki <- tryCatch(sf::st_read(GEO_PATH, quiet = TRUE),
                    error = function(e) { message("ERROR read GEO: ", e$message); NULL })
  if (!is.null(g_dki)) {
    name_col <- guess_name_col(g_dki)
    g_dki <- g_dki %>% mutate(Wilayah_std = std_wilayah(as.character(.data[[name_col]])))
  }
}

# ===============================
# 9) UI
# ===============================
ui <- dashboardPage(
  title = "Hepatitis A DKI Jakarta",
  skin = "black",
  
  dashboardHeader(
    title = span("HEPATITIS A DKI JAKARTA", style="font-weight:900; font-family:'Poppins',sans-serif; font-size:14px;"),
    titleWidth = 280
  ),
  
  dashboardSidebar(
    width = 280,
    sidebarMenu(
      id = "tabs",
      menuItem("Dashboard", tabName = "dashboard", icon = icon("chart-pie")),
      menuItem("Statistik Deskriptif", tabName = "stats", icon = icon("table")),
      menuItem("Peta IR/100k", tabName = "map", icon = icon("map")),
      menuItem("Temporal & Forecast", tabName = "forecast", icon = icon("chart-line")),
      menuItem("Database", tabName = "data", icon = icon("database")),
      hr(),
      div(style="padding: 0 15px 10px 15px;",
          h5("FILTER", style="color:#bfc7d5; font-weight:900; font-size:11px; letter-spacing:.7px; margin-bottom:10px;"),
          uiOutput("ui_filter_wilayah"),
          br(),
          uiOutput("ui_year_range"),
          br(),
          uiOutput("ui_map_year")
      )
    )
  ),
  
  dashboardBody(
    useShinyjs(),
    tags$head(
      tags$link(rel="stylesheet", href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700;800;900&display=swap"),
      tags$style(HTML("
        body, h1, h2, h3, h4, h5, h6, .main-header .logo { font-family:'Poppins',sans-serif !important; }
        .content-wrapper { background:#f4f6f9; }
        .skin-black .main-header .navbar { background: linear-gradient(90deg, #1f3a5f 0%, #2e86c1 100%); }
        .skin-black .main-header .logo { background:#1f3a5f; color:#fff; font-weight:900; }
        .skin-black .main-sidebar { background:#1d2b3a; }
        .box { border-radius:16px !important; border:none !important; box-shadow: 0 10px 26px rgba(16,24,40,.08) !important; overflow:hidden; }
        .box-title { font-weight:900 !important; }
        .small-box { border-radius:16px !important; box-shadow: 0 10px 26px rgba(16,24,40,.10) !important; }
        .leaflet-container { border-radius:14px; }
      "))
    ),
    
    tabItems(
      # ---------------- DASHBOARD
      tabItem(tabName = "dashboard",
              fluidRow(
                column(12, h3("Monitoring Epidemiologi Hepatitis A – DKI Jakarta",
                              style="font-weight:900; margin-top:0; color:#1f3a5f;"))
              ),
              fluidRow(
                valueBoxOutput("vbox_kasus", width = 3),
                valueBoxOutput("vbox_ir", width = 3),
                valueBoxOutput("vbox_toprisk", width = 3),
                valueBoxOutput("vbox_moran", width = 3)
              ),
              fluidRow(
                box(width = 8, title = "Tren IR/100k (Kab/Kota) – Cross Section", status = "primary", solidHeader = TRUE,
                    withSpinner(plotlyOutput("plot_trend_ir", height = "380px"), type = 4)
                ),
                box(width = 4, title = "Top 5 Risiko (IR) – Tahun Peta", status = "warning", solidHeader = TRUE,
                    tableOutput("top_rank_table")
                )
              )
      ),
      
      # ---------------- STATISTIK
      tabItem(tabName = "stats",
              fluidRow(
                column(12, h3("Statistik Deskriptif (Provinsi & Kab/Kota)",
                              style="font-weight:900; margin-top:0; color:#1f3a5f;"))
              ),
              fluidRow(
                box(width = 6, title = "Ringkasan Tahunan Provinsi (Agregasi)", status = "primary", solidHeader = TRUE,
                    dataTableOutput("tbl_prov_year")
                ),
                box(width = 6, title = "Deskriptif Provinsi (Min/Max/Mean/Median/Range)", status = "warning", solidHeader = TRUE,
                    tableOutput("tbl_desc_prov")
                )
              ),
              fluidRow(
                box(width = 12, title = "Deskriptif Kab/Kota (IR/100k) – Min/Max/Mean/Median/Range", status = "info", solidHeader = TRUE,
                    dataTableOutput("tbl_desc_kabkota")
                )
              ),
              fluidRow(
                box(width = 12, title = "Ranking IR Kab/Kota (per Tahun)", status = "danger", solidHeader = TRUE,
                    dataTableOutput("tbl_ranking")
                )
              )
      ),
      
      # ---------------- MAP
      tabItem(tabName = "map",
              fluidRow(
                column(12, h3("Peta Incidence Rate (IR/100k) – Tahun Terpilih",
                              style="font-weight:900; margin-top:0; color:#1f3a5f;"))
              ),
              fluidRow(
                box(width = 12, title = "Peta IR/100k", status = "danger", solidHeader = TRUE,
                    withSpinner(leafletOutput("epid_map", height = "520px"), type = 6)
                )
              )
      ),
      
      # ---------------- FORECAST
      tabItem(tabName = "forecast",
              fluidRow(
                column(12, h3("Analisis Temporal & Forecast 12 Bulan (Holt vs ETS vs ARIMA)",
                              style="font-weight:900; margin-top:0; color:#1f3a5f;"))
              ),
              fluidRow(
                box(width = 6, title = "Tren Bulanan Kasus (Total Provinsi) – TS File", status = "primary", solidHeader = TRUE,
                    withSpinner(plotlyOutput("plot_ts_total", height = "320px"), type = 4),
                    uiOutput("stationarity_box")
                ),
                box(width = 6, title = "Akurasi TEST (12 bulan terakhir)", status = "danger", solidHeader = TRUE,
                    dataTableOutput("tbl_metrics"),
                    p("Catatan: MAPE bisa besar saat Actual kecil. RMSE/MAE lebih stabil.",
                      style="font-size:12px; color:#667085; margin-top:10px;")
                )
              ),
              fluidRow(
                box(width = 12, title = "Prediksi TEST 12 bulan: Actual vs Holt/ETS/ARIMA", status = "info", solidHeader = TRUE,
                    withSpinner(plotlyOutput("plot_fc_test", height = "360px"), type = 6)
                )
              ),
              fluidRow(
                box(width = 12, title = "Forecast 12 Bulan ke Depan (Mean + Interval)", status = "success", solidHeader = TRUE,
                    withSpinner(plotlyOutput("plot_fc_future", height = "360px"), type = 6),
                    br(),
                    dataTableOutput("tbl_fc_future")
                )
              )
      ),
      
      # ---------------- DATABASE
      tabItem(tabName = "data",
              fluidRow(
                box(width = 12, title = "Database Cross-Section (Kasus, Penduduk, IR/100k)", status = "primary", solidHeader = TRUE,
                    dataTableOutput("raw_table_cs")
                )
              ),
              fluidRow(
                box(width = 12, title = "Database Time Series (Long) – dari TS File", status = "info", solidHeader = TRUE,
                    dataTableOutput("raw_table_ts_long")
                )
              ),
              fluidRow(
                box(width = 12, title = "Database Time Series (Total Provinsi)", status = "warning", solidHeader = TRUE,
                    dataTableOutput("raw_table_ts_total")
                )
              )
      )
    )
  )
)

# ===============================
# 10) SERVER
# ===============================
server <- function(input, output, session) {
  
  observe({
    if (is.null(cs_raw)) showNotification("ERROR: Cross-section gagal dibaca. Cek delimiter ';' dan nama kolom.", type="error", duration=12)
    if (is.null(ts_pack)) showNotification("ERROR: Time-series gagal dibaca. Pastikan TS punya kolom Tanggal + Total.", type="error", duration=12)
    if (is.null(g_dki)) showNotification("WARNING: GeoJSON gagal dibaca. Peta tidak tampil.", type="warning", duration=10)
  })
  
  cs_data <- reactive({
    validate(need(!is.null(cs_raw), "Cross-section belum terbaca."))
    cs_raw
  })
  
  ts_long <- reactive({
    validate(need(!is.null(ts_pack), "Time-series belum terbaca."))
    ts_pack$ts_long
  })
  
  ts_total <- reactive({
    validate(need(!is.null(ts_pack), "Time-series belum terbaca."))
    ts_pack$ts_total
  })
  
  # ---------- UI FILTERS
  output$ui_filter_wilayah <- renderUI({
    req(cs_data())
    wilayah_choices <- cs_data()$Wilayah %>%
      unique() %>%
      as.character() %>%
      sort()
    
    wilayah_choices <- wilayah_choices[!stringr::str_detect(toupper(wilayah_choices), "PROVINSI")]
    
    pickerInput("filter_wilayah", "Wilayah:",
                choices = c("SEMUA WILAYAH", wilayah_choices),
                selected = "SEMUA WILAYAH",
                options = list(`style`="btn-default", `live-search`=TRUE)
    )
  })
  
  
  output$ui_year_range <- renderUI({
    req(cs_data())
    yrs <- sort(unique(cs_data()$year))
    sliderTextInput("year_range", "Rentang Tahun:",
                    choices = yrs, selected = c(min(yrs), max(yrs)), grid = TRUE
    )
  })
  
  output$ui_map_year <- renderUI({
    req(cs_data())
    yrs <- sort(unique(cs_data()$year))
    sliderTextInput("map_year", "Tahun Peta:",
                    choices = yrs, selected = max(yrs), grid = TRUE
    )
  })
  
  filtered_cs <- reactive({
    req(cs_data(), input$year_range)
    df <- cs_data() %>% filter(year >= input$year_range[1], year <= input$year_range[2])
    if (!is.null(input$filter_wilayah) && input$filter_wilayah != "SEMUA WILAYAH") {
      df <- df %>% filter(Wilayah == input$filter_wilayah)
    }
    df
  })
  
  # ---------- PROVINSI (agregasi semua kab/kota) per tahun
  prov_year <- reactive({
    req(cs_data(), input$year_range)
    cs_data() %>%
      filter(year >= input$year_range[1], year <= input$year_range[2]) %>%
      group_by(year) %>%
      summarise(
        Cases = sum(Cases, na.rm = TRUE),
        Population = sum(Population, na.rm = TRUE),
        IR_100k = if_else(Population > 0, (Cases/Population)*100000, NA_real_),
        .groups = "drop"
      ) %>%
      arrange(year) %>%
      mutate(IR_100k = round(IR_100k, 3))
  })
  
  # =========================
  # VALUE BOXES
  # =========================
  output$vbox_kasus <- renderValueBox({
    req(filtered_cs())
    valueBox(comma(sum(filtered_cs()$Cases, na.rm=TRUE)), "Total Kasus (filter)", icon=icon("hospital-user"), color="aqua")
  })
  
  output$vbox_ir <- renderValueBox({
    req(filtered_cs())
    valueBox(sprintf("%.2f", mean(filtered_cs()$IR_100k, na.rm=TRUE)), "Rata-rata IR/100k", icon=icon("chart-line"), color="blue")
  })
  
  output$vbox_toprisk <- renderValueBox({
    req(cs_data(), input$map_year)
    top1 <- cs_data() %>%
      filter(year == input$map_year) %>%
      filter(!str_detect(toupper(Wilayah), "PROVINSI")) %>%
      arrange(desc(IR_100k)) %>%
      slice(1)
    label <- if (nrow(top1)==1) paste0(top1$Wilayah, " (", sprintf("%.2f", top1$IR_100k), ")") else "-"
    valueBox(label, paste0("Top IR (", input$map_year, ")"), icon=icon("triangle-exclamation"), color="yellow")
  })
  
  output$vbox_moran <- renderValueBox({
    req(cs_data(), input$map_year)
    validate(need(!is.null(g_dki), "GeoJSON belum tersedia."))
    
    map_df <- cs_data() %>%
      filter(year == input$map_year) %>%
      filter(!str_detect(toupper(Wilayah), "PROVINSI")) %>%
      transmute(Wilayah_std, IR_100k)
    
    g_map <- g_dki %>% left_join(map_df, by="Wilayah_std")
    moran_data <- g_map %>% filter(!is.na(IR_100k))
    
    if (nrow(moran_data) < 3) {
      valueBox("N/A", "Moran’s I", icon=icon("globe"), color="red")
    } else {
      nb <- spdep::poly2nb(moran_data, queen = TRUE)
      lw <- spdep::nb2listw(nb, style="W", zero.policy = TRUE)
      mor <- spdep::moran.test(moran_data$IR_100k, lw, zero.policy = TRUE)
      label <- paste0(sprintf("%.3f", mor$estimate[["Moran I statistic"]]), " | p=", sprintf("%.3f", mor$p.value))
      valueBox(label, paste0("Moran’s I (", input$map_year, ")"), icon=icon("globe"), color="red")
    }
  })
  
  # =========================
  # DASHBOARD: TREND IR
  # =========================
  output$plot_trend_ir <- renderPlotly({
    req(cs_data(), input$year_range)
    df <- cs_data() %>%
      filter(!str_detect(toupper(Wilayah), "PROVINSI")) %>%
      filter(year >= input$year_range[1], year <= input$year_range[2])
    
    if (!is.null(input$filter_wilayah) && input$filter_wilayah != "SEMUA WILAYAH") {
      df <- df %>% filter(Wilayah == input$filter_wilayah)
    }
    
    p <- ggplot(df, aes(x=year, y=IR_100k, color=Wilayah, group=Wilayah)) +
      geom_line(linewidth=1) + geom_point(size=2) +
      theme_minimal() +
      labs(x="Tahun", y="IR per 100.000", color="Wilayah") +
      scale_x_continuous(breaks=seq(min(df$year), max(df$year), 1))
    
    ggplotly(p) %>% config(displayModeBar = FALSE)
  })
  
  output$top_rank_table <- renderTable({
    req(cs_data(), input$map_year)
    cs_data() %>%
      filter(year == input$map_year) %>%
      filter(!str_detect(toupper(Wilayah), "PROVINSI")) %>%
      arrange(desc(IR_100k)) %>%
      head(5) %>%
      transmute(Wilayah, Kasus=Cases, `IR/100k`=round(IR_100k, 2))
  }, striped=TRUE)
  
  # =========================
  # STATISTIK
  # =========================
  output$tbl_prov_year <- renderDataTable({
    req(prov_year())
    datatable(prov_year(), rownames=FALSE, options=list(pageLength=10, scrollX=TRUE))
  })
  
  output$tbl_desc_prov <- renderTable({
    req(prov_year())
    py <- prov_year()
    tibble(
      Indikator = c("Minimum","Maksimum","Mean","Median","Range"),
      Kasus = c(min(py$Cases, na.rm=TRUE),
                max(py$Cases, na.rm=TRUE),
                mean(py$Cases, na.rm=TRUE),
                median(py$Cases, na.rm=TRUE),
                max(py$Cases, na.rm=TRUE) - min(py$Cases, na.rm=TRUE)),
      IR_100k = round(c(min(py$IR_100k, na.rm=TRUE),
                        max(py$IR_100k, na.rm=TRUE),
                        mean(py$IR_100k, na.rm=TRUE),
                        median(py$IR_100k, na.rm=TRUE),
                        max(py$IR_100k, na.rm=TRUE) - min(py$IR_100k, na.rm=TRUE)), 3)
    )
  }, striped=TRUE, width="100%")
  
  output$tbl_desc_kabkota <- renderDataTable({
    req(cs_data())
    kk <- cs_data() %>%
      filter(!str_detect(toupper(Wilayah), "PROVINSI")) %>%
      group_by(Wilayah) %>%
      summarise(
        IR_min    = min(IR_100k, na.rm=TRUE),
        IR_max    = max(IR_100k, na.rm=TRUE),
        IR_mean   = mean(IR_100k, na.rm=TRUE),
        IR_median = median(IR_100k, na.rm=TRUE),
        IR_range  = IR_max - IR_min,
        .groups="drop"
      ) %>%
      mutate(across(starts_with("IR_"), ~ round(.x, 3))) %>%
      arrange(desc(IR_mean))
    
    datatable(kk, rownames=FALSE, options=list(pageLength=10, scrollX=TRUE))
  })
  
  output$tbl_ranking <- renderDataTable({
    req(cs_data(), input$year_range)
    df <- cs_data() %>%
      filter(!str_detect(toupper(Wilayah), "PROVINSI")) %>%
      filter(year >= input$year_range[1], year <= input$year_range[2]) %>%
      group_by(year) %>%
      arrange(desc(IR_100k), .by_group = TRUE) %>%
      mutate(Rank = row_number()) %>%
      ungroup() %>%
      transmute(year, Rank, Wilayah, Kasus=Cases, Penduduk=Population, IR_100k=round(IR_100k, 3)) %>%
      arrange(year, Rank)
    
    datatable(df, rownames=FALSE, options=list(pageLength=12, scrollX=TRUE))
  })
  
  # =========================
  # MAP
  # =========================
  output$epid_map <- renderLeaflet({
    req(cs_data(), input$map_year)
    validate(need(!is.null(g_dki), "GeoJSON belum terbaca. Pastikan id-jk.min.geojson ada di data/."))
    
    data_year <- cs_data() %>%
      filter(year == input$map_year) %>%
      filter(!str_detect(toupper(Wilayah), "PROVINSI")) %>%
      transmute(Wilayah_std, Cases, Population, IR_100k)
    
    shp <- g_dki %>% left_join(data_year, by="Wilayah_std")
    pal <- colorNumeric(palette="Reds", domain=cs_data()$IR_100k, na.color="#f2f4f7")
    
    leaflet(shp) %>%
      addProviderTiles(providers$CartoDB.Positron) %>%
      addPolygons(
        fillColor = ~pal(IR_100k),
        weight=2, opacity=1, color="white", fillOpacity=0.88,
        highlight = highlightOptions(weight=4, color="#e74c3c", bringToFront=TRUE),
        
        # TOOLTIP saat hover
        label = ~lapply(
          paste0(
            "
", "", Wilayah_std, "
", "Tahun: ", input$map_year, "
", "Kasus: ", scales::comma(Cases), "
", "Penduduk: ", scales::comma(Population), "
", "IR/100k: ", sprintf('%.2f', IR_100k), "
" ), htmltools::HTML ), labelOptions = labelOptions( direction = "auto", sticky = TRUE, opacity = 0.95, textsize = "12px", style = list( "background-color" = "white", "border" = "1px solid rgba(0,0,0,0.15)", "border-radius" = "10px", "padding" = "8px 10px", "box-shadow" = "0 8px 18px rgba(0,0,0,0.12)" ) ), # popup (klik) boleh tetap ada, biar lengkap popup = ~paste0( "", Wilayah_std, "
", "Tahun: ", input$map_year, "
", "Kasus: ", scales::comma(Cases), "
", "Penduduk: ", scales::comma(Population), "
", "IR/100k: ", sprintf('%.2f', IR_100k) ) )}) # ========================= # FORECAST # ========================= fc_obj <- reactive({ validate(need(!is.null(ts_pack), "Time-series belum terbaca.")) compute_forecast_objects(ts_total()) }) output$plot_ts_total <- renderPlotly({ obj <- fc_obj() df <- obj$ts_total p <- ggplot(df, aes(x=Tanggal, y=Cases)) + geom_line(linewidth=1) + theme_minimal() + labs(x="Tanggal", y="Kasus (Total)", title="Tren Bulanan Kasus Hepatitis A (Total Provinsi)") ggplotly(p) %>% config(displayModeBar=FALSE) }) output$stationarity_box <- renderUI({ obj <- fc_obj() adf_p <- if (!is.null(obj$adf)) obj$adf$p.value else NA_real_ kpss_p <- if (!is.null(obj$kpss)) obj$kpss$p.value else NA_real_ div(style="margin-top:10px; font-size:12px; color:#475467;", tags$div(strong("Uji stasioneritas (ringkas):")), tags$div(paste0("ADF p-value: ", ifelse(is.na(adf_p), "NA", sprintf("%.4f", adf_p)), " | KPSS p-value: ", ifelse(is.na(kpss_p), "NA", sprintf("%.4f", kpss_p)))), tags$div("Interpretasi cepat: ADF besar & KPSS kecil → cenderung tidak stasioner.") ) }) output$tbl_metrics <- renderDataTable({ obj <- fc_obj() datatable(obj$metrics %>% mutate(across(c(RMSE, MAE, MAPE), ~ round(.x, 3))), rownames=FALSE, options=list(dom="t", pageLength=5)) }) output$plot_fc_test <- renderPlotly({ obj <- fc_obj() df_line <- obj$df_test %>% pivot_longer(cols=c("Holt","ETS","ARIMA"), names_to="Metode", values_to="Pred") p <- ggplot() + geom_line(data=obj$df_test, aes(x=Tanggal, y=Actual), linewidth=1.2) + geom_line(data=df_line, aes(x=Tanggal, y=Pred, color=Metode), linewidth=1) + theme_minimal() + labs(title="Forecast TEST 12 Bulan: Actual vs Holt/ETS/ARIMA", x="Tanggal", y="Kasus") ggplotly(p) %>% config(displayModeBar=FALSE) }) output$plot_fc_future <- renderPlotly({ obj <- fc_obj() fc <- obj$fc_future_tbl p <- ggplot(fc, aes(x=Tanggal, y=Forecast, color=Metode)) + geom_line(linewidth=1) + theme_minimal() + labs(title="Forecast 12 Bulan ke Depan (Mean)", x="Tanggal", y="Kasus") ggplotly(p) %>% config(displayModeBar=FALSE) }) output$tbl_fc_future <- renderDataTable({ obj <- fc_obj() datatable(obj$fc_future_tbl, rownames=FALSE, extensions="Buttons", options=list(dom="Bfrtip", buttons=c("copy","csv","excel","pdf","print"), pageLength=12, scrollX=TRUE)) }) # ========================= # DATABASE # ========================= output$raw_table_cs <- renderDataTable({ req(cs_data()) df <- cs_data() %>% select(Wilayah, year, Cases, Population, IR_100k) %>% mutate(IR_100k = round(IR_100k, 3)) datatable(df, filter="top", extensions="Buttons", options=list(dom="Bfrtip", buttons=c("copy","csv","excel","pdf","print"), pageLength=12, scrollX=TRUE), rownames=FALSE) }) output$raw_table_ts_long <- renderDataTable({ req(ts_long()) df <- ts_long() %>% select(Tanggal, Wilayah, Cases) %>% arrange(Tanggal, Wilayah) datatable(df, filter="top", options=list(pageLength=12, scrollX=TRUE), rownames=FALSE) }) output$raw_table_ts_total <- renderDataTable({ req(ts_total()) datatable(ts_total(), filter="top", options=list(pageLength=12, scrollX=TRUE), rownames=FALSE) }) } # =============================== # 11) RUN APP # =============================== shinyApp(ui, server)


  1. Data Penelitian

Bagian ini menyajikan dataset utama yang digunakan dalam seluruh analisis penelitian ini. Data meliputi jumlah kasus tahunan dan bulanan Hepatitis A serta data kependudukan di Provinsi DKI Jakarta periode 2017–2025. Selain itu, dilampirkan pula sampel data indikator kesehatan dan sosial ekonomi tingkat kabupaten/kota sebagai referensi pengembangan model di masa mendatang.

Tabel Lampiran 5.1. Data Kasus Hepatitis A per Wilayah di DKI Jakarta (2017–2025)
Wilayah 2017 2018 2019 2020 2021 2022 2023 2024 2025
Jakarta Pusat 35 32 43 27 6 13 11 10 10
Jakarta Utara 11 165 137 84 16 65 62 47 48
Jakarta Barat 75 83 140 69 16 18 27 45 118
Jakarta Selatan 109 107 255 73 19 18 11 18 13
Jakarta Timur 89 66 110 50 17 25 36 26 31
Kab. Kep. Seribu 0 0 0 0 0 0 0 0 0
TOTAL PROVINSI 319 453 685 303 74 139 147 146 220


Tabel Lampiran 5.2. Data Jumlah Penduduk per Wilayah di DKI Jakarta (2017–2025)
Wilayah 2017 2018 2019 2020 2025
Jakarta Pusat 921.344 924.686 928.109 1.056.896 1.038.396
Jakarta Utara 1.781.316 1.797.292 1.812.915 1.778.981 1.819.009
Jakarta Barat 2.528.065 2.559.362 2.589.933 2.434.511 2.487.199
Jakarta Selatan 2.226.830 2.246.137 2.264.699 2.226.812 2.219.225
Jakarta Timur 2.892.783 2.916.018 2.937.859 3.037.139 3.085.058
Kab. Kep. Seribu 23.897 24.134 24.295 27.749 29.088
TOTAL PROVINSI 10.374.235 10.467.629 10.557.810 10.562.088 10.677.975

Data kependudukan di atas merupakan populasi berisiko (population at risk) yang digunakan sebagai denominator dalam perhitungan Incidence Rate (IR) per 100.000 penduduk guna melakukan perbandingan risiko antarwilayah secara objektif.

Tabel Lampiran 5.3. Sampel Data Indikator Pembangunan dan Kesehatan Masyarakat
No Wilayah Kasus HIV Penduduk Pengangguran (%) Kemiskinan (%) IPM
1 Kab. Cilacap 57 2.007.829 8,74 10,99 72,04
2 Kab. Banyumas 88 1.828.573 6,35 12,53 73,96
3 Kab. Purbalingga 55 1.027.333 5,61 14,99 70,51
4 Kab. Banjarnegara 64 1.047.226 6,26 14,90 69,16
5 Kab. Kebumen 57 1.397.555 5,11 16,34 71,88
36 PROVINSI JATENG 1.608 37.540.000 5,13 10,77 73,39

Seluruh data yang tercantum dalam lampiran ini telah diverifikasi kelengkapannya melalui proses pembersihan data (data cleaning) sebelum digunakan dalam pemodelan deret waktu (ARIMA, ETS, Holt) serta analisis autokorelasi spasial Moran’s I.

SELESAI.